Status: written by Sage, lead. It follows `docs/plans/BRIEF-TEMPLATE.md`. It covers the faults from Jason's first PM test that rows 89 to 92 don't: the PM's findings file (`/workspaces/mosaic-stack/pm/findings-2026-10-12.md`) faults 3, 5 and 8, Jason's "blind after each send" report (Mos sage-0103) and his Console report (Mos sage-0105). Fault 9 went into row 89. Jason's ruling through Mos (sage-0104, Q125 yes): these are fixed through Sage's rows, not by the running PM, then the stack is redeployed and his test re-run. Gitea issues #1587, #1588 and #1589. # The fixes from Jason's PM test (2026-10-12) ## A review loop through the bus ### Problem The coder role holds `git.push.working` and `review.request`, and the reviewer holds `review.verdict` (`roles/coder.json:9`, `roles/reviewer.json:9`). The broker lists all three actions (`packages/bus/src/broker.mjs:13-17`) and records `review.*` events against a task (`:254`), but no verb handles them and no typed tool exists (`packages/harness/src/tools.mjs`). In the test the coder couldn't hand its change to the reviewer except by pasting it into a message, the reviewer sent its verdict as a message, and the PM cited that message in `task_close`. P19 #229's review asked for changes only because nothing could be pushed. ### Owner and reviewers Owner: Filbert, after row 91 (a project checkout in each role workspace) and their row 95. Reviewers: Darkwing and Dewey, both required, since the host runs git for a session. ### Files owned - `packages/bus/src/broker.mjs`, the new review verbs only - `packages/cli/src/launcher.mjs` and `packages/seat/src/checkout.mjs`, only the host-side push and fetch - `packages/harness/src/tools.mjs`, `runner.mjs`, the new tools only - `roles/coder.md`, `roles/reviewer.md`, `roles/pm.md`, the review paragraphs only - `packages/bus/README.md`, `packages/harness/README.md`, `packages/seat/README.md` - tests under `packages/bus/tests/`, `packages/harness/tests/`, `packages/seat/tests/` and `packages/cli/tests/` - `agents/filbert/work/queue-/` ### What ships - A working repository per business and project, a bare git repository under the data root that only the host writes. It is not the project's root and not a forge. No credential goes near it. - `push_working` (action `git.push.working`): the host, outside the session's sandbox, pushes one branch of the caller's clone to the working repository as `work/`. It takes a task ref and a branch, refuses anything but a fast-forward of that one ref, and records the commit. - `request_review` (action `review.request`): records a `review.request` event on the task, naming the commit already in the working repository, and messages the reviewer role. A commit not in the working repository refuses. - `fetch_review` for holders of `review.verdict`: the host fetches the requested commit into the caller's clone as a remote-tracking ref, so the reviewer reads and tests it in its own workspace. - `review_verdict` (action `review.verdict`): records `approve` or `changes` with a body on the task, citing the request event. The PM's `task_close` cites the verdict event's id. Whether the broker checks that citation is a choice the packet makes and argues. - The session text from the bundle names the review steps; the role files say the same. - Tests: the whole loop between a fake coder and a fake reviewer session over real clones, each refusal (another task's branch, a non-fast-forward, an unknown commit, a role without the action), and the project root unchanged after every case. Suites: the bus, harness, seat and cli node suites, and every `scripts/test-*.sh`. ### Out of scope Landing an approved commit in the project, which stays Sage's gate under R28. `git.push.protected`. A forge or Gitea remote. Credentials in any session. ### Gate Darkwing and Dewey approve on the row's issue, naming the candidate manifest. The suites above pass on Sage's gate rerun. Live at the next bus restart, which Mos approves first (sage-0104). ## Every message to the human, live ### Problem `mosaic talk` sends, then prints whatever reaches the human until the direct reply comes, and stops there (`packages/cli/src/cli.mjs:204-214`). A message the PM sends later, from a turn another seat or the PM itself started, has no `in_reply_to` and waits unread until the next `talk` prints it. Jason reads that as each answer arriving one message late. `mosaic inbox` lists decisions only (`broker.mjs:846-851`). The Console shows replies to its own send (row 81, the `replies` verb, `broker.mjs:915-925`) and nothing else, so it never shows those messages either. ### Owner and reviewer Owner: Rocko, after row 77, and before their rows 84 and 85. Reviewer: Dewey, who built row 81. ### Files owned - `packages/bus/src/broker.mjs`, a `messages` read verb only - `packages/cli/src/cli.mjs`, `talk` and `inbox` only - `packages/webui/src/serve.mjs` (`BUS_VERBS`), `packages/webui/src/public/bus.js`, `bus.css` if needed - `packages/bus/README.md`, `packages/cli/README.md`, `packages/webui/README.md` - tests under `packages/bus/tests/`, `packages/cli/tests/` and `packages/webui/tests/` - `agents/rocko/work/queue-/` ### What ships - A broker `messages` read verb for the human and the Console's reader: messages to the human, newest first, with `limit` (default 50, at most 200), `after` for paging, and whether each is read. Reading it marks nothing. - `mosaic talk --follow`: after the send, it keeps printing each message to the human as it arrives, marking it read as `talk` does today, until Ctrl-C. `mosaic follow` does the same without sending. Without the flag, `talk` is unchanged. - `mosaic inbox` lists unread messages to the human under the decisions, without marking them read. - A Messages page in the Console, refreshing like the other bus pages, shows every message to the human with its sender, class, time, read state and a trail link. It marks nothing, so `talk` still prints each one. The nav shows the unread count. - Tests: a message with no `in_reply_to` arriving after a `talk` returns is printed by `talk --follow` and by `follow`, listed by `inbox` and shown in the Console; nothing the Console reads is marked read; the verb's paging and refusals. Suites: the bus, cli and webui node suites, and every `scripts/test-*.sh`. ### Out of scope Pushing messages to the browser (polling stays). Marking messages read from the Console. Messages between roles. ### Gate Dewey approves on the row's issue, naming the candidate manifest. The suites above pass on Sage's gate rerun. Sage notifies Mos before the landing; it is live after the bus and Console restart Mos approves. ## The Console's Board shows the stack's seats and a sign-out ### Problem The Console's Board is the machine scan the old board server serves at `127.0.0.1:7331` (`packages/webui/src/serve.mjs:8`, proxied as `/api/board`). It lists tmux and T3 sessions. The stack's own seats (the PM, coder and reviewer) appear only on the Agents page (`packages/webui/src/public/bus.js:410-415`), and a message to them only from the Inbox page's "Message a role" form (`bus.js:270`). In the test Jason saw the fleet's seats and not the stack's (findings fault 8, P19 #231), and he approved hiding the machine sessions. Jason also reports he signs in to mosaic.woltje.com and then can't see updated info or interact (Mos sage-0105). Checked 2026-10-12: - The edge admits the Console. ops-01 read the outpost log: forward-auth 302s only from 19:28:16Z to 19:29:26Z, after a pod replacement at 19:28:07Z, then 225 browser 200s and no 302 to 19:48:25Z (Mos sage-0106). The Console has no websocket; every page polls with `fetch`. - The Board's data is current: the scan at 7331 answered in 1.3 s with `generatedAt` 2026-10-12T19:49:24Z, 44 sessions, projects `mosaic-stack` and `fleet`, all machine sessions. - The running Console started at 19:05:38Z, before row 81 (c9a5650e) landed, so it shows no reply to a web send. Its log has no send line since then. So the Console works as built, and what Jason needs from it isn't there: the stack's seats aren't on the Board, the send is on another page, the replies aren't deployed, and messages the PM starts never show (the row above). During the 19:28 sign-in loss every Console API fetch got a 302 to the sign-in origin (the same answer an unauthenticated `GET https://mosaic.woltje.com/api/board` gets). A fetch that follows a redirect across origins fails with Firefox's "NetworkError when attempting to fetch resource", which the Board showed earlier today. The Console keeps the last data and retries every 10 s, failing the same way until the page is reloaded (`app.js:290-291`, `bus.js:31,42`). A pod replacement will do this again. ### Owner and reviewer Owner: Filbert. Reviewer: Darkwing. ### Files owned - `packages/webui/src/public/app.js`, `bus.js`, `index.html`, `app.css` - `packages/webui/src/serve.mjs`, only if the Board needs a new route - `packages/webui/README.md` - tests under `packages/webui/tests/` - `agents/filbert/work/queue-/` ### What ships - The Board's first section lists the stack's held roles from the bus `agents` read, including claims with no address, each with its state and a Message action that opens the send form addressed to that role. - The machine scan's sessions sit behind a "Show machine sessions" toggle, off by default, kept in the page's settings like the other Board filters. - Every Console fetch uses `redirect: 'manual'`. A redirect answer (`opaqueredirect`) stops the refresh and shows one line: signed out, reload the page to sign in. It doesn't show a NetworkError or stale data as current. - Tests: the Board with held roles and no machine sessions, a claim with no address, the toggle, the Message action's role, and a redirect answer on a Board read, a bus read and a send. Suites: the webui and control-board node suites, and every `scripts/test-*.sh`. ### Out of scope The edge's sign-in lifetime, which is ops-01's through Mos. Removing the machine scan. A Board for other businesses. ### Gate Darkwing approves on the row's issue, naming the candidate manifest. The suites above pass on Sage's gate rerun. Live after the Console restart Mos approves. ## Row 95 follow-up: Board tests fail fast, README lists every redirect ### Problem Darkwing's review of row 95 (comment 27953, approve) left three notes, and row 95 landed in 17ad6f35 without a second round. - N1: `packages/webui/tests/browser-edge.test.mjs` awaits `boardRequested` (line 29), which only the server's `/api/board` handler resolves. If the "Show machine sessions" toggle stops reading the board, the await never settles. The test is cancelled at 120 s, its `finally` never closes the browser and server, and the file never exits. `node --test` has no file timeout, so the webui suite stalls instead of failing. - N2: test 2 in `board-held.test.mjs` claims no further request on the poll, but it waits 600 ms and both polls are 10 s. Mutants 13 and 27 survive for that reason. - N3: the webui README's test paragraph says test 2 redirects three reads. It redirects six: the board, `agents`, `/api/conversations`, `/api/seen`, `/api/queue` and the send. ### Owner and reviewer Owner: assigned on the row. Reviewer: Darkwing. ### Files owned - `packages/webui/tests/browser-edge.test.mjs`, `packages/webui/tests/board-held.test.mjs`, and any test helper they share - `packages/webui/README.md`, the test paragraph only - `agents//work/queue-/` ### What ships - N1: the toggle step in `browser-edge.test.mjs` fails within a few seconds when no `/api/board` request comes, and the browser and server close either way. Prove it with the toggle-on mutant: the file exits nonzero inside 30 s, with no browser or node process left. - N2: either test 2 reaches a poll tick, by a shorter poll in the test or a clock the test controls, and mutants 13 and 27 die; or the test's comment and the README stop claiming the poll. The packet says which, and why. - N3: the README's test paragraph names all six redirected requests. Suites: the webui and control-board node suites, and every `scripts/test-*.sh`. ### Out of scope Any change under `packages/webui/src/`. Row 95's behavior stays as landed. ### Gate Darkwing approves on the row's issue, naming the candidate manifest. The suites above pass on Sage's gate rerun. Tests and README only, so nothing goes live and no restart is involved. ## Row 94 follow-up: text-mode inbox works against a pre-94 bus ### Problem Row 94 (2b2a9e14) made text-mode `mosaic inbox` call the bus `messages` verb after `inbox`. The running bus predates row 94 and has no `messages` verb, so it answers `unknown-verb`. Text-mode inbox then fails and prints nothing, not even the decisions. `--json` still works because it returns before the messages call. Mos found it; Sage reproduced it in-process with a stub transport. The bus restart stays where decision 118 puts it, so the CLI has to cope (decision 132). ### Owner and reviewer A Haiku subagent builds it under decision 129, owner `researcher`. Filbert reviews. ### Files owned - `packages/cli/src/cli.mjs`, the text-mode branch of the `inbox` verb only. - `packages/cli/tests/` (the existing cli test file that covers `inbox`, or a new one). ### What ships - When the `messages` call fails with error code `unknown-verb`, text-mode inbox prints the decisions as it did before row 94, then one line, `messages: not available until the bus restarts`, and exits 0. - Any other error from the `messages` call still fails as it does now. - `--json` is unchanged. - Tests, with a stub transport as the existing cli tests use: the unknown-verb case prints the decisions and the line and exits 0; another error code still fails; the normal path still lists unread messages. Each test is shown to fail on a mutant that removes or widens the fallback. Suites: the cli node suite and every `scripts/test-*.sh`. ### Out of scope The bus, the Console, `talk`, `messages` and `follow`. No restart. ### Gate Filbert approves on the row's issue, naming the candidate manifest. The suites above pass on Sage's gate rerun. `cli.mjs` applies to new CLI runs as soon as it lands, so it gets decision-109 notices to Mos before and after landing. ## Row 93 follow-up: git socket hardening and refusal tests ### Problem Row 93 (4da68152, #1587) landed with approvals from Dewey (28196) and Sage (28197), and neither blocked. Their non-blocking notes, with Darkwing's `agents/darkwing/work/queue-93/r1-notes.md` (eabd5982), leave these gaps: - Five host refusals have no test: `not-a-role`, `host-closing`, `git-busy`, `root-unreadable`, `unknown-project`. - `working.mjs` kills a timed-out git with SIGKILL, so git never removes its lockfiles. A stale `refs/heads/.lock` then blocks every later push to that branch until an operator removes it. - When `record` throws, the work ref's rollback `update-ref` result isn't checked. A failed rollback leaves an unrecorded commit on the work ref, and nothing reports it. - `readBody` in `git-socket.mjs` has only a per-chunk idle timeout. A session that sends a byte every 29 seconds holds a slot, and eight such sessions make the socket refuse `git-busy` for everyone. - A reviewer that disconnects mid-fetch is filed as `git-failed`. - The runner fills `have` (HAVE_MAX 32) from all origin and working refs in sort order, so with many refs it can drop the ones that matter. - The repository lock is in-process: it assumes one launcher per dataRoot, and no README says so. ### Owner and reviewer Darkwing builds it. Dewey and Sage review. ### Files owned - `packages/seat/src/working.mjs`, `packages/seat/tests/working.test.mjs`, `packages/seat/README.md` - `packages/cli/src/git-socket.mjs`, `packages/cli/tests/git-socket.test.mjs`, `packages/cli/README.md` - `packages/harness/src/git-tools.mjs` and its tests, for `have` only ### What ships - One test per untested refusal named above. - On timeout, host git gets SIGTERM, a short grace period, then SIGKILL. A test shows a timed-out push leaves no ref lock behind, and that the next push to the branch lands. - A failed rollback refuses `rollback-failed` and is logged by the host, in place of the record's code. The broker's own refusal is already on the trail. Test it with a rollback forced to fail. - `readBody` has a total deadline as well as the idle timeout. The deadline is a stated constant, or a stated function of the request's size. A trickling client test refuses within it and frees its slot. - A client gone mid-fetch is filed as `fetch-aborted`, not `git-failed`. - The runner's `have` lists the working refs before origin's and never more than HAVE_MAX. A test uses more than 32 refs. - The seat README says the lock assumes one launcher per dataRoot. The reviewer docs say what `nothing-to-fetch` means. Each new check is shown to fail on a mutant that removes it. Suites: every node suite and every `scripts/test-*.sh`. ### Out of scope - Tying a push's project to the task's own project, and checking that the pusher is the task's assignee. Both need a design ruling first. - gc of objects from refused pushes. - Any broker or schema change. ### Gate Dewey and Sage approve on the row's issue, naming the candidate manifest, and the suites above pass on Sage's gate rerun. The cli and seat changes reach the host only at a bus restart. That restart needs Mos's go, and it batches with row 80's. Decision 109 notices go to Mos before and after landing.