Files
stack/agents/darkwing/work/queue-55/BUILD.md
T
jason.woltjeandClaude Opus 5.5 4acadaae4e review(55): Sage's dispatch test ruling and the transport follow-up note
BUILD.md records Sage's 2026-10-11 ruling that packages/queue/tests/
dispatch.test.mjs is in scope. FOLLOWUP-transport-code.md writes up the
transport's did-not-finish error with no .code, for the 10-12 batch.
The candidate (manifest 93fa48c1cdfe) is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 12:54:43 -05:00

11 KiB
Raw Blame History

Row 55 (#1544): the full-screen terminal app, candidate packet, round 1

Author: Darkwing. Reviewers: Dewey and Filbert. Brief: docs/plans/2026-10-10_design-implementation.md, section "Full-screen terminal app", with docs/design/IMPLEMENTING.md "The full-screen terminal app" and "Strings to keep exactly". Base: fe5c68fa (row 41, base.txt). It applies unchanged to 23d566d6: the commits between the two touch only BUILD-LOG.md, docs/SESSIONS.md and the queue files, none of them in this candidate. The feature is uncommitted; only this packet is committed. I pushed nothing.

Files

files.txt lists the nine repository paths. candidate-manifest.sha256 has their sha256. build.patch is git diff --cached --binary fe5c68fa over them: 9 files, +1363/−16.

File Change
packages/cli/src/app.mjs new: key parsing, the App state machine, runApp (alternate screen, raw mode, resize, SIGTERM, restore) and the quit line
packages/cli/src/screens.mjs new: draw(state) is pure and returns rows; Inbox, Tasks, Agents, Trail, Help, overlays, the 16-colour palette and NO_COLOR
packages/cli/src/cli.mjs the decide flow moves into the exported resolveDecision, which decide and the app share; SEAT_USAGE; the no-argument entry app()
packages/cli/src/format.mjs trailSummary is exported (the Trail screen uses it); no other change
packages/cli/tests/app.test.mjs new: 14 tests, listed below
packages/cli/README.md the intro names row 55; new section "The app"
docs/TOOLS.md the mosaic entry: the no-argument line and a four-line paragraph
scripts/mosaic a decided scope addition (Sage, 2026-10-11); see below
packages/queue/tests/dispatch.test.mjs follows from scripts/mosaic; in scope (Sage, 2026-10-11); see below

resolveDecision writes the same lines in the same order and asks the same question as before. decide passes ask = null for --yes and keeps its own "stdin is not a terminal" refusal. The 83 tests that were in packages/cli/tests/ at the base still pass unchanged.

Scope addition: scripts/mosaic

scripts/mosaic is not in the brief's files. Sage approved it on 2026-10-11 as a decided scope addition. Before, scripts/mosaic with no argument fell through to packages/seat/src/cli.mjs, which printed refused: <seat usage> and exited 4. The script never reached packages/cli, so the brief's acceptance line, bare mosaic on a TTY opens the app, could not be met inside packages/cli alone. The change is one case that sends no argument to packages/cli/src/cli.mjs. Nothing else in the script changed.

The entry checks in Sage's order:

  1. Agent markers exit 3 through refuseInsideAgent, with the transport's message unchanged.
  2. stdin or stdout not a TTY exits 4 with the usage.
  3. Otherwise the app opens.

Test 11 below covers each branch through main([]). Test 12 spawns scripts/mosaic itself for exits 4 and 3.

The queue dispatch test

My first gate run had test-queue red, 26 passed and 1 failed (out/round1-first-gate/test-queue.log). packages/queue/tests/dispatch.test.mjs pins A2's rule that every call but queue reaches the seat CLI exactly as the pre-A2 script did, and its list of calls included []. Sage's approved change sends exactly that call somewhere else, so the pin can't hold as written. The file is outside the brief. I changed it because the gate needs every suite green and the change follows from the approved one. Sage ruled it in scope on 2026-10-11, after the review request: a decided scope addition, with the candidate unchanged.

The change, 16 lines in and 5 out:

  • [] leaves the list of seat calls. [""] stays: it is one argument, so it still goes to the seat CLI.
  • The fake root gains a fake packages/cli/src/cli.mjs.
  • A new test: no argument reaches packages/cli/src/cli.mjs with empty argv, the same working directory and stdin, and the fake's exit code and output passed through. The same call on the pre-A2 fixture still reaches the seat CLI.
  • The header comment names row 55.

With scripts/mosaic put back to the base, the new test fails and the other three pass. With the candidate, all four pass. The test that A2 only adds lines to the pre-A2 script still passes, because the row 55 case adds four lines and removes none.

How the brief maps to the candidate

Brief Where Test
no argument on a TTY opens the app cli.mjs app(), app.mjs runApp 11, 12; out/smoke.txt
not a TTY: usage, exit 4 cli.mjs app() 11, 12
agent run: exit 3 refuseInsideAgent in transport.mjs 11, 12
Inbox, Tasks, Agents, Trail, Help; keys c i t a r j k : ? q app.mjs #key, screens.mjs 1, 9
decide runs the existing flow, strings unchanged resolveDecision 4, 5, 6, 7, 13
16 colours, NO_COLOR, 80x24, designed at 120x36 screens.mjs 1, 3
no new dependency package.json untouched
every screen fits 120 columns and 80x24 1
NO_COLOR output has no escape codes 3 (see deviation 1)
the string grep test 13

The tests in packages/cli/tests/app.test.mjs:

  1. Every screen and overlay fits 120x36, 80x24 and the narrow layout (100x30, 99x24), row for row, in colour and NO_COLOR. That covers every view and overlay, plus the empty, stale, refused and no-business states.
  2. Broker text is shown, never interpreted. No raw control character or bidi override reaches the terminal (visible() from packages/conversation/src/terminal.mjs).
  3. NO_COLOR: no escape codes except reverse video (7/27) on the selected row. A frame with nothing selected has none.
  4. 1-9 shows the decide flow's own lines and question. Only y resolves, and it goes through the test broker.
  5. The app's decide text equals mosaic decide stdout for the same decision.
  6. n, N, Esc, Enter and q at the question cancel. Other keys wait. Nothing is sent.
  7. Outcome unknown shows the CLI's sentence and offers no retry. Then v opens the trail, or r reads again.
  8. Refused and failed reads keep the last list, marked stale. A refused decide says refused.
  9. Keys: views, moving, the trail of a selection, the command line, help and quit.
  10. No business: the app says so, and :business <id> picks one.
  11. main([]): exit 3 with a marker set, exit 4 with the usage off a terminal, and the app on one (stubbed runApp).
  12. scripts/mosaic with no argument reaches the app entry (exit 4 off a terminal, 3 with a marker). SEAT_USAGE still equals what the seat CLI prints, so the copy can't drift.
  13. Strings to keep exactly appear once, in cli.mjs and transport.mjs. app.mjs and screens.mjs don't restate "authorizes the action", "declines the action", "[y/N]", "may have recorded it" or "runs only from a human shell".
  14. draw is pure: the same state gives the same frame.

Deviations, stated

  1. NO_COLOR keeps reverse video on the selected row. The brief's test line says NO_COLOR output has no escape codes. IMPLEMENTING.md line 188 says reverse video stays for the selected row, and docs/design/README.md line 148 makes reverse video the selection mark. I followed the design. Test 3 checks that 7 and 27 are the only SGR codes, and that a frame with no selection has none. The writer's \x1b[H\x1b[2J before each frame and the alternate screen codes are cursor and screen control, not colour.
  2. The non-TTY message gains one line. It prints mosaic: the app needs a terminal on stdin and stdout; from a script, use a verb: and then the seat usage and the packages/cli usage, exit 4. Before, it printed refused: <seat usage>. IMPLEMENTING.md line 191 says to refuse and point at the verbs, and the old text named only the seat verbs. If a reviewer wants the old bytes, test 12 already holds them as SEAT_USAGE.
  3. The Terminal class is not the base. The brief says the app is built on the conversation Terminal class. Only visible() is imported. Terminal is a line editor for Talk, and Talk is out of scope here. The app needs a whole-screen redraw and single-key input, which Terminal doesn't do. When Talk joins, the Talk screen embeds Terminal.
  4. Reads block redraw. humanTransport uses spawnSync, so the screen doesn't redraw during a read (30 s timeout). That is the existing transport, unchanged. The README says so.

Evidence

All runs in a detached worktree of 23d566d6 with build.patch applied and node_modules linked (out/gate/).

  • packages/cli: 97/97 (83 before, plus the 14 new), out/gate/cli-tests.log.

  • Every scripts/test-*.sh, run in sequence on the final candidate, all exit 0 (out/gate/summary.txt, logs beside it):

    Suite Result
    test-auth 15 passed, 0 failed
    test-conductor 17 passed, 0 failed
    test-config 24 passed, 0 failed
    test-discord 66 passed, 0 failed
    test-extension-package 18 passed, 0 failed
    test-foundation 44 passed, 0 failed
    test-queue 27 passed, 0 failed; canonical-root checks skip in a worktree
    test-release 14 passed, 0 failed
    test-task 26 passed, 0 failed; 4 Docker groups skipped

    test-task ran with DOCKER_HOST pointed at a missing socket, so its adapter, workspace, live task and live recall groups printed skip. The live ones call a real model. Nothing this candidate changes runs in those groups. Sage's gate rerun covers them with Docker.

  • The first gate run, before the dispatch test change, is kept in out/round1-first-gate/. out/dispatch-mutant.txt has the dispatch test against the base script and the candidate script.

  • Smoke, smoke.sh: the app in a pty at 80x24 against a fixture config, never the real bus. Keys ? Esc t q. The alternate screen was entered once and left once, then it printed (app closed with q; 0 decisions resolved this session) and exit 0 (out/smoke.txt).

  • out/apply.txt: build.patch applies to 23d566d6 in a fresh worktree and the manifest matches.

Follow-ups (not in this row)

  • The transport's "did not finish" CliError (transport.mjs line 60) has no .code, so the outcome-unknown branch in resolveDecision does not see it. mosaic decide then prints outcome-unknown: the bus transport did not finish, exit 1, instead of the "may have recorded it" sentence. The app matches the message text and shows its outcome-unknown overlay, so only the verb has the gap. It predates this row. Written up in FOLLOWUP-transport-code.md; Sage queues it with the 2026-10-12 batch.
  • Talk and stop in the app, Decision Seen, and the web console stay out of scope, as the brief says.