Skip to content

refactor(engine): output inversion batch 2 — the lifecycle cluster loses its terminal imports (#429) - #461

Merged
justrach merged 1 commit into
release/0.0.242from
feat/429-batch2-output-inversion
Aug 6, 2026
Merged

refactor(engine): output inversion batch 2 — the lifecycle cluster loses its terminal imports (#429)#461
justrach merged 1 commit into
release/0.0.242from
feat/429-batch2-output-inversion

Conversation

@justrach

@justrach justrach commented Aug 6, 2026

Copy link
Copy Markdown
Owner

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

file offending imports lines
session_start.zig 3 → 0 (anim, ansi, pickers) 461 → 465
session_run.zig 5 → 0 (approvals, anim, ansi, pickers, term) 597 → 450
providers.zig 1 → 0 (ansi) 589 → 586
hooks.zig 2 → 0 (approvals, term) 271 → 270

Two of those imports (anim/pickers in session_start) were already dead on main.

session_run.zig dropping 597 → 450 matters beyond this PR. It was at 599/600 with #440 applied, i.e. one line from the hard cap. This is the epic's claim that inversion "relieves 600-line cap pressure in exactly the files pinned at it", realized.

Byte-identity: verified against raw terminal bytes

Two binaries built from git archive main and from this branch, run under a real PTY in identically seeded workspaces, diffing raw bytes including escape codes (only the random trace id, unix-ms session name and timings normalized).

17 scenarios, all identical: banner in colour and no-colour, worktree line, YOLO badge, displays-on, both MCP invalid-config reports, the unsupported-config path, the consent prompt answered y and declined, skipped-servers, companion auto-connect failure, approvals+hooks loaded, saved-model-unavailable both blocked and unblocked, --selftest-spinner, --selftest-markdown, fatal one-shot, --json, and a real turn against scripts/openai_mock.py down to the ↩ session saved → line.

Transcripts were dumped to confirm the harness actually renders the converted lines rather than comparing two empty runs, e.g. \x1b[31m⚠ YOLO\x1b[0m mode (--yolo): …. Exact-byte unit tests cover what the PTY cannot reach: provider_fallback (terminal line, stderr fallback, and the {"seq":N,"type":"model",…} wire line), the save-failed warning, and every notice tone.

scripts/test-tty-release-on-exit.py (the #396 guard, which is precisely what run_finished re-plumbs) passes.

Eight new events

session_notice (a shared shape reused at 11 call sites, where tone is the only delegated rendering choice and lead carries a coloured badge), plus session_banner, worktree_entered, saved_model_unavailable, mcp_consent_prompt, provider_fallback, session_saved, run_finished. Only provider_fallback is durable — it has always been the wire's model event. Structured variants exist only where inline styling or frontend-relevant fields forced them; everything else reuses Notice.

Two design points worth review attention:

  • engine_sink.writerSink(?*Io.Writer) is new machinery rather than a second pattern. This cluster fires before an Agent exists and after it stops mattering, so forAgent has nothing to hand it. A null writer (one-shot, acp) is a normal state; only the failover notice speaks then, to stderr, as before.
  • enableColor() crosses the boundary as a call, not an event — it settles what the palette is before anything is drawn.

Left for #430, deliberately

session_start.initRegistryConsent still reads the consent answer inline via in.takeDelimiter. Only the question is inverted; the read is input-shaped and belongs to input inversion. mcp_consent_prompt is marked TRANSITIONAL in the vocabulary so #430 replaces it rather than extending it.

Verification

zig build test 1052/1052 (baseline 1043, +9). scripts/eval-tier1.sh green. Windows cross-build (-Dtarget=x86_64-windows-gnu) clean, which matters here because the win32 shim moved files.

Integration note

This branch and #458 (batch 3) both edit engine_events.zig and engine_sink.zig substantially in opposite directions (batch 2: sink 528→327, events 330→449; batch 3: events 330→420, sink 528→533). They will conflict and are being integrated deliberately rather than merged independently. Batch 4 (agent.zig) should be cut from the integrated result, since it depends on the vocabulary both batches extend.

scripts/test-model-preference.py fails with exit 127 on this branch and on main's tree — a pre-existing environment problem with that script, not a regression.

Batch 2 of #429: session_start, session_run, providers and hooks lose every
terminal-cluster import (3/5/1/2 -> 0/0/0/0). What they used to print is a
typed event now, rendered by session_render.zig — the lifecycle's terminal
half, sibling of agent_stream_render.zig and agent_tool_render.zig.

Eight new variants, each a moment the vocabulary had no way to say:

  session_notice          the shared shape, reused at eleven call sites so the
                          cluster does not need a variant per line. `tone` is
                          the only rendering choice delegated (it is about
                          meaning); `lead` carries a badge like "⚠ YOLO" that
                          is colored apart from the rest of its line.
  session_banner          the startup line. The key hints on it are frontend
                          knowledge and moved into the sink.
  worktree_entered        -w entered its scratch checkout; inline styling on
                          the path forces structure.
  saved_model_unavailable startup's twin of provider_fallback. Fields, not
                          prose: a frontend offering "use it anyway" needs them.
  mcp_consent_prompt      TRANSITIONAL — only the QUESTION is inverted; the
                          stdin read stays for #430.
  provider_fallback       the one durable variant, because this moment has
                          always been the wire's `model` event. The wire's
                          fixed note and the terminal's context note differ,
                          and that difference now lives at the one translation
                          point instead of at the call site.
  session_saved           the durable session file landed.
  run_finished            #396's terminal handoff: the engine says the run is
                          over, a terminal frontend hands raw mode back.

Two of those four files printed nothing at all — hooks.zig wanted only the
settings path and one Windows pipe peek, so those move to leaves
(harness_settings.zig, win_api.zig) that approvals.zig and term.zig re-export.
session_run's settings/theme phase does legitimately draw, so it moves whole
to session_settings.zig rather than pretending otherwise.

engine_sink.zig gains a writer-backed sink: this cluster runs before an Agent
exists and after it stops mattering, and a null writer (a one-shot, `acp`) is
a normal state, not an error. Its tests move to engine_sink_tests.zig — the
file was one line under the ceiling.

Line cap relieved where #422 said it would be: session_run 597 -> 450,
session_start 461 -> 465, providers 589 -> 586, engine_sink 528 -> 327,
term 564 -> 519. Nothing over 600.

Proven byte-identical: 17 scenarios run under a real PTY against a binary
built from main and one built from this branch, diffing RAW terminal bytes,
escape codes included — banner, worktree, YOLO badge, displays-on, both MCP
config reports, the consent prompt answered both ways, skipped servers,
companion auto-connect failure, approvals/hooks loaded, saved-model-unavailable
(blocked and not), both selftests, a fatal one-shot, --json, and a real turn
through the offline mock down to the session-saved line. All identical.
1052/1052 tests pass (1043 before, +9), tier 1 green, Windows cross-build clean.

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 d583176 into release/0.0.242 Aug 6, 2026
6 checks passed
@justrach
justrach deleted the feat/429-batch2-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