diff --git a/agents/gander/work/queue-253/BUILD.md b/agents/gander/work/queue-253/BUILD.md
index d0d405a43..39843a932 100644
--- a/agents/gander/work/queue-253/BUILD.md
+++ b/agents/gander/work/queue-253/BUILD.md
@@ -183,3 +183,13 @@ F5 now names row 276 / #1781 in docs/plans/DEFERRED.md (sage-2331). That path wi
c4/REPAIR.diff, *.repair.txt and REPAIR-SHAS.json freeze the complete repair for Duckworth's C1/C3/C5 static look. The original c4/*.sh.txt and FROZEN-PRODUCT.json stay unchanged. The runnable product and test files remain uncommitted pending the applicable gate; this packet commit is records only. No node_modules path is included.
2026-10-22 02:13:30 UTC: sage-2335 publishes b64aa316a and explicitly requires a committed runnable candidate before the gate. Accordingly the seven repaired source/test/doc paths, the existing BUILD-LOG append and this record are committed now. All seven bytes still match c4/REPAIR-SHAS.json; this commit changes no reviewed packet source. CANDIDATE-SHAS.json is refreshed for the new gate. MANIFEST.sha256 covers the eight product/test/doc/log paths, including DEFERRED.md. No repaired-source verification ran; the static verdict and CLEAR remain pending, and all old results are historical only.
+
+## duckworth-0112 delta, no runs
+
+B1: both supervisor copies use fork-free IFS= read -r -d '' with content-based EOF/error handling. The 250 ms confirmation budget starts after the first full inventory pass. A deterministic stand-in clock test pins this placement without wall timing or real signals; M40 moves the budget back before pass one. All 40 proposed targets statically occur once. No test or syntax execution ran.
+
+M1: BUILD-LOG.md is removed from MANIFEST.sha256. Its row253 phase lines land as a separate doc-only append after code, preserving newer upstream lines, under sage-2342. The manifest now has seven entries. The initial eight-entry manifest is superseded.
+
+N4: SID confirmation now waits within the existing inner watchdog budget rather than only 100 sleeps. It still refuses a vanished/unreadable child or unconfirmed session, and the independent outer bound remains. N1/N2 are explicitly deferred in the existing F5 entry to row276 / #1781, as Sage rules. No case1 analogue checks were added in this delta.
+
+The required gate base is the refactor tip Sage names at CLEAR, 378a76b85 or later. This delta is prepared on the current seat branch for static review only; no old-base gate is authorized. The final integrated gate candidate will get fresh hashes and integration records before execution. c4/delta-0112/ freezes the seven source/test/doc paths, the delta from aa81ca390, and static uniqueness. Prior frozen packets remain unchanged.
diff --git a/agents/gander/work/queue-253/CANDIDATE-SHAS.json b/agents/gander/work/queue-253/CANDIDATE-SHAS.json
index 08f68f996..79936dcc9 100644
--- a/agents/gander/work/queue-253/CANDIDATE-SHAS.json
+++ b/agents/gander/work/queue-253/CANDIDATE-SHAS.json
@@ -1,9 +1,9 @@
{
- "scripts/case2-unit.sh": "3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe",
- "scripts/case1-accept.sh": "f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7",
+ "scripts/case2-unit.sh": "d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b",
+ "scripts/case1-accept.sh": "c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd",
"packages/cli/tests/case-sudo-fixture.mjs": "b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008",
- "packages/cli/tests/case2-unit.test.mjs": "72a07e9ad9d5c8bb01851eef18f792f2fe9b95d56803e313fad6aa1f1d6c3e8c",
+ "packages/cli/tests/case2-unit.test.mjs": "1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44",
"packages/cli/tests/case1-accept.test.mjs": "6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901",
"packages/webui/README.md": "2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9",
- "docs/plans/DEFERRED.md": "67bbf7bf57aa70dcade36ca4642f01cccb71ce01c130a88b84e1edb7ea674377"
+ "docs/plans/DEFERRED.md": "3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f"
}
diff --git a/agents/gander/work/queue-253/MANIFEST.md b/agents/gander/work/queue-253/MANIFEST.md
index 2c6d0c5f0..27b76202a 100644
--- a/agents/gander/work/queue-253/MANIFEST.md
+++ b/agents/gander/work/queue-253/MANIFEST.md
@@ -1,7 +1,7 @@
# Row253 committed C4 candidate manifest
-MANIFEST.sha256 names the eight source/test/doc/log landing paths and pins their bytes. Its digest is the review identifier. The seven repair paths match c4/REPAIR-SHAS.json from b64aa316a; BUILD-LOG.md is append-only phase evidence. Land its row253 additions as appends, preserving later upstream entries.
+MANIFEST.sha256 names seven source/test/doc landing paths and pins their bytes. Its digest is the review identifier. BUILD-LOG.md is excluded: land the row253 phase lines as a doc-only append after the code, preserving upstream appends (duckworth-0112 M1, sage-2342).
-Base: c3d88840c. Static repair packet: b64aa316a. Full records live under agents/gander/work/queue-253/. Those records are review evidence; the manifest separately identifies the eight landing paths. The current upstream overlap check against ddfb29af7 is c4/UPSTREAM-PATHS.json. DEFERRED.md preserves upstream entries and adds the F5 follow-up row276 / #1781.
+Base before the required gate integration: c3d88840c. Initial static repair packet: b64aa316a; initial runnable candidate: aa81ca390. The delta packet is c4/delta-0112/. Full records live under agents/gander/work/queue-253/ as review evidence. DEFERRED.md preserves upstream entries and names row276 / #1781 for F5 and N1/N2.
-Sage-2335 authorizes this runnable commit before the whole gate. No repaired-source syntax check, test, probe or host experiment has run. No results from 8a8df4171 carry. Duckworth's static verdict and Sage's CLEAR are required before gate execution. The final build review will receive fresh verification records.
+Sage-2335 authorizes the runnable commit before the whole gate. No repaired-source syntax check, test, probe or host experiment has run. No results from 8a8df4171 carry. After Duckworth's delta static verdict, the gate must use the candidate integrated onto the refactor tip named by Sage at CLEAR (378a76b85 or later under sage-2342), never this old base. Fresh integration records and hashes will identify that final gate candidate.
diff --git a/agents/gander/work/queue-253/MANIFEST.sha256 b/agents/gander/work/queue-253/MANIFEST.sha256
index bcd657535..3642dfade 100644
--- a/agents/gander/work/queue-253/MANIFEST.sha256
+++ b/agents/gander/work/queue-253/MANIFEST.sha256
@@ -1,8 +1,7 @@
-43d3f1664f195839ff303dbb0bd0f040c177ac1923d0760a23c09cc7130ec1b5 BUILD-LOG.md
-67bbf7bf57aa70dcade36ca4642f01cccb71ce01c130a88b84e1edb7ea674377 docs/plans/DEFERRED.md
+3ea465cf7ed664ceb1ea4076b829f706f8480f80c4a8aaa6a7487e4eceaf318f docs/plans/DEFERRED.md
b259b8c7926e49829c8a956674021cb0cc85ddd65aebdc786d05cc08d7790008 packages/cli/tests/case-sudo-fixture.mjs
6cfd2963303315aa6911c4d4f6d75d3ab87d9fcba8e128b056216e7f85fd8901 packages/cli/tests/case1-accept.test.mjs
-72a07e9ad9d5c8bb01851eef18f792f2fe9b95d56803e313fad6aa1f1d6c3e8c packages/cli/tests/case2-unit.test.mjs
+1e0696eb196e83a0766b433ef71f27df0a3f8fcb6399787ab8c1c0f05c54dc44 packages/cli/tests/case2-unit.test.mjs
2eeb1f79324f64463a9b3427e67492d672b8d1c14db6c0349c7de17d89e52fc9 packages/webui/README.md
-f93eac4cc9dd9fc95a67be19d9fa7c7f6e7d7bfc84e662360b4aed05763850c7 scripts/case1-accept.sh
-3ec41265b47770f70d60640a786b4f547ffb0157512fd8477ea7712e91a724fe scripts/case2-unit.sh
+c02bef6b3495e5a4548d7b4e89675ed52be26a0c9ccce4d9d5cfc5fe2ce437bd scripts/case1-accept.sh
+d7670bdb049ae41ba3774fc83de633636859b6723be858cfb31ea106b0e8812b scripts/case2-unit.sh
diff --git a/agents/gander/work/queue-253/c4/delta-0112/DEFERRED.md.txt b/agents/gander/work/queue-253/c4/delta-0112/DEFERRED.md.txt
new file mode 100644
index 000000000..ea0e19227
--- /dev/null
+++ b/agents/gander/work/queue-253/c4/delta-0112/DEFERRED.md.txt
@@ -0,0 +1,463 @@
+# Deferred list
+
+One line per item, written from evidence, never from inertia. Add a line
+when a gap is found and not fixed in the current piece; strike it (move to
+Done at the bottom with the commit) when a piece closes it. Nothing here
+starts on its own; a line becomes a piece only when Jason calls it. Check it
+at every gate. Started 2026-09-12 during the control board MVP.
+
+## Open
+
+- **Inherited Discord engine test intermittency during #1512 review.** Frozen
+ serialized run recorded a tool-turn engine assertion failure and then timed
+ out at 120s. A later full TAP run with a 15s test deadline passed 351/351.
+ Rocko isolated the busy/settled-event test race and missing failure cleanup;
+ Darkwing independently reproduced the event-split assertion. Rocko also reported
+ a timeout-log test race and a separate engine followUp-after-timeout defect.
+ No live incident established. Row21 owner orch-01 notified; no Discord source
+ takeover. Not assumed identical to earlier concurrent hangs.
+ Integration held pending owner disposition; logs and scope in
+ `agents/darkwing/work/relaunch-activity/r1-review.md`. (2026-09-15)
+ Outcome 2026-09-26: 1685deb4 fixed the busy/settled race and the engine no
+ longer sends a follow-up (held prompts). A re-run of the frozen candidate
+ against 21e3e908 passed the serial acceptance command 397/397 three times.
+ Two engine failures remain when tests run concurrently, and they reproduce on
+ clean HEAD without the #1512 files, so they are #1509's (entry below). Sage
+ ruled that #1512 integrates on the serial evidence; it landed in af4203ca.
+ Record: `agents/darkwing/work/relaunch-activity/rerun-2026-09-26.md`.
+
+
+
+- **Registration has no "set by" field.** `mosaic seat task` records the
+ task and `updatedAt` but not who set it, so a human session and the
+ orchestrator look the same on the board. One-line record change; rides
+ with the orchestrator assignment piece. (2026-09-12, #1504)
+- **Usage-limit notice was missed.** Jason reports Rocko exhausted usage without
+ Jason seeing the notice, and is now available again. Investigate where the
+ notice appeared and what operator notification is missing as part of Claude
+ console integration. User-reported, not independently reproduced; no root
+ cause established. Keep outside #1512's relaunch-display scope. (2026-09-15)
+- **Claude Code seats register but get no board row.** Rocko has no Pi
+ session log, and rows come only from Pi logs. Needs a second row source
+ or a Claude-side log reader. (2026-09-12, darkwing's #1504 report)
+ 2026-09-15 #1511 live-test preflight reconfirmed no Rocko row: expected
+ `.pi/state/rocko/sessions` discovery directory absent. Its existing repo
+ registration names dead PID 185602, while the verified native lock owner is
+ 3707667. No registration repair, discovery expansion or relaunch performed;
+ test write refused before mutation. Evidence:
+ `agents/darkwing/work/task-attribution/backend-live-verification.json`.
+- **Fleet seats (`~/.mosaic`) stay on the old launchers.** No registration,
+ no board reply, no `seat task`. Moving them is the stress test and comes
+ after the WebUI and fleet launch work. (Jason, 2026-09-12)
+- **Comms skill has no rule for senders without a pane.** The board trailer
+ covers it today; if another pane-less sender appears, put the rule in
+ `skills/ms-communications` instead of a second trailer. (2026-09-12, #1505)
+- **Board: multiple sessions per seat, stopping seats, auth beyond
+ loopback.** Out of scope for the MVP by the plan; listed so they are not
+ forgotten. (plan page, Boundaries)
+- **`MOSAIC-STACK-D-001` is Answered, not Completed.** Mark `--completed` in
+ jarvis-brain once Jason says the board answers "who is waiting on me".
+ (2026-09-12)
+- **Uncommitted working tree.** `AGENTS.md` autonomy section,
+ `agents/README.md` Sage row, Dewey's `work/wui` docs and evidence,
+ `docs/plans/reviews/2026-09-08..10_*`, `skills/ms-{arena,blast-radius,
+ board,executive-update,git-operations,technical-writing}`. Jason's edits
+ from before 2026-09-12; they ride along in the next commit that touches
+ them unless staged on purpose. (2026-09-12)
+- **Fleet debt (orch-01, `~/.mosaic`):** `sweep-commit.sh` deny-list regex
+ skips `launch.env`; needs the fix plus a regression test. (2026-09-12)
+- **Parked, not cancelled:** Dewey D05 dashboard mockup work beyond design 3,
+ registry increment 3 (headless identity-env leak), new seat directories.
+ (plan page, 2026-09-12 parking note)
+
+- **Gitea helper response-file hardening.** Predictable `/tmp/gitea-api-response.$$`
+ has no explicit 0600 mode; use `mktemp` with cleanup in a separately reviewed
+ pass. Credential validation and later reread also have a race window. Other
+ non-blocking ledger review notes remain in
+ `reviews/2026-09-12_ledger-verdict.md`. No change beyond the reviewed GET
+ cleanup fix in this piece. Filbert, LEDGER-1506-R1, 2026-09-12, #1506.
+
+- **One shared checkout for every seat and the coordinator.** Any `git add`
+ of a shared file (CURRENT.md, SESSIONS.md, the plan page) commits another
+ seat's in-progress edit under the wrong commit; it happened at f80404f5
+ (dewey's CURRENT.md status note went in with the piece 5 brief, harmless
+ but unowned). Fix candidates: per-seat worktrees, or shared files edited
+ only by the seat named in CURRENT.md. (2026-09-12)
+
+- **Console mockup fields absent from the board API.** Controller, generation,
+ config fingerprint, turn count, goal/report and activity stream are omitted.
+ Workspace paths are shown as board data, not invented registered workspace
+ nodes. Launch, observe, stop, full transcript navigation, command palette and
+ other screens are absent. Existing palette/appearance switches remain on the
+ single page. Revisit only with a new brief. (2026-09-13 UTC, #1507)
+
+- **Board send can turn into a Pi slash command.** `tools/tmux`
+ `send-message.sh` pastes onto whatever is already in Pi's composer without
+ clearing it. On Jason's 2026-09-26 20:10:57Z send, a leftover "/" landed in
+ front of the agent-send header. Pi read it as plain text that time, but the
+ same path could run a board reply as a slash command. Belongs to CHAT-03I/B3.
+ No tools/tmux change until that piece. Found by Dewey in the CHAT-02 brief
+ evidence. (2026-09-26, #1507)
+
+- **Discord wedge restarts have no cumulative stop.** Since 6b R2, a wedged
+ pi ends the connector with exit 1, and the unit restarts it. With
+ turnTimeoutSeconds 600, one cycle takes at least 645 s, longer than the
+ 600 s StartLimit window. So a pi that wedges on every prompt restarts once
+ per prompt and never hits the limit. It only happens when someone sends a
+ message, and that person gets an error reply. Sage accepted it for now. The
+ fix is a separate policy (a wedge count kept across restarts). (2026-09-26,
+ #1509, Rocko 6b R2 review)
+- **Connector does not journal the source it started from.** The unit runs
+ from the live checkout, so an automatic restart loads whatever Discord
+ source is in the tree, even uncommitted source. Rocko recommends a startup
+ journal line with HEAD, the dirty state of `packages/discord` and a digest
+ of the runtime files. Until then the rule is that unreviewed Discord source
+ stays uncommitted in the tree only as long as a review needs it.
+ (2026-09-26, #1509)
+- **Index-export suite runs leave Docker networks behind.** Each export
+ directory becomes a compose project, and its `
_default` network
+ stays after the run. On 2026-09-26 the host ran out of address pools. The
+ task and release suites then failed 31 and 3 tests with "all predefined
+ address pools have been fully subnetted". Sage removed its own five stale
+ networks. The dw-* ones are still there. Fix: the export recipe, or the
+ suites, removes the project network on exit. (2026-09-26)
+ 2026-09-27: it happened again on the Piece D candidate (task 31 and
+ release 3 failures). Sage removed 12 leftover export networks from
+ other seats, each checked for zero attached containers first, and both
+ suites then passed. Still open.
+
+- **Ledger T3 read: snapshot isolation has no test.** `readT3` runs every
+ query in one read transaction, but no test proves a write landing during
+ the read stays invisible. Filbert's note 4 on the Gate F build (e47ec6da).
+ Notes 1 to 3 go to Darkwing's follow-up. (2026-09-26, #1506)
+- **A T3 sender role with spaces counts as human.** The SetSpark lead's
+ reply to Sage (21:41:24Z) used the role "main content-engine agent". The
+ 6a rule requires a one-word role, so the ledger counts that message as
+ human in Sage's row. Gate F reads only Filbert's row, so it is not
+ affected. Filbert found it through the live diagnostic (14 went to 15).
+ Sage asked the SetSpark lead to use a one-word role. The rule stays as
+ it is. (2026-09-26, #1506)
+
+- **Queue tests: the cold-start failure came back (reopened).** Row 33
+ closed this as not reproduced in 20 idle cold runs. On 2026-10-04 Sage's
+ index export of the row 33 candidate ran the nine suites, then
+ `node --test packages/queue/tests/`, and got 147 passed, 1 failed. Sage's
+ recipe threw the output away, so again nobody knows which test failed.
+ Ten more runs all passed 148/148: three reruns in the same export, four
+ under parallel suite load, and three fresh exports run straight after
+ `test-queue.sh`. It needs a first run on a fresh tree after a full
+ suite pass, and even then it's rare. Darkwing's n2 on row 33 (the cold
+ runs were idle) was right. Next time: Sage's export recipe keeps the
+ node test output in a file, so the first failure has a name. Then fix it
+ as its own row. (2026-10-04, #1508)
+- **Queue: `add --issue N` also sets closes to N.** Follow-up rows under
+ an umbrella issue, like rows 31 and 33 under #1508, would close the issue
+ when they finish. Sage narrowed both by hand (revs 36 and 43). Either
+ `add` takes `--closes` or the lead checks closes on every add. Not
+ urgent while Sage adds the rows. (2026-10-04, #1508)
+
+- **Ledger metric call can orphan curl.** The metric call in
+ `packages/ledger/src/ledger.mjs` still relies on execFileSync's
+ timeout, which kills the helper but can leave curl running. E's queue
+ calls already use `timeout -s KILL`; the metric call should match.
+ (2026-09-27, #1508)
+- **Ledger: a missing `timeout` command reads as a Gitea failure.** With
+ no `timeout` on PATH the queue issue calls report "credential or Gitea
+ request failure". Optional test and clearer message. Filbert, E r2.
+ (2026-09-27, #1508)
+- **`review record` doesn't check which issue the verdict comment is on.**
+ Row 5 round 1's request (26671) and Filbert's verdict (26683) are on
+ #1507. Darkwing's verdict 26681 went to #1508, and `review record`
+ accepted it, because only `review resolve` reads the comment back
+ (`checkComment` in `packages/queue/src/review.mjs`). The receipt
+ points at a real comment on the wrong issue. Sage posted a pointer on
+ #1507 (comment 26685). Fix: have `record` check the comment's issue and author the way
+ `resolve` does, or refuse a comment id it can't verify. Found by
+ Filbert. (2026-10-04, #1508)
+- **Host seats launch Pi with `--approve`.** A future `.pi/SYSTEM.md` or
+ `.pi/settings.json` in the checkout would load in every host seat. This
+ is latent, since neither file exists today. Fix in
+ `scripts/agent-host-dev.sh`: pass `--no-approve` and an explicit system
+ prompt. Filbert, meta-harness survey section 8.4. (2026-10-04)
+- **Workers can read provider keys.** The worker container passes
+ `ZAI_API_KEY` and `ANTHROPIC_API_KEY` in the environment, which Pi's
+ `bash` inherits, and Pi's `read` can open the mounted `auth.json`.
+ Slice 1 role tokens stay in the broker (lead decision 47). The workers'
+ own exposure needs a separate piece. Filbert, survey section 8.6.
+ (2026-10-04)
+- **CHAT-03 I1 round 2 follow-ups, for the I3 brief.** Neither reviewer
+ held any of these as blocking (Darkwing comment 26690, Filbert comment
+ 26694 on #1507). Four of the five are closed by slice 1 S5 (see Done);
+ this one stays open.
+ - No test proves that the processes are listed only after the cgroup
+ is frozen. The code waits for `frozen 1` in `cgroup.events`, and
+ both Filbert and Dewey confirmed it by reading it. Mutants r2-B5b and
+ C4 survive. Proposed: a test-only hold after `members`, or a FUSE or
+ privileged fixture. Filbert F1.
+ (2026-10-04, #1507)
+- **CHAT-03 `engine.envKeys` accepts founder credential names.**
+ `CREDENTIAL_NAME` in `packages/conversation/src/pi-pin.mjs` matches any
+ `*_API_KEY` or `*_TOKEN`, including `GITEA_TOKEN` and `GH_TOKEN`, which
+ the S6 runner refuses as founder credentials (REQ-CRED-2). Nothing
+ builds a Controller outside the tests yet. Whoever wires the entry point
+ narrows the pattern to provider names or checks the founder list.
+ Filbert N2 on #1522. (2026-10-10)
+- **`test-task.sh` makes a live Pi call without the Docker guard.** The
+ "user recall" block near line 478 runs `run-task.sh` with no
+ availability check, so a run with Docker unavailable fails "user recall
+ run succeeds" and "recalled user name" (26 pass, 2 fail). The other
+ live groups skip. Rocko hit it during the S2 suites. Wrap the block in
+ the same guard as the other live groups, so a seat without provider
+ access gets clean skips. (2026-10-04, #1519)
+- **`node --test /` fails on Node 24.** Node 24 reads a directory
+ argument as a module, so every package's test command in that form
+ fails there, while Node 26 accepts it. The glob form
+ (`node --test 'packages/x/tests/*.test.mjs'`) works on both. Package
+ READMEs, `docs/TOOLS.md` and Sage's suite recipe should use the glob
+ form wherever Node 24 is a target. Darkwing, S1 packet. (2026-10-04,
+ #1518)
+- **`stack-upgrade.sh` doesn't check `$REPO`.** Nothing confirms the
+ source checkout is root-owned or that its tree is clean before the
+ upgrade builds from it. Filbert N2 on row 233 (29922, #1735).
+ (2026-10-21)
+- **`stack-upgrade.sh` has no signal handling between A2 and A5.** A HUP,
+ INT or TERM in that stretch leaves the run failed closed, but recovery
+ is by hand. A trap that writes the stop point and the recovery step to
+ the log would make it a sheet step. Filbert N3 on row 233 (29922,
+ #1735). (2026-10-21)
+- **`checkCreative` doesn't refuse `credentials`.** Row 213's
+ `checkCreative` (packages/business/src/role.mjs:143 at 2d2ad2760)
+ limits a creative role's tools, authority and messaging, but a role
+ file that gave companion or research a credential would still load.
+ The business file could then map a credential to that instance
+ (business.mjs:157). Nobody has traced whether a mapped credential
+ reaches the run's environment. The decision paths stay closed (M6),
+ but a creative role with `web` holding a live token is a wider channel
+ than design 4 means. The fix: refuse any `credentials` in
+ `checkCreative`, with a role.test.mjs case. It goes after row 213
+ lands, as a follow-up row or in the next row that edits role.mjs.
+ Webby N1 on row 213 r3 (REVIEW-r3.md, 3ab3c010b), mos-1160.
+ (2026-10-21)
+- **The boot host check orders only one business's bus.**
+ `mosaic-hostcheck.service` (row 240) has
+ `Before=mosaic-bus@mosaic-stack.service`. `case2-unit.sh` enables the
+ bus as `mosaic-bus@.service`, so on a host whose business id
+ isn't `mosaic-stack` nothing orders the two. The bus can start before
+ the boot stop runs, and after a boot-time drift role sessions can use
+ bwrap until it does. The fix: `After=mosaic-hostcheck.service` in
+ `packages/cli/systemd/mosaic-bus.service.in`, which orders every
+ instance and does nothing where the unit isn't installed. It goes in
+ before the first install whose business id isn't `mosaic-stack`.
+ Filbert N1 on row 240 (29945, #1746). (2026-10-21)
+- **The run sheet writes the apt hook before the first `--check`.**
+ Section 4 installs `80mosaic-hostcheck`, then runs
+ `hostcheck.sh --check`. If that check fails, the next package run
+ closes bwrap before anyone has read the failure. It fails closed, but
+ it surprises an installer. The fix: run `--check` first, or say what a
+ failure at that step means. Doc-only, for the next row that edits
+ `docs/guides/host-run-sheet.md`. Filbert N2 on row 240 (29945, #1746).
+ Filbert's N3, that a closed bwrap appends to `DRIFT` on every later
+ apt run and boot, is by design; ops-01's STATE read should expect the
+ repeats. (2026-10-21)
+- **The sso-config first-step hint has no business.** When the target
+ copy has no `console.sso`, `stack-release.sh` (`:938`) refuses with
+ "activate $TO without --sso first, then ... activate $TO --sso". The
+ usage line is `activate ID BUSINESS` (`:47`), so pasting either command
+ as printed gives a usage error, not the first step. It fails safe,
+ since the usage error changes nothing. The fix: add `$BUSINESS`
+ (set at `:2138`) to both commands, and the business (`acme`)
+ to the three expected strings in `stack-release.test.mjs`
+ (`:1538`, `:1689`, `:1691`). For a later row that edits
+ `stack-release.sh`. Webby N1 on row 238 r1 (REVIEW-r1.md, 433a95292,
+ #1744). (2026-10-21)
+- **Lane image prep inspects a base that BuildKit never tagged.**
+ (N7 of the row 197 proof.) After the Compose build,
+ `scripts/testing/lane-image.mjs:41` (`prepareImage`) runs
+ `docker image inspect node:24-bookworm-slim` to put the base digest
+ in the receipt. BuildKit resolves the base from the registry
+ without tagging it in the local store, so on a host without the tag
+ the inspect throws "No such image" after a good build. That
+ happened in Sage's proof try 1 (2026-10-21 09:03Z, run cf48efccf083).
+ The throw fails closed: the run's terminal record is unclean, both
+ lanes quarantine, and `gate-lane.sh recover` clears them. Pulling the
+ base first fixes it, and the receipt digest then matches what the
+ build used (91acba91 both ways). The fix: before the build, inspect
+ the base and refuse with a named reason if it's missing, or pull it.
+ After the build, refuse if the base digest changed. Add a test with a
+ fake Docker that has no base tag. Kept out of row 197 r6 by Sage
+ (sage-1883). Mos asked for this record in mos-1162. (2026-10-21)
+- **Case 1 acceptance script: root parses with the node it's checking, and the header says less than root does.**
+ (Duckworth N1-N3 on the row 195 manifest, REVIEW-m1 6afcee2a5.)
+ N1: F9 runs `$ROOT$NODE` as root (`scripts/case1-accept.sh:298`)
+ before F10 checks that `--node` is the `/opt/mosaic-stack/node-`
+ path (:303-304). On a host installed as designed, `case2-unit.sh:128-133`
+ refused a non-root-owned `--node` and the args file is root's, so no
+ escalation path exists today. Run F9's parse through `AS_USER`, or
+ apply F10's path check first. N2: the header (:10) and the TOOLS.md
+ entry say root only reads, but root also runs `curl` (F2), `sudo -l`
+ (P1, P3), the jarvis probe (P9), `hostcheck.sh --check` and
+ `systemctl`. Doc-only. N3: F6's prefix match (:261) accepts a
+ `--claude` with `..` after `claude-/`. That claude runs only as
+ `mosaic`, and F10 uses equality. Darkwing has a follow-up on
+ `darkwing/row195-n1` 1e3c4767c, not yet run; it gets its own row now
+ that 195 has landed (b65014647), Duckworth reviews. (2026-10-21)
+- **Morgana's row 213 mutation set: the B3 pattern no longer matches `bundle.mjs`.**
+ (Webby N3 on row 221 round 1, REVIEW-r1 a0a00630e.) Row 221's
+ candidate changes `packages/harness/src/bundle.mjs`, and the B3
+ anchor at Morgana's 213 `mutants.py:48` stops matching on that
+ base. This matters only if someone reruns that set. Fix the anchor
+ in that tooling before any rerun, and check that a stale anchor
+ refuses instead of skipping the mutant (sage-1941). (2026-10-21)
+- **Grant executor test X5 starts a `sleep 30` that can outlive its test.**
+ (Webby N1 on row 248 r1, webby-0080; deferred by Duckworth in the row
+ 249 review, REVIEW-r1 28174fe08, #1751 comment 30070.) The test at
+ `packages/bus/tests/grant-executor.test.mjs:989` (:934 before row
+ 249) runs `sleep 30` on the sshd side, where an abort doesn't reach it. Fix it with a stub
+ that exits once its test dir is gone, as row 248 did for the hold
+ stubs, or with a shorter sleep. This binds the next row that touches
+ that test file. Until then, a lane cleanup that runs after this file
+ waits 30 s before it calls a lone `sleep 30` a leak. The row 197
+ lanes scan only at lane end, about 15 minutes after pkg-bus, so they
+ can't see it (mos-1169). (2026-10-21)
+
+- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
+ (mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
+ harness with a prompt that draws a known tell. Mos changed that gate. The
+ Claude Code session ran before landing on a scratch root from the
+ candidate tree (run r4ab24630d323, landed 2c38325ec). Pi ran launch only there (run r1dd94171bede):
+ this host has no credential pi reads until P19 #383 settles. Two live
+ checks remain. Claude Code: in the first re-pin whose tree includes row 66,
+ backup first (Q173). Pi: one full session with a tell after #383. Each
+ attaches the operator note on the RESULT, the next-turn correction and the
+ `unslop.jsonl` counts. (2026-10-21)
+
+- **Console tester listener: `testerNoSignIn` is a dead parameter.**
+ (Duckworth N2 on the row 196 manifest, REVIEW-m1; carried as C1 in
+ REVIEW-m2 cdbf7b64e, #1694 comment 30156.) Row 196 landed 7247d80c7
+ with the parameter and its comment still at `serve.mjs:472-475` and
+ passed from `webui/src/cli.mjs:109` and `sso.test.mjs:392`. Nothing
+ in `serve.mjs` reads it after the destructure. Remove it, or wire it
+ with a test. This binds the next row that touches
+ `packages/webui/src/serve.mjs`. (2026-10-21)
+- **Console library routes: the tester listener and `library.sync`.**
+ (Duckworth N5 on row 196 round 2, REVIEW-m2 cdbf7b64e.) At 7247d80c7
+ `startServer` (`serve.mjs:475`) takes none of `libraryFile`,
+ `libraryStatus` or `librarySync`, so the callbacks `cli.mjs` passes
+ do nothing yet. The row that adds the Console library routes decides
+ whether the tester listener gets `library.sync`. Duckworth's view is
+ that it shouldn't, because sync is a human write. (2026-10-21)
+- **Console library routes: four library codes still map to 502.**
+ (Dewey N2 on the row 254 manifest, #1759 comment 30173.) Row 254
+ gave six library codes a STATUS entry in `packages/webui/src/bus.mjs`.
+ `library-mapping-invalid`, `library-remote-invalid`,
+ `library-state-invalid` and `library-git` still fall through
+ `busStatus` to 502. The row that adds the Console library routes
+ decides their statuses and gives its `humanCall` for `library.sync`
+ a timeout over 130 seconds. (2026-10-21)
+- **T27 sweep test leaves two `sleep 60` children running.**
+ (Darkwing row 251 slot, darkwing-0113 and darkwing-0117; Dewey on
+ r2, comment 30168.) The sweep test spawns two detached shells,
+ `planted` at `host-run.test.mjs:528` and `decoy` at `:536`, each
+ running `sleep 60; :` with the run dir as `$0`. `sweepRunDir` kills
+ the planted shell by pid (`host-run.mjs:238`), and `t.after` kills
+ only the decoy shell. Neither kill reaches the `sleep` child, so each
+ one lives for up to 60 s after the test and trips any busy check that
+ follows. The fix is to kill the process group in both `t.after`
+ hooks: `process.kill(-pid, 'SIGKILL')`. Each shell is spawned
+ detached, so it leads its own group. `exec sleep 60` doesn't work.
+ The sweep matches the run dir in `/proc//cmdline`
+ (`host-run.mjs:236`), and exec drops the `$0` that carries it, so the
+ planted process would survive and the "planted process killed" assert
+ would fail. This binds the next row that touches
+ `packages/bus/tests/host-run.test.mjs`. (2026-10-21)
+- **Real-model suite cases run outside the gate.** Row 244 moved
+ `test-task.sh` fork recall (M11) and user recall (M16) behind
+ `MOSAIC_LIVE_PROVIDER=1`, and `test-release.sh` activations to the mock
+ adapter. Before any re-pin whose tree changes `packages/harness` or the
+ session fork code (`scripts/mosaic-task.mjs` session and
+ `sessionForkFrom` handling, the pi adapter), an operator runs
+ `MOSAIC_LIVE_PROVIDER=1 bash scripts/test-task.sh` with the real pi
+ credential and a real `release.sh activate`, and records both results on
+ the re-pin row. Morgana's wording (row 244 BUILD.md), Webby N2 on row 244
+ r1 (#1757). (2026-10-21)
+
+## Queue
+
+Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.
+
+## Done
+
+- Ledger (piece 3, #1506): shipped cd0aa5fb, Gate D written 2026-09-12 (18.9 human messages per closed issue).
+- Relaunched seat shows the old last message (darkwing, Step 3 log, 2026-09-12): closed by #1512 relaunch activity, af4203ca. Filbert authored it; Darkwing and Dewey reviewed. The live board restart and Gate F remain separate.
+- Sage launcher hook uncommitted (2026-09-12): closed by the agents/sage launch files commit, Dewey review R1 revise, R2 approve (`agents/dewey/work/sage-launch-review-2026-09-26.md`), launcher test now covers sage.
+- Ledger counts T3 agent messages as human (2026-09-26, #1506): fixed in ef0020ad (Darkwing 6a, Filbert reviewed). `messageKind` now counts a T3 header as agent and the control-board sender as board. It changes no current count, because no source the ledger reads has a T3 header yet. That waits for the T3 thread source (Gate F).
+- Control board checked the bind address but not Host or Origin (2026-09-26, #1507, found by Dewey): closed by the board guard commit. `foreignRequest` refuses a non-loopback Host, a wrong port, userinfo or a path in Host, and any foreign Origin on every route. Dewey authored it, and Rocko approved it (`agents/rocko/work/board-guard-review-2026-09-26.md`, 5a12f08e). Rocko's one low finding: URL normalization accepts a root slash, empty userinfo and an empty query in Host, so the notes' wording overstates what the raw-syntax check refuses. No bypass was found.
+- Discord engine: leaked fake pi and the timeout gap in `busy` (2026-09-26, #1509): fixed in 3edb15eb (Darkwing 6b R2, Rocko approved, review ed5510a0). Tests stop the engine in `finally`. A turn pi never started now stops pi with exit 1 instead of guessing, and the unit restarts it. Known limit: see the open entry on wedge restarts.
+- Combined concurrent test hangs (2026-09-14, #1509): closed on evidence from 3edb15eb. At default concurrency the union passed 406/406 three times on the R2 snapshot and 408/408 on the commit's index, where clean HEAD had hung four runs out of four. The inherited-intermittency entry from 2026-09-15 stays open until a later run shows it is the same cause.
+- A nested `node --test` passes even when its tests fail (2026-09-26, #1508): closed. Queue A1 (34a72af9) clears NODE_TEST_CONTEXT on its own path. N13 fixes `test-foundation.sh` and `test-discord.sh`, each with a check that fails the suite if the clearing is lost. Darkwing wrote it (n13.patch 00868b2f) and Filbert approved it (e464be6c). Sage's grep on 2026-09-27 finds no other nested `node --test`: in scripts, only the two suites, `test-queue.sh` and `queue-commit.sh`, and in JS, only the queue test that covers the fix.
+- SetSpark service accepted any string as a decision approver (2026-09-26, #1509): closed. Fixed in shared-signals cc74d92 (Rocko approved R2, lead decision 24). The operations seat (T3 12fe8cda) deployed it to VM 1022 at 00:23Z on 2026-09-27. The running `service.py` hash matches cc74d92, and `/healthz` returns 200 inside the network and through the public tunnel. The read-only README survey found one row breaking the rule (DEC-009, Proposed, version 1), one open request that isn't ready (DEC-009's), and no Accepted decision with more than 16 approvers. No rows changed. DEC-009 stays as stored (lead decisions 21 and 28). The service now refuses approvals on it, so nothing can go wrong silently. Side effect: `up -d` also recreated caddy, about 20 s of edge downtime for all hosts. That is for the shared-signals deploy procedure to fix, not Mosaic.
+- Queue suite failed its live checks outside the canonical checkout
+ (2026-09-27, #1508): closed by Piece D, f539466f. `test-queue.sh` now
+ skips `verify` and `render --check` with a printed reason when the
+ toplevel isn't the recorded `canonicalRoot`. An index export passed
+ 27/0, and the canonical checkout 29/0.
+- Queue: an owner listed as a reviewer blocked the row (2026-09-27,
+ #1508): closed by row 31, bb2fa970. `add`, `set reviewers` and `assign`
+ now refuse the owner as a reviewer with exit 2 and write nothing. Rocko
+ built it in the Gate G run, and Filbert approved round 1 (#1508 comment
+ 26629, 8 of 8 mutants caught).
+- Queue follow-ups from rows 12, 13 and 31 (2026-10-04, #1508): closed by
+ row 33, 6fb50cc0. Filbert built it, and Darkwing approved round 1
+ (#1508 comment 26651).
+ - `genesis` refuses a map row whose owner is one of its reviewers (exit
+ 2, nothing written). The check is on the map, not in replay, so a log
+ from before the rule still loads.
+ - The `assign` refusal now reads "can't assign row N to SEAT: SEAT is
+ one of its reviewers".
+ - `queue-commit.sh` compares HEAD with H before each canary and again if
+ the clean run fails. A moved HEAD exits 2 with "HEAD moved since H was
+ recorded"; the guard message is kept for a real guard failure.
+ - Every time and date the validator accepts must format back to itself,
+ so month 13, day 32, 2026-02-30, 2027-02-29 and 24:00 are refused.
+ The live log at rev 45 still replays.
+ - The cold-start failure (2026-09-27): not reproduced in 20 cold runs.
+ Each ran `node --test packages/queue/tests/` in a fresh tree, 10
+ shared clones and 10 exports, one after another. All 20 passed
+ 148/148 (2960 of 2960). The 300 ms deadline test is unchanged. Counts:
+ `agents/filbert/work/queue-33/cold-runs.txt`.
+- CHAT-03 I1 round 2 follow-ups (2026-10-04, #1507): four of five closed
+ by slice 1 S5 (row 40, #1522; Dewey authored, Darkwing and Filbert
+ reviewing). The commit is named in the row's review record when it
+ lands.
+ - Engine command and `preArgs`: `engine` takes only `extraArgs`, `cwd`
+ and `envKeys`. The controller always launches the pinned Pi and
+ checks that at construction and at bind. The fake engine comes in
+ through a symbol key that JSON config can't carry (N24b).
+ - Engine environment: an explicit list (`ENGINE_ENV` in `pi-pin.mjs`)
+ plus `*_API_KEY` and `*_TOKEN` names from `engine.envKeys`. Nothing
+ else is inherited. `HOME` passes, so Pi reads `~/.pi/agent` unless
+ `PI_CODING_AGENT_DIR` is set (Darkwing F3).
+ - `escalating` is cleared when the force-stop fence throws (Darkwing F2,
+ races.test.mjs).
+ - Takeover then Enter in one chunk: input after Ctrl-T or Ctrl-O waits
+ for that action, in the same chunk or a later one, and also while an
+ earlier action is still pending, so it is judged as if typed one key
+ at a time (Filbert F2; Darkwing and Filbert T1 on #1522;
+ flows.test.mjs).
+ The frozen-cgroup test (Filbert F1) stays open above.
+- Gate runs mount the host's pi auth file (Webby N4 on the row 197 proof
+ driver, 059577886): closed by row 244 (#1757), landing e69260a3c. The
+ suites and the lane's `cleanEnv` export a 0600 fixture `PI_AUTH_FILE`,
+ `guardDocker` refuses a Compose run without one, and
+ `packages/cli/tests/gate-lane-auth.test.mjs` proves no gate step mounts
+ the host default. The real-model cases this moved off the gate are open
+ above.
+
+## Row253 F5: legacy Case AS_USER supervision
+
+Tracked as row 276, Gitea #1781 (sage-2331), owner Gander, reviewer Duckworth, after row253.
+Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised legacy AS_USER calls”.
+
+case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
+
+Row 276 / #1781 also owns the case1 root-side capture/tools mutant counterparts (duckworth-0112 N1) and documenting the setuid-member cleanup refusal in the headers (N2). A member that changes uid and cannot be killed causes inner-cleanup refusal, never successful observation. N4 is addressed in row253 by allowing SID confirmation until the existing inner watchdog expires, instead of a separate 200 ms scheduling margin.
diff --git a/agents/gander/work/queue-253/c4/delta-0112/README.md.txt b/agents/gander/work/queue-253/c4/delta-0112/README.md.txt
new file mode 100644
index 000000000..fe04f31e7
--- /dev/null
+++ b/agents/gander/work/queue-253/c4/delta-0112/README.md.txt
@@ -0,0 +1,1055 @@
+# Mosaic Console
+
+Piece 4, #1507. The control board is the first and only WebUI screen.
+
+## Run
+
+Start the existing board in one terminal, then the WebUI in another:
+
+```sh
+node packages/control-board/src/cli.mjs serve
+node packages/webui/src/cli.mjs serve
+```
+
+Open http://127.0.0.1:7330/. Optional `--port N` and
+`--board http://127.0.0.1:7331` select another port or loopback board origin.
+`--business ID` picks the business the bus views read; without it they read
+the running bus host's business (`mosaic bus start `).
+Port 0 picks a free port. Ctrl-C stops each foreground server. No daemon,
+installation, account, login or deployment is added. The preview mode below
+checks one header a sign-in proxy sets; it signs nobody in itself.
+
+Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback
+by default, rejects a repeated Host (400), nonlocal Host and cross-origin
+requests, sends no CORS
+headers, refuses redirects and accepts only JSON object POST bodies up to 4096
+bytes (16 KiB for row 76's send, below). Do not put the board behind a public proxy. Put the Console behind one
+only in the preview mode below, with a proxy that meets its contract.
+
+## Preview behind a sign-in proxy (row 74, #1568)
+
+```sh
+node packages/webui/src/cli.mjs serve --port 7332 \
+ --public-host NAME --auth-header HEADER --allow-user ID
+```
+
+The three flags go together; one or two refuse at start, before any config
+is read. NAME is a DNS name, no scheme or port. HEADER is a header name other
+than Host or Origin; Node lowercases incoming header names, so it matches
+without regard to case (`X-Example-Uid` on the flag finds `x-example-uid`).
+ID is the one user id, 1 to 256 visible ASCII characters without a comma,
+and the value compare is exact. The Console still binds to loopback only. It
+names no identity provider and has no login code: the proxy signs the user
+in and limits sign-in to the right group.
+
+In preview mode:
+
+- A request whose Host is exactly NAME must carry HEADER once, with a value
+ equal to ID. Missing, different or repeated answers 401 `sign-in required`
+ before any route runs, so no bus read or board call is made. An Origin,
+ when sent, must be `https://NAME`.
+- A request whose Host is loopback on the Console's port (`127.x`,
+ `localhost` or `[::1]`) is checked as before and needs no HEADER, so the
+ operator's browser on the host still works. Any other Host is 403.
+- A request that carries HEADER with any Host but NAME answers 403 `proxy
+ Host refused`. The operator's browser never sends HEADER; a proxy that
+ rewrote Host does, and it fails on its first signed-in request instead of reaching
+ the loopback path unchecked.
+- More than one Host header answers 400 before any other check, in this
+ mode or not. Node keeps the first, so a loopback Host followed by
+ `Host: NAME` would otherwise skip the sign-in check.
+- Every method other than GET and HEAD answers 403 `read-only`, whatever the
+ Host. `/api/seen` and `/api/reply` never reach the board, so Seen and Reply
+ fail in the page with that refusal. The one exception is POST
+ `/api/bus/message` when the Console was started with `--send` (next
+ section).
+- The GET routes are unchanged: the bus views, the Queue, Business and
+ Settings reads, the board panel and the assets. If the board is down, its
+ panel shows the existing error and the bus views still answer.
+
+What the proxy must do. The Console trusts HEADER only because the proxy
+controls it:
+
+- It terminates TLS for NAME and is the only thing that reaches the port
+ from off the host.
+- It strips HEADER from every client request and sets it only after the
+ identity provider signs the user in.
+- It forwards only requests whose Host is NAME, and forwards that Host
+ unchanged. nginx rewrites Host to the upstream by default; set
+ `proxy_set_header Host $host;` (or the literal NAME). Caddy and Traefik
+ keep the client's Host by default.
+- Any loopback Host on the Console's port, `localhost` included, is the
+ operator's path and skips the header check, so a proxy that forwarded a
+ client's `Host: 127.0.0.1:7332` would let anyone through without signing
+ in. A tunnel whose local end uses the same port number sends such a Host
+ and reaches the Console unchecked. Anything that reaches a tunnel's local
+ end can send a loopback Host without HEADER, so bind that end to loopback.
+- Every hop on this host that reaches the loopback port is bound by this
+ contract too, not only the edge.
+- It passes the Console's status and body through and doesn't rewrite
+ upstream errors (nginx's `proxy_intercept_errors` with an `error_page`,
+ for example). Under `--send` the page reads a 4xx other than 408 as "not
+ sent", so a proxy that turned the Console's 503 `outcome-unknown` into a
+ 4xx or a redirect would report a message that may exist as not sent.
+
+The local exposure doesn't change ("Data and boundaries"): any process on
+the host can still connect to the port and send `Host: NAME` with HEADER and
+ID, or just a loopback Host, and read what a reader capability reads. The
+user id is not a secret; the sign-in at the edge is what keeps others out.
+Under `--send` the first of those can also send (next section).
+
+To run the Console as a systemd user unit, `scripts/webui-service.sh`
+renders and installs one with these flags (row 75; `docs/TOOLS.md`
+"Console service"). Jason starts and restarts it, and so may Mos under lead
+decision 102, which accepts that sends from a Console Mos started enter the
+trail as Jason. Any other agent starting it works around the human proof
+(lead decisions 62 and 94). Without `--send` the unit is read-only. The script takes `--send` (row 83)
+only with the three preview flags; otherwise it exits 1 and writes nothing.
+With it, the unit's ExecStart ends with `--send`, and the Console needs the
+system config at start: without it serve refuses with `--send needs the bus
+configured` and exits 2, which the unit doesn't restart. `status` says
+whether the installed unit sends.
+
+The unit can run a read-only shared stack copy (rows 77 and 85), with
+serve's --checkout naming the canonical checkout so the queue stays current.
+webui-service.sh copy calls stack-copy.sh copy: the default directory is
+mosaic-stack/copies/, and commits must be on both local and pushed
+refactor. The copy includes bus/CLI/Console code, adapters and pinned npm
+dependencies. install --copy ID refuses unless the bus's effective
+ExecStart and WorkingDirectory name that same copy, including drop-ins.
+It also refuses while the Console runs. With --auth-header X-authentik-uid,
+allow-user must be the 64-character Authentik uid (decision 101). Status
+names the copy and refactor drift. Files and directories are read-only;
+copies are retained. Existing Console-only copies are not migrated or pruned.
+
+With `--instance NAME` (row 190) every verb works on
+`mosaic-webui-NAME.service`, which sets NAME's `MOSAIC_CONFIG`; install
+checks NAME's `mosaic-bus-NAME@` units for the copy. render and install then
+need `--port`, and 7330, 7332 or 7333 as the main or legacy tester port exits 4
+with port-reserved. For every instance, install refuses a `--port` or
+legacy tester port that another installed Console unit in the unit directory
+already takes. That refusal is port-claimed, exit 1, and writes nothing. A
+unit with no `--port` holds 7332. See
+[TOOLS.md, Stack instances](../../docs/TOOLS.md#stack-instances---instance-name-row-190).
+
+The single update and rollback sequence is
+[TOOLS.md, Pinned stack update](../../docs/TOOLS.md#pinned-stack-update).
+A bus restart ends managed sessions, so no PM turn may be in flight.
+Switching the Console, bus or wrapper, changing any unit, and starting or
+stopping either service requires Mos's go for that live step. Agents build
+and test fixtures; they stop before the live sequence.
+
+## Sign-in in the Console (Q175 phase 1, #1690)
+
+`serve --sso [--send] [--tester-socket]` makes the Console an OpenID Connect
+relying party. It reads `console.sso` from the system config (`publicOrigin`
+and one or more providers, each with its issuer, client id, a client secret
+slot, the signing algorithms it accepts and the subjects allowed in). The
+design is `agents/sage/work/q175-sso/DESIGN.md`. `--sso` can't be combined
+with `--public-host`, `--auth-header`, `--allow-user` or `--proof-*`; `--send`
+takes `--sso` in place of the preview set.
+
+- The client secret is read once at start from
+ `/secrets/mosaic-stack/`. The start refuses a slot that is
+ a symbolic link, not a regular file, owned by another user, has any group or
+ other mode bit, or sits in the repository, the `--checkout` directory or a
+ stack copy. The value is never printed.
+- At start the Console fetches each provider's discovery document and JWKS. A
+ provider that can't be reached (a network error, a timeout, a status other
+ than 200, or a body that isn't JSON) leaves the Console up with that
+ provider's `/auth/login` at 503 and a retry every 5 s, doubling, capped at 5
+ minutes. A document that fails a check (issuer, algorithms, S256, a usable
+ key) refuses the start. `/healthz` adds `sso: { : "ready" | "unreachable"
+ | "refused" }`.
+- The Console listener serves only the Host of `publicOrigin`, whatever port
+ it listens on. Any other Host, a loopback one included, gets 403 `public
+ Host required`, except `GET /healthz`, which stays open on loopback for the
+ status and release gates. The loopback operator path in the preview section
+ above does not exist under `--sso`.
+- Without a session, `/api/*` gets 401 `sign-in required` and a page load gets
+ a 303 to `/auth/login?return=`. The page then shows the signed-out line
+ with a link back to sign-in. A session lasts at most 8 hours and ends after 1
+ hour without a page load or a write; a restart ends every session.
+- The page shows Sign out only under `--sso`. It posts to `/auth/logout`, which
+ ends the session and goes to the provider's end-session endpoint when it has
+ one, or to `/auth/signed-out`.
+- With `--tester-socket`, the tester listener refuses any request carrying an
+ `Authorization` header, a `__Host-mosaic_session` or `__Host-mosaic_login`
+ cookie, or an `x-authentik-*` header (403 `tester listener takes no
+ sign-in`). It never serves `/auth/*`.
+
+## Message a role (row 76, #1570)
+
+```sh
+node packages/webui/src/cli.mjs serve --port 7332 \
+ --public-host NAME --auth-header HEADER --allow-user ID --send
+```
+
+`--send` is off by default. It goes only with the three preview flags and
+refuses at start without them. It also needs the system config: without it
+the start refuses with `--send needs the bus configured`, where a read-only
+Console would still serve the board. A unit that sends comes from
+`scripts/webui-service.sh install` with `--send` (above). With `--send` the
+Inbox has a "Message a role" box: a role picker filled from the agents read,
+a text box and Send. The message reaches the role as a message from the
+human, class INFO,
+through `message.send` on the human transport. It goes the same way as one
+sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
+decision 95). Decisions are still answered in a
+terminal. There are no other message classes, no corrections and no seat
+conversations here.
+
+What `--send` grants: two kinds of client can send a human instruction to
+any role while it runs.
+
+- Anyone the proxy lets through as ID.
+- Any local process that reaches the port and sets HEADER to ID with
+ `Host: NAME`. The user id is not a secret. This is the same class of
+ exposure as the human CLI (lead decision 62), accepted for the PoC by
+ lead decision 95.
+
+The request is POST `/api/bus/message` with a JSON object `{to, body,
+in_reply_to?}`. Nothing is sent unless all of these hold, each checked on
+every request:
+
+- Host is NAME and HEADER is ID, once: otherwise 401 `sign-in required`.
+ There is no loopback path. The operator's browser on `127.0.0.1` reads
+ but can't send, and a loopback Host with HEADER is 403 `proxy Host
+ refused`, as above.
+- Origin is exactly `https://NAME`, else 403 `cross-origin request refused`.
+- `Sec-Fetch-Site` is sent once and is exactly `same-origin`, else 403
+ `cross-site request refused`. Browsers set it, and page script can't.
+- Content-Type is `application/json` and the body is at most 16384 bytes
+ and a JSON object with `to` and `body`, optionally `in_reply_to`, and no
+ other key. Anything else is 400 `invalid-request`.
+
+The Console doesn't judge `to` or the body beyond that; the broker does,
+with `message.send`'s own rules. Its body limit, 32768 characters, is past
+the request limit, so the 16 KiB limit binds first. A broker refusal means
+nothing was written. It answers `{ error, message, sent: false }` with the
+read routes' statuses: 400 for `unknown-role`, `invalid-request`,
+`message-not-found` and `request-too-large`, 403 for `human-required` and
+`read-only`, 503 for `not-configured` and `no-bus-host`, 502 otherwise.
+
+When the transport can't say what happened (a timeout, a child that exits
+without a code, an answer that can't be read or that has no message id), the
+message may have been written. The answer is 503 `outcome-unknown` with
+`sent: "unknown"`. Nothing retries, in the server or in the page.
+
+Each send that gets past the sign-in check writes one line to the Console's
+stdout: the time, `send`, the user, the `to` value (JSON-quoted, at most 64
+characters), then `id=` or `error=`. The body is never
+logged. A request refused at sign-in or by the shared Host and Origin
+checks writes no line.
+
+On the page:
+
+- The draft, role and text, survives the ten-second refresh, kept in page
+ memory like a Reply draft. Reloading the page loses it. Send is disabled
+ while a send is pending.
+- Sent shows the message id and links its trail, `#/trail/message/`:
+ the message and its deliveries. A reply is not in the trail; it shows
+ under the receipt (below). The text is cleared unless it was edited
+ during the send.
+- Not sent shows the broker's code and keeps the draft. A redirect counts
+ as not sent: the Console never redirects this route, so only the sign-in
+ proxy does, when its session has expired. The page asks for a reload to
+ sign in again, and signs out (row 95, below).
+- Outcome unknown says the message may have been sent, keeps the draft, and
+ has no trail to link, since no id came back. Check the role's side, or
+ the bus trail from a terminal, before sending again. A 408 counts as
+ unknown, here and on the board's Reply, since a proxy can answer it
+ after forwarding the request (Envoy's `stream_idle_timeout`).
+
+The role's reply comes back as a message to the human with `in_reply_to`
+set to the sent message's id. Since row 81 (#1575) the Sent receipt lists
+those replies, oldest first, each with its role, time, class, body and a
+trail link. It reads them with the bus `replies` read on every render, so a
+reply shows on the next ten-second refresh or Read again. "No reply yet"
+means the read returned none; "could not be read" means it failed, and the
+next refresh tries again. The bus returns at most 50; at 50 the receipt says
+only the first 50 show. The list belongs to the last Sent receipt: a new
+send starts its own, and reloading the page drops it.
+
+The Console only shows replies. It never receives a message or marks one
+delivered or read: `replies` writes no `deliveries` row. So Jason's next
+`mosaic talk` in a terminal still prints each reply, within talk's limits:
+
+- talk receives only after it sends its own message;
+- `--wait 0` returns straight after the send and receives nothing;
+- otherwise each receive returns every undelivered message to the human, up
+ to 100 a call. The broker records each one delivered when
+ `message.receive` returns it, so the next receive doesn't return it
+ again; talk prints each one and marks it read.
+
+## Use
+
+- Waiting on you uses the board's `waitingOnYou` flag across all projects.
+- Select a project on the left to filter the session table. Counts show visible
+ rows out of the total when Hide offline or Hide seen hides anything.
+- Select an agent to open its inspector. Arrow keys move between table agent
+ buttons, Enter opens, and Close or Escape in the inspector returns focus.
+- Seen clears the row from Waiting on you until the board detects new activity.
+ The collapsed Seen section keeps those rows available; Unsee returns them.
+- Reply appears only where the board's registration permits it. A failure shows
+ the board's stderr and keeps the draft. Transport success is the board's
+ `delivered` receipt, not proof that the seat processed the message.
+- Refresh runs the existing board scan. Automatic refresh uses the board page's
+ ten-second interval; Pause stops it. Failed refreshes keep the last snapshot
+ with a warning. There are no automatic action retries.
+- Palette and appearance come from `tokens.css` (see "Shell" below) and persist
+ in this browser when local storage is available. Appearance defaults to
+ System. Settings (row 54) shows the same control and nothing else that
+ changes.
+- History opens a read-only conversation view for a seat, from its Waiting
+ card, its table row or its inspector (#1507, CHAT-02). It shows the whole
+ branch with nothing clipped. Tool calls, tool results and thinking start
+ collapsed. Session text is always shown as text: Markdown stays as source,
+ and terminal controls, bidi controls and marks show as visible symbols. Reply in the view
+ uses the same board reply path as the inspector.
+- The view checks for new entries on the same ten-second refresh, and Pause
+ stops it. It never switches on its own. A fork, a newer session for the seat
+ or a rewritten file each shows a marker with a button to open the other
+ history. Reloading after a rewrite keeps the branch the view was on; if that
+ branch is gone, the view opens the latest one and says so. Session lists
+ older sessions for the seat, and is empty while the list is read or when
+ that read fails. Only repository Pi seats
+ have history; other harnesses say so.
+
+Drafts and receipts stay in page memory, including across refresh, inspector
+changes and a stale registration. Reloading or closing the page loses them.
+A pending send stays disabled across refresh; new text typed during a send is
+not cleared by the earlier send's success. If the proxy loses the response,
+delivery may be unknown: inspect the seat before sending again.
+
+## Inbox, tasks, agents and trails (slice 1 S5, #1522)
+
+The Console sidebar adds Inbox, Tasks and Agents next to the control board,
+which stays at `#/`. Routes: `#/inbox`, `#/inbox/`, `#/messages`, `#/tasks`,
+`#/tasks/`, `#/agents`, `#/trail/task/[`,
+`#/trail/decision/` and `#/trail/message/` (row 76), with `?kind=` filters on a trail. The design
+note is `agents/dewey/work/wui/SLICE1-VIEWS.md`.
+
+- Every view reads the four verbs of the Q1 module (`packages/bus/src/views.mjs`,
+ lead decision 56): `inbox`, `tasks`, `agents` and `trail`. The CLI reads the
+ same functions, so both show the same broker data.
+- Nothing writes but the "Message a role" box, the Settings update, the Assistant
+ section's three buttons and the spec and docs editor's Save, all under `--send` (above,
+ "Assistant settings" and "Spec and docs editor"). A decision shows `mosaic decide ` for each choice,
+ with Copy, which only puts the command on the clipboard. It is answered in a
+ terminal (REQ-DEC-3, Q4). Seen is not set from the browser either. The
+ command has no `--business`, so `mosaic decide` uses the running bus host's
+ business, as the Console does by default. From a Console started with
+ `--business` for another business the copied command refuses (no bus host,
+ or no such decision) rather than answering elsewhere.
+- What no Q1 verb returns is labelled where it would show, for example
+ "Bot names: not in the Q1 module" (lead decision 63). Nothing is guessed.
+- A task's list row can only say its current state came from a Vikunja read,
+ since the list has no trail. The task page reads the trail and tells a task
+ the stack never created from one changed in Vikunja after the stack wrote it.
+ A stale poll read is shown in the snapshots and never as current.
+- The task page's "Who may write" section lists each field with the verbs
+ that write it and the writer each verb checks: pm, the assigned role or any
+ role (row 61, lead decision 113). Whether a write also needs a resolved
+ decision is the role's authority, on the Business page. If the table read
+ fails, the section says why and the rest of the page still shows.
+- The Tasks table has Requirement and Priority columns, from the `tasks` row's
+ `requirement` and `priority` (row 58, #1547). Either one shows "none" when
+ the row has none. A tombstone's page shows the priority of the last snapshot
+ that read the task.
+- All bus- and agent-authored text is rendered inert: controls show as control
+ pictures or `[U+XXXX]`, and Markdown stays as source.
+- Views refresh on the board's ten-second interval and stop on Pause. Focus,
+ open sections and the copy status survive a refresh.
+- A failed read keeps the last good answer for that read and says it is old,
+ with the time it was read. A refusal (403, `not-configured`, `no-bus-host`,
+ or row 54's `queue-refused`) never shows kept rows (row 53), and it drops
+ every kept read: the Inbox count and the palette's decisions, tasks and
+ queue rows go too, until a read succeeds.
+ A page never read, or refused, shows the failure instead,
+ titled "Bus refused the read" (403: the bus refused the Console's read, for
+ example `human-required` from a Console started inside an agent run), "No bus to read" (no system
+ config, or no bus host and no `--business`) or "The read failed".
+
+## Agent state (row 59, #1548)
+
+`#/agents` adds State and Attention columns. State is the board's mark
+(● working, ◐ waiting, – idle) from the bus `agents` verb, with the number
+of messages and since when for a working role. Attention shows
+"Input needed:" with the first line of the role's first open decision,
+linked to `#/inbox/`, a blocking chip, and "+N more" to the Inbox when
+there are others. The page says what each state means. It writes nothing;
+the server passes the rows through unchanged. How the bus derives the state
+and its limits: `packages/bus/README.md`, "Agent state".
+
+## Board: held roles and signing out (row 95, #1589)
+
+The Board (`#/`) opens with **Held roles**: each role a run holds on the
+bus, from the same bus `agents` read as `#/agents`, including a claim with
+no address ("no address"). Each row shows the role, its state and attention
+(as in "Agent state"), the address, the holding run with its harness, and
+since when. With send on (`--send`, "Message a role"), each row has a
+Message action: it opens `#/inbox` with the send form addressed to that role
+and the caret in the message. Without send there is no action, and the
+section says so. A refused or failed first read says why and shows no roles
+rather than a guess; a failed later read keeps the last roles under a stale
+warning.
+
+The machine scan's sessions (Waiting on you, Seen, All sessions and the
+project list) sit behind **Show machine sessions**, off by default. While
+it is off the page makes no `/api/board` request and polls nothing for it.
+The choice is kept in this browser's local storage under
+`mosaic-console-board` (`{"machine":true}`), like the appearance; where
+storage is refused it starts off on each load. Turning it off drops the
+board data and closes an open conversation.
+
+Every Console fetch the page makes uses `redirect: 'manual'`. The Console
+never redirects, so a redirect answer (`opaqueredirect`) is the sign-in
+proxy's: its session has ended. The first one, on a board read, a bus read,
+a page read or a send, signs the page out:
+
+- One line says "Signed out. Reload the page to sign in." No NetworkError,
+ failure banner or stale warning is shown for it.
+- Everything drawn from a Console read is removed (the board, the held
+ roles, the bus views, an open conversation and the inspector), because
+ none of it can be refreshed. A read that was in flight is dropped when it
+ lands.
+- Refresh and Pause are disabled; nothing polls and no route change reads.
+- The send form, if open, keeps its draft and receipt so the text isn't
+ lost; a further Send is refused in the page ("signed out"). A board
+ Reply that meets the redirect is not sent, and its inspector closes with
+ the rest of the board, so its draft isn't shown again.
+- Reloading the page goes through the sign-in proxy again.
+
+How long a sign-in lasts is the edge's setting (ops-01), not the Console's.
+
+## Shell (row 53, #1542)
+
+The Console is restyled to `docs/design` (`IMPLEMENTING.md`). It is a restyle
+only: no new data, route or write.
+
+- `src/public/tokens.css` and `src/public/icons.svg` are byte copies of
+ `docs/design/tokens.css` and `docs/design/icons.svg`; `tests/shell.test.mjs`
+ fails if either differs. Edit `docs/design`, rebuild there, then copy.
+- Colours come only from `tokens.css`, selected by `data-palette` and
+ `data-mode` on ``. The page sets no colour inline. Appearance System
+ removes `data-mode`, so the OS light or dark setting applies; it is the
+ default, with Harbor. The choice is kept under `mosaic-console-appearance`.
+- `shell.css` loads last and holds the restyle: radii `--r` 6px and `--r-lg`
+ 10px, the type scale, tabular numbers, the command bar, section list, dense
+ table, inspector, status marks, class chips, toast, command palette and
+ not-found page. The earlier CSS files are unchanged.
+- JetBrains Mono 400, 500 and 700 come from the official JetBrains release
+ (v2.304); `assets/fonts/jetbrains-mono-sources.txt` has the URL and sha256
+ of the archive and each file, and `jetbrains-mono-OFL.txt` the licence.
+ Manrope has no 800 file here, so 800 headings render at 700.
+- Ctrl+K (or the search field) opens the palette over views, inbox decisions,
+ tasks and (row 54) queue rows from the last queue read; arrows move, Enter opens, Esc closes and returns focus. Arrow
+ keys also move between task links in the bus tables.
+- Copy reports in a toast. If the clipboard is unavailable, the toast stays
+ until dismissed and the command is selected for Ctrl+C. When no toast shows,
+ the toast is an empty live region, visually hidden but still in the
+ accessibility tree, so the first copy is announced.
+- The command bar shows, in UTC, when the board was scanned and the bus read,
+ and says when the last attempt failed. A board refusal drops the last scan
+ and everything drawn from it: the cards, the table, the inspector, the
+ project filter and tree, the counts, the footer and the scan line all say
+ the board was not read. A failed read with no scan held, after a refusal or
+ before the first scan, shows the board empty and says the read failed; it
+ never brings back rows from a refused scan. A refusal also closes an open
+ conversation and clears its history. If that removes what had focus, focus
+ moves to the page.
+- From 760px the footer is a one-line status bar and the side column fits
+ between it and the command bar. Below 760px the section list is one strip
+ that scrolls inside itself.
+
+## Queue, Business and Settings (row 54, #1543)
+
+Three read-only views, in the section list and the palette. Each has the
+same five states as the bus views: loading (the skeleton names the source),
+normal, empty, a failed read over kept data (labelled stale, with its time),
+and a refusal that shows nothing kept. The command bar names the source:
+"Queue read …", "Business not read: refused". A refusal shows the code in
+mono and the module's own message, redacted by the server.
+
+- `#/queue` lists every row with its state, owner, reviewers, brief and last
+ update, filterable by state. `#/queue/` adds the gate, issues, claim,
+ `after`, note and review rounds. Rows move only with
+ `scripts/mosaic queue move` in a terminal; the row page shows that command
+ with a Copy button and has no button that moves a row. The brief is shown
+ as its path and anchor, because Console serves no repository files.
+- `#/business` shows the business named by `--business`, else the running
+ bus host's business: the human, the arbiters as instance and kind ("pm
+ (delivery), cto (technical)"), projects, launch rules, the vars
+ on an allowlist, each role instance with its holder and harness, the
+ authority of each instance for each action as the business package
+ classifies it (`resolveInstance`, `classify`), and credentials as
+ metadata only: service, account, role, expiry or rotate-by date, and a
+ state worked out from the date alone. A missing or invalid business file
+ is a `not-configured` refusal with the business module's own text.
+- `#/settings` shows appearance (the row 53 control, kept in this browser),
+ the notifier binding by name only, and about: the Console's loopback
+ address, `RELEASE`, the business and the board URL. A Settings refusal
+ still shows appearance, because it needs no read.
+
+Console never reads a token, a token file or a credential variable, and no
+response, page or log carries one, a file path or a Discord channel, guild or
+user id. The server redacts every message and string field before it answers
+(`redactor` in `src/reads.mjs`): the config directory becomes ``, the
+data root ``, any other absolute or `~/` path ``, a 17 to 20
+digit run ``, and a JSON parse error's quote of the file is dropped. The
+startup log goes through the same redactor. A path starts at the beginning
+of the text or after a space, an opening bracket, a quote, `=`, `,`, `<` or
+`:`, so `file:/x` and `file:///x` are redacted and `https://host/x` is not.
+Known gap: a path stops at the first space, so a path containing a space is
+redacted only up to that space and the rest shows. A bare `~` or `~user/`
+is not treated as a path, and neither is a relative path.
+
+## Spec and docs editor (row 148, #1642)
+
+The Docs page (`#/docs`) lists, for each project of the business, spec/north-star.md and
+the Markdown files directly under docs/. A file opens at `#/docs//` with
+its current commit. The page is a `]