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]>
206 lines
11 KiB
Markdown
206 lines
11 KiB
Markdown
# 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.
|