review(55): Darkwing round 1 build packet (candidate manifest 93fa48c1cdfe)

The full-screen terminal app (#1544): BUILD.md, build.patch over fe5c68fa
(9 files), candidate manifest, the gate logs on 23d566d6 and the pty smoke
script. The candidate itself is not committed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 12:51:09 -05:00
co-authored by Claude Opus 5.5
parent 23d566d62d
commit 93bd679069
22 changed files with 2280 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
# 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`; my addition, for Sage to confirm; 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. This file is outside the brief and outside what Sage approved. I
changed it because the gate needs every suite green and the change follows
directly from the approved one. Sage, please confirm or rule it out.
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.
- Talk and stop in the app, Decision Seen, and the web console stay out of
scope, as the brief says.
+1
View File
@@ -0,0 +1 @@
fe5c68fa031923ff79cbd8aeebb4c8925eb5af23
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
fcc2791aa2f80d7e196b0ce18f02475f9fc6730c74cbb56d2db3f9aae333253e docs/TOOLS.md
001ae6159638f783520fa300c3f9ce71c25701568058664a032760dc6d79062e packages/cli/README.md
534fdf2e70f7459789c32f31e467e78400a44d90df53dd319a774d8cf959506e packages/cli/src/app.mjs
c33d3fedb91be7977cf7b468c908719d8f07cd7a2dad1bb7d9c24d62bcdac432 packages/cli/src/cli.mjs
c2f6852226c7071dec132d422773d76bdfc356450f582ac28cb7a5077860b4c0 packages/cli/src/format.mjs
681279edb4982ce103cdbb1a5fe3490721bca27f0877163f5a8c2f5ce6ace419 packages/cli/src/screens.mjs
17e4a3ccb8a8e896c0d9fb6d0781db9636ceff65ba3d48dd7bdff75934150e4d packages/cli/tests/app.test.mjs
5c892aea6a455f05aabb28d312f5fc4bb256584254357c6499319ac956f58109 packages/queue/tests/dispatch.test.mjs
ab246b3e39ea0c503d97e7af93ef8b958caedb3cf3006e387f14fa93b329856c scripts/mosaic
+9
View File
@@ -0,0 +1,9 @@
docs/TOOLS.md
packages/cli/README.md
packages/cli/src/app.mjs
packages/cli/src/cli.mjs
packages/cli/src/format.mjs
packages/cli/src/screens.mjs
packages/cli/tests/app.test.mjs
packages/queue/tests/dispatch.test.mjs
scripts/mosaic
@@ -0,0 +1,13 @@
base: 23d566d62db9189e5d7c252ae278c80c99213423
check: ok
apply: ok
docs/TOOLS.md: OK
packages/cli/README.md: OK
packages/cli/src/app.mjs: OK
packages/cli/src/cli.mjs: OK
packages/cli/src/format.mjs: OK
packages/cli/src/screens.mjs: OK
packages/cli/tests/app.test.mjs: OK
packages/queue/tests/dispatch.test.mjs: OK
scripts/mosaic: OK
9 files changed, 1363 insertions(+), 16 deletions(-)
@@ -0,0 +1,16 @@
# packages/queue/tests/dispatch.test.mjs with scripts/mosaic at fe5c68fa (the base):
✔ every call but `queue` reaches the seat CLI exactly as before A2 (669.173751ms)
✖ failing tests:
ℹ fail 1
ℹ pass 3
✖ no argument reaches the app entry in packages/cli, and only that call (34.852154ms)
✔ `queue` reaches the queue CLI with the rest of the arguments (210.558498ms)
✔ the pre-A2 fixture is the script A2 changed (0.318757ms)
# with the candidate's scripts/mosaic:
✔ every call but `queue` reaches the seat CLI exactly as before A2 (607.518999ms)
✔ no argument reaches the app entry in packages/cli, and only that call (71.984814ms)
✔ `queue` reaches the queue CLI with the rest of the arguments (224.724883ms)
✔ the pre-A2 fixture is the script A2 changed (0.339528ms)
ℹ pass 4
ℹ fail 0
@@ -0,0 +1,107 @@
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (657.730481ms)
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (86.710314ms)
✔ NO_COLOR: no escape codes, except reverse video on the selected row (85.638005ms)
✔ 1-9 shows the decide flow's own lines and question; only y resolves (159.198152ms)
✔ the app shows the same decide text as mosaic decide (130.463236ms)
✔ n, Esc, Enter and q at the question cancel; other keys wait; nothing is sent (658.383117ms)
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (128.743686ms)
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (177.266492ms)
✔ keys: views, moving, trail of a selection, the command line, help and quit (163.853199ms)
✔ no business: the app says so and :business picks one (82.097108ms)
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.988286ms)
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (258.258873ms)
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (1.575316ms)
✔ draw is pure: same state, same frame (1.081875ms)
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (129.136475ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (138.929431ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (117.191582ms)
✔ decide prints a declining choice as declining (131.258653ms)
✔ an unknown outcome is reported once and never resent (124.564502ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (100.431046ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (115.464391ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (105.521856ms)
✔ every human command refuses inside an agent run before it touches the bus (105.068364ms)
✔ usage errors exit 4; no business and no host is a usage error (93.017952ms)
✔ agents and tasks print through the broker (106.492206ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.649953ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (88.090598ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (66.070636ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (67.66776ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (54.311037ms)
✔ a business without tracker.baseUrl gets no trackers entry (42.543295ms)
✔ an unknown business and a broken system config refuse with exit 3 (103.913445ms)
✔ empty views say so (1.499ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.958082ms)
✔ tasks print the tracker fields the snapshot carries (0.30169ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (1040.624201ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (348.328083ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (219.533172ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1190.312123ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (317.451334ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (169.522641ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (462.061614ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (220.316923ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (141.186895ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (207.730923ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (173.736902ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (326.023075ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (38.675069ms)
✔ bus stop refuses to signal a live pid that is not a bus host (205.281889ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (343.372644ms)
✔ bus start refuses with exit 3 without a notifier config (114.419596ms)
✔ bus start runs until bus stop; status reports it while it runs (865.081835ms)
✔ bus-service.sh renders the unit and installs it into a given directory (40.694753ms)
✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1368.731037ms)
✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (787.355153ms)
✔ a runner that stops at once ends its launch with the runner's reason (255.975513ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (803.364181ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3947.590018ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (140.226608ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (151.079496ms)
✔ a launch client that never closes its side doesn't hold the host's close (149.960005ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1294.430684ms)
✔ zoned uses the IANA zone across DST (26.013355ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (147.099869ms)
✔ two blocking decisions get two DMs with different nonces (132.429819ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.4487ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (106.047926ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (126.668979ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (134.274487ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (109.872742ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (197.106078ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (156.492966ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (138.849231ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (89.590199ms)
✔ an inbox read failure is logged and the next poll retries (1.258133ms)
✔ no Discord id reaches the journal or the log (161.188556ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (35.942562ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (55.52388ms)
✔ the journal: a whole file that is one torn line truncates to empty (28.58631ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.411597ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (1.159198ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.236096ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.461047ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.645397ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.530816ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.583496ms)
✔ digest content stays within Discord's 2000 characters (0.355957ms)
✔ runLoop never overlaps ticks and stops after the one in flight (111.677618ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (521.916131ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (61.362517ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2268.525051ms)
✔ busExit and refuseInsideAgent (0.502004ms)
✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (236.288887ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1126.015734ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (181.101284ms)
✔ launches off and on go to the broker and change the business's launch state (124.237954ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (100.26265ms)
ℹ tests 97
ℹ suites 0
ℹ pass 97
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 9077.632776
@@ -0,0 +1,11 @@
cli rc=0
test-auth rc=0
test-conductor rc=0
test-config rc=0
test-discord rc=0
test-extension-package rc=0
test-foundation rc=0
test-queue rc=0
test-release rc=0
test-task rc=0
DONE
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,23 @@
On branch refactor
Your branch is up to date with 'origin/refactor'.
nothing to commit, working tree clean
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,26 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK failed validation modified nothing
selftest: 24 passed, 0 failed
@@ -0,0 +1,70 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 178)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 66 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 414866 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,35 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 150
ℹ pass 150
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r55/gate) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
@@ -0,0 +1,16 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
OK status safe on empty state (exit 0)
OK status created no pointer
OK fault-injected activation refuses (exit 1)
OK refused activation wrote no pointer
OK refusal logged exactly once with valid fields
OK healthy activation succeeds (exit 0)
OK pointer written with valid fields
OK repeat activation succeeds (log grows) (exit 0)
OK log is append-only across activations
OK rollback without previous refuses (exit 1)
selftest: 14 passed, 0 failed
@@ -0,0 +1,32 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
skip adapter seam cases (docker daemon unavailable)
skip workspace/capability cases (docker daemon unavailable)
skip live task cases (docker unavailable)
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
skip live recall pair: user recall run, recalled user name (docker daemon unavailable)
OK no agent identity on headless run
selftest: 26 passed, 0 failed
@@ -0,0 +1,11 @@
cli rc=0
test-auth rc=0
test-conductor rc=0
test-config rc=0
test-discord rc=0
test-extension-package rc=0
test-foundation rc=0
test-queue rc=1
test-release rc=0
test-task rc=0
DONE
@@ -0,0 +1,41 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 149
ℹ pass 148
ℹ fail 1
✖ every call but `queue` reaches the seat CLI exactly as before A2 (423.059344ms)
✖ failing tests:
✖ every call but `queue` reaches the seat CLI exactly as before A2 (423.059344ms)
AssertionError [ERR_ASSERTION]: []
+ "Error: Cannot find module '/home/jwoltje/darkwing-scratch/r55/tmp/mosaic-queue-dispatch-CI2rEd/packages/cli/src/cli.mjs'\n" +
actual: { status: 1, out: '', err: "node:internal/modules/cjs/loader:1573\n throw err;\n ^\n\nError: Cannot find module '/home/jwoltje/darkwing-scratch/r55/tmp/mosaic-queue-dispatch-CI2rEd/packages/cli/src/cli.mjs'\n at Module._resolveFilename (node:internal/modules/cjs/loader:1569:15)\n at wrapResolveFilename (node:internal/modules/cjs/loader:1123:27)\n at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1147:10)\n at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1174:12)\n at Module._load (node:internal/modules/cjs/loader:1346:5)\n at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)\n at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)\n at node:internal/main/run_main_module:33:47 {\n code: 'MODULE_NOT_FOUND',\n requireStack: []\n}\n\nNode.js v26.8.1\n", capture: null },
FAIL node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r55/gate) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 26 passed, 1 failed
@@ -0,0 +1,4 @@
(app closed with q; 0 decisions resolved this session)
EXIT=0
alternate screen entered: 1
alternate screen left: 1
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Row 55 smoke run: opens the app in a pty at 80x24 against a fixture
# config (never the real bus), presses ? Esc t q, and prints what the
# terminal got after the last frame. Usage: smoke.sh <repo> <scratch-dir>
set -euo pipefail
REPO="$(cd "$1" && pwd)"; S="$(mkdir -p "$2" && cd "$2" && pwd)"
cat > "$S/mkfix.mjs" <<JS
import { fixture } from "$REPO/packages/cli/tests/helpers.mjs";
const f = fixture(process.argv[2]);
console.log(f.config + " " + f.roles);
JS
mkdir -p "$S/fx"
read -r CONFIG ROLES < <(node "$S/mkfix.mjs" "$S/fx")
{ sleep 1.5; printf '?'; sleep 0.5; printf '\033'; sleep 0.5; printf 't'; sleep 0.5; printf 'q'; sleep 1; } \
| env -u MOSAIC_BUS_CAP -u MOSAIC_RUN_ID -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT -u MOSAIC_AGENT_RUN -u CODEX_THREAD_ID -u PI_AGENT_DIR \
MOSAIC_CONFIG="$CONFIG" MOSAIC_ROLES_DIR="$ROLES" \
script -qfec "stty cols 80 rows 24; '$REPO/scripts/mosaic'; echo EXIT=\$?" "$S/typescript.log" > "$S/drive.out"
# The tail after the alternate screen closes: the quit line and the exit code.
sed -n 's/.*\x1b\[?1049l//p; /^EXIT=/p' "$S/drive.out" | tr -d '\r'
grep -c $'\x1b\[?1049h' "$S/drive.out" | sed 's/^/alternate screen entered: /'
grep -c $'\x1b\[?1049l' "$S/drive.out" | sed 's/^/alternate screen left: /'