Skip to content

refactor(engine): output inversion batch 3 — the long tail loses its terminal imports (#429) - #458

Merged
justrach merged 2 commits into
release/0.0.242from
feat/429-batch3-output-inversion
Aug 6, 2026
Merged

refactor(engine): output inversion batch 3 — the long tail loses its terminal imports (#429)#458
justrach merged 2 commits into
release/0.0.242from
feat/429-batch3-output-inversion

Conversation

@justrach

@justrach justrach commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Batch 3 of #429 (part of epic #422). Five files, zero banned imports remaining in each.

file banned imports lines
skills.zig 1 → 0 408 → 408
skill_docs.zig 2 → 0 494 → 446
review.zig 1 → 0 142 → 143
imagegen.zig 1 → 0 487 → 488
agent_prompt.zig 2 → 0 231 → 108

The finding that shaped the batch, and that the epic should absorb

Four of the seven offending imports were approvals.zig, and not one of them was a terminal write. approvals.zig is already a pure std leaf. It is on the ban list because it is the consent surface (the mutable allow-list a human grows by answering y/n), not because it drags in the terminal. What these files actually wanted was policy: settings_path for skills/skill_docs, confinedPath/noSymlinkEscape for imagegen, readOnlyAllowed for review.

So the pure half is split into harness_policy.zig as a move+alias, every decl re-exported unqualified so method bodies and all Approvals.x call sites elsewhere are untouched, with a test asserting the re-exports really are aliases rather than a second copy.

This means batch 1 is not at zero either. exec, tools, edit_verify and agent_tool_gate still import approvals, and agent_tools still imports term. They can now switch to harness_policy for free. Worth reflecting in the epic's ratchet, which currently counts a policy import as if it were terminal coupling.

One new event

prompt_ready: PromptStatus, carrying CostMeter (off / flat-rate / unpriced / a figure), ContextMeter, and vocabulary-owned ReasoningEffort / PrivacyTier enums, so a transport-split sink need not import the engine. Exhaustive switches at the emit site, with a test pinning both enum sets in step. It is a presentation pulse: the wire never had a prompt, and prompt() still returns early on json_mode rather than relying on a silent sink.

skill_docs.zig deliberately got a relocation, not events. Its ansi use is the /skills command surface, whose other half (companion rows) is printed inline by commands_session.zig. Routing one half of one command through a contract with no wire shape, while the other half stays inline, would manufacture exactly the divergence #422 exists to remove.

Byte-identity: verified, with a harness that had to be built

There is no committed golden harness. The phrase traces to a CHANGELOG entry describing an ad-hoc before/after run; nothing under scripts/ implements it. So one was built: a scripted-model PTY session at three pane widths (160/60/34) capturing the status line with and without usage, the /skills catalog, and remove/add/unknown paths — 20 windows, raw bytes plus rendered text, 40 artifacts.

  • Harness determinism proven first: two runs of the same binary produced identical output.
  • diff -rq before after: no differences. All 40 artifacts byte-identical, re-verified after zig fmt rewrote a file.
  • The first pass showed a 12-byte delta (ESC[?2004h ESC[6n, readline's terminal setup) landing inside one capture window. Diagnosed as a capture-window race, fixed with a settle before opening each window, after which both binaries matched exactly. Recorded rather than waved away.

Tests

zig build test exit 0, 1051 passing. scripts/eval-tier1.sh green: fmt, 600-line ceiling, reachability (1016 declared tests compiled in), build, suite, 9 named invariants, SDK sync. commands_session.zig stays at exactly 600 — its existing import line was repointed rather than a new one added.

Behavioural delta worth flagging

promptLine swallows a write error where prompt() used to propagate it, because a sink emit returns void. Every other TuiSink branch already behaves this way and the only path there is a terminal that vanished mid-prompt, but it is a real, if pathological, difference.

Not covered

NO_COLOR rendering has no PTY golden: with NO_COLOR=1 this build never draws the interactive prompt over a PTY at all (pre-existing, unrelated to this change). Covered instead by in-tree tests that zero ansi.style and assert the whole line byte-for-byte at four widths. --json was not exercised live; no new event is durable (asserted by test) and prompt() returns early in json_mode, so the wire is untouched by construction rather than by observation.

The anyOf output this batch saw on baseline runs is the display artifact diagnosed in #444 (PR #456), not a real failure.

Batch 3 of #429: skills.zig, skill_docs.zig, review.zig, imagegen.zig and
agent_prompt.zig go from 1/2/1/1/2 banned imports to zero. Only one of the
seven was an actual terminal write, so the batch splits three ways.

Four of the seven were approvals.zig, and none of them wanted the approval
gate: skills and skill_docs want the settings-file path, imagegen wants
confinedPath/noSymlinkEscape, review wants readOnlyAllowed. approvals.zig is
already terminal-free — it is on the ratchet's list because it is the CONSENT
surface, the mutable allow-list a human grows by answering y/n. So the pure
half moves to harness_policy.zig (settings location, path confinement, the
command classifiers) and approvals.zig keeps the session state. A pure move:
every decl is re-exported unqualified, so method bodies and `Approvals.x` call
sites are untouched, and a new test asserts the re-exports ARE the aliases
rather than a second copy — a security predicate that drifts is a hole.

agent_prompt.zig is the real inversion. prompt() now gathers the session into
one `.prompt_ready` event and emits it; the palette, the badge frame and the
#209 width budget move to agent_prompt_render.zig behind TuiSink, the same
shape agent_stream_render/agent_tool_render have. One new variant and its
payload types:

  prompt_ready: PromptStatus  — what the status line SAYS: model, provider,
    cwd, the mode badges, and the meters as values (CostMeter distinguishes
    off / flat-rate / unpriced / a real figure; ContextMeter carries tokens,
    window and the compaction threshold). No widths, no colors, no assembled
    segments — which badges survive a narrow pane is a rendering decision.
    A presentation pulse: the wire has never had a prompt, because a --json
    client drives its own turns, and the emit site still returns early on
    json_mode rather than relying on a silent sink.

ReasoningEffort and PrivacyTier are the vocabulary's own enums rather than
main.zig's and learning_privacy's, so a transport-split sink can read an event
without importing the engine; the emit site maps with exhaustive switches and
a test pins the two sets in step.

skill_docs.zig gets a relocation, not an event pair, and deliberately: its
ansi use was the `/skills` command surface, whose other half (the companion
rows) is printed inline by commands_session.zig. Routing one half of one
command through a contract with no wire shape for it, leaving the other half
inline, would manufacture exactly the divergence #422 exists to remove. The
command layer is frontend territory, so handleRemove/handleAdd/printSection
move whole to skill_docs_render.zig and skill_docs.zig goes back to being the
skill subsystem its own header describes. commands_session.zig stays at 600
lines by repointing its existing import rather than adding one.

One behavioral note: promptLine swallows a write error where prompt() used to
propagate it, because a sink emit returns void. Every other TuiSink branch
already behaves that way, and the only path there is a terminal that vanished
mid-prompt.

Proven byte-identical by a golden before/after harness: a scripted-model PTY
session at three pane widths (160/60/34), capturing the status line with and
without usage, the `/skills` catalog, and remove/add/unknown — 40 artifacts,
raw bytes and rendered text, all identical between the pre- and post-change
binaries. Capture windows open only after a turn has settled, so no spinner
frame is inside one and every .raw is deterministic (verified by two runs of
the same binary agreeing). The layout is additionally pinned in-tree: the
renderer's tests assert the whole line byte-for-byte at four widths, including
the #209 shed order and the rule that the cache badge never floats free of the
context meter. tier 1 green; 1051 tests pass.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
The instrument that proved batch 3 behavior-preserving lived only in a session
scratchpad, which made its verdict unreproducible the moment that session
ended. Three separate issues depend on it — #429's remaining batches ride the
same bar, #430's acceptance is golden byte-identity in both modes, and #431's
attach-parity eval is this harness pointed at two transports — so it moves to
scripts/eval/golden/ in the shape scripts/eval/live-ab/ established:

- both arms' binaries come from GRAFF_EVAL_BEFORE / GRAFF_EVAL_AFTER, the same
  variables the live-ab harness uses, resolved lazily so capturing one arm
  needs only that arm's variable, and failing with an actionable message
  rather than a traceback. No path in the file is absolute: ROOT derives from
  __file__ and the shared PTY driver is imported through it;
- the determinism self-check is a first-class step and gates everything after
  it. The same binary is captured twice and must agree before any before/after
  diff is believed, because PTY capture has several ways not to be stable and
  a diff from an unstable instrument cannot be attributed;
- the session script is data (STEPS x WIDTHS), so the next batch adds a window
  rather than rewriting the driver;
- runs/ is gitignored. Only the instrument ships.

The README writes down the capture-window race that cost real time here: a
window opened at `cursor = len(raw)` straight after wait_for_literal can fall
on either side of readline's 12 setup bytes, and the failure is stable per run,
so one arm can look self-consistent while the two arms differ by a prefix with
byte-identical content behind it. Every step now settles before its window
opens. The known gaps are documented too rather than left to be rediscovered:
no NO_COLOR arm (this build draws no prompt under it), no --json arm (these
surfaces have no wire shape), and no approval prompt (#430's, not this).

Verified end-to-end on batch 3's two binaries: self-check green, 40 artifacts
byte-identical, exit 0. Also verified it FAILS correctly — a tampered artifact
and a deleted one are both reported, exit 1 — since an instrument that always
says "identical" is worse than none. zig build test green; tier 1 green.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
@justrach
justrach changed the base branch from main to release/0.0.242 August 6, 2026 12:32
@justrach
justrach merged commit 3eaa466 into release/0.0.242 Aug 6, 2026
6 checks passed
@justrach
justrach deleted the feat/429-batch3-output-inversion branch August 6, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant