From 1c5f6bc3a0c831d8bb5933b251449aef6bc792cf Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 26 Sep 2026 16:05:16 -0500 Subject: [PATCH] docs(queue): queue-as-data plan round 6, Rocko approved (#1508) Plan 282fabbb (Filbert) and the six adversarial rounds (Rocko, r6 80cde839). Lead item 15: Gate F first, then A1 and A2 as separate reviewed commits. Co-Authored-By: Claude Opus 5.5 --- .../work/queue-as-data-plan-2026-09-26.md | 2404 +++++++++++++++++ .../queue-as-data-adversarial-2026-09-26.md | 403 +++ ...queue-as-data-adversarial-r2-2026-09-26.md | 352 +++ ...queue-as-data-adversarial-r3-2026-09-26.md | 410 +++ ...queue-as-data-adversarial-r4-2026-09-26.md | 170 ++ ...queue-as-data-adversarial-r5-2026-09-26.md | 133 + ...queue-as-data-adversarial-r6-2026-09-26.md | 63 + docs/plans/2026-09-26_lead-decisions.md | 9 + 8 files changed, 3944 insertions(+) create mode 100644 agents/filbert/work/queue-as-data-plan-2026-09-26.md create mode 100644 agents/rocko/work/queue-as-data-adversarial-2026-09-26.md create mode 100644 agents/rocko/work/queue-as-data-adversarial-r2-2026-09-26.md create mode 100644 agents/rocko/work/queue-as-data-adversarial-r3-2026-09-26.md create mode 100644 agents/rocko/work/queue-as-data-adversarial-r4-2026-09-26.md create mode 100644 agents/rocko/work/queue-as-data-adversarial-r5-2026-09-26.md create mode 100644 agents/rocko/work/queue-as-data-adversarial-r6-2026-09-26.md diff --git a/agents/filbert/work/queue-as-data-plan-2026-09-26.md b/agents/filbert/work/queue-as-data-plan-2026-09-26.md new file mode 100644 index 00000000..9b922324 --- /dev/null +++ b/agents/filbert/work/queue-as-data-plan-2026-09-26.md @@ -0,0 +1,2404 @@ +# Queue as data (#1508, QUEUE rows 9–13): build plan + +Author: Filbert (T3 thread 9cb9731e), for Sage (lead). Planning only; no +source edits. Written 2026-09-26 against HEAD 43d7574d plus the shared +uncommitted tree. Brief: `docs/plans/2026-09-13_queue-as-data.md`. + +Evidence was gathered read-only. Two read-only Gitea calls were made +(`GET user`, `GET issues/N` for the issues the queue references). + +> **Section 8 is the active specification (round 6).** Where sections 0–7 +> differ from it, section 8 wins; sections 0–7 are kept as the record of +> how the design got here. + +## 0. Status (updated 2026-09-26) + +**Round 6:** Rocko's round 5 +(`agents/rocko/work/queue-as-data-adversarial-r5-2026-09-26.md`, +3b031a70…177e) returned "revise" with two medium findings. G1: Gate G +pinned session evidence that Pi hasn't written yet. G2: matching hook bytes +don't prove the guard runs. Section 8 answers both (8.11, 8.12), and 8.19 +maps them. No owner ruling was needed. + +**Round 5:** Rocko's round 4 +(`agents/rocko/work/queue-as-data-adversarial-r4-2026-09-26.md`, +fcb8933d…efcff) returned "revise", with the lead choices accepted and five +residuals, F1–F5. Section 8 answers them, and 8.18 maps each one. Jason's +per-seat token ruling for D is in 8.9. No new owner ruling was needed. + +**Round 4:** Rocko's round 3 +(`agents/rocko/work/queue-as-data-adversarial-r3-2026-09-26.md`, +13a32804…a274) returned "revise", with findings T1–T8 and the architecture +and rulings accepted. Section 8 now answers them, and 8.17 maps each one. +Lead-level choices for Sage are listed at the end of 8.15. No new owner +ruling was needed. + +**Round 3:** Rocko's round 2 (`agents/rocko/work/queue-as-data-adversarial-r2-2026-09-26.md`, +b3a2d72a…e70f4) returned "revise". Section 8 applies Sage's +direction: fail closed, manual recovery verbs, one host, canonical checkout +only. One departure is stated in 8.0: the log lives inside `queue.json`, +so there is no journal file to tear. Sage, deciding as lead on Jason's +instruction, has ruled on every open item, Q1–Q4 and J1–J9 (8.15). The +round-2 text below about nine items, the journal file and incarnation is +superseded. + +Sage (lead) reviewed this plan and decided Q5–Q9 and the transition fixes +in 5.3 and 5.4 (see section 4, "Decided"). Q1–Q4 are the open set. Sage is +holding them for Jason until his next-phase target arrives, since that +target may settle Q2. + +Build assignment: not yet made. Rows 9–13 wait for row 6 to close and for +Jason's target. When they open, the brief's split stands: Darkwing builds, +Filbert reviews. Sage may move Filbert to author if Darkwing is still on +row 6. + +What each open question gates: + +| Open | Gates | Buildable before the ruling | +|---|---|---| +| Q1 Gate G wording | Gate G only | A, B, C, D, E | +| Q2 T3 identity and runtime | Gate G; `next` with no seat argument | A (refuse without identity), B, C, E | +| Q3 automatic commit | A's write path | A with no commit step (recommendation changed, section 7) | +| Q4 posting identity | D | A, B, C, E | + +**Round 2 (2026-09-26):** Rocko's adversarial review +(`agents/rocko/work/queue-as-data-adversarial-2026-09-26.md`, SHA-256 +df433f93…2cbf5c, reviewed this plan at 59d5a22f…) returned "revise" with +14 findings. Section 7 disposes of each. Sage amended Q6 (per R9) and Q7 +(per R8). **The Q3 recommendation has changed: no automatic commit.** +Section 7.2 lists nine further items that need Jason (J1–J9), plus the +two Sage is already holding for him (reduced liveness gate, E before D). + +## 1. Build order and dependencies + +``` +row 6 closes (Gate F or blocked) + └─ A queue record + CLI + render + migration (row 9) + ├─ C brief template + `add` brief refusal (row 11, ships inside A's round) + ├─ B AGENTS.md + CONTEXT.md point at the command (row 10) + │ └─ Gate G demonstration (verifies A and B) + ├─ D review request posted by `move in-review` (row 12; needs identity ruling Q4) + └─ E ledger queue section (row 13; Q6, Q7 decided) +``` + +- **A first.** Every other piece reads or writes `queue.json`. +- **C with A.** The `--brief must exist` refusal is A's code. The template + is one file. Shipping them separately would mean a second review round on + the same CLI for one check. +- **B after A, before Gate G.** Gate G tells the seat the command directly, + so it could technically pass without B. Running it after B also verifies + B, which the brief says Gate G does. +- **D and E are independent of each other** in code. The brief says A to E + ship in order, so E follows D unless Jason allows E first while Q4 holds + D (R14, needs Jason). E's rulings (Q6, Q7, as amended) are decided. +- **A is larger than the brief's "one commit-sized scope"** after round 2: + the journal, lock and verify (7, R1/R3) roughly double it. It may land as + two commits under one row: A1 = journal, lock, CLI and verify; A2 = + migration, render and dispatch. Sage's call. +- **Migration is part of A.** The 25 current rows and the parked table + have to become `queue.json` in the same commit that makes QUEUE.md a + rendered view. Otherwise there are two sources of truth. + +## 2. Files and packages per piece (as the code stands today) + +### A (+ C) + +New: +- `packages/queue/package.json`: same shape as `packages/seat/package.json` + (`type: module`, `engines.node >=24`, `test: node --test tests/`, no deps). +- `packages/queue/src/queue.mjs`: schema validation, transition table, + `next`, `render`, and the journal/lock/verify protocol in 7.1 (R1, R3). + This replaces the earlier "atomic write under a lock file". +- `docs/plans/queue.log.jsonl`: the append-only operation journal (7.1). +- `packages/queue/src/cli.mjs`: `list`, `next`, `add`, `move`, `assign`, + `note`, `render`, plus `render --check` (see 5.9). Exit codes follow seat: + 0 ok, 1 operation failed, 2 invalid data/refused transition, 4 usage. +- `packages/queue/tests/queue.test.mjs` and `tests/fixtures/`. +- `packages/queue/README.md`. +- `docs/plans/queue.json`: the migrated rows. +- `docs/plans/BRIEF-TEMPLATE.md` (C). +- `scripts/test-queue.sh`: see 3.A. Package tests for seat, ledger, + control-board and webui are in no suite script today. Nothing but a + manual `node --test` covers them, so "commit only after suites are green" + does not cover them either. + +Changed: +- `scripts/mosaic`: today it is `exec node packages/seat/src/cli.mjs "$@"` + with no dispatch. It needs `queue) exec node packages/queue/src/cli.mjs` + and a fallthrough to seat. `launch` and `seat task` behavior must stay + byte-identical, because every `agents/*/launch.sh` and + `scripts/test-{darkwing,rocko}-launch.mjs` go through it. +- `docs/plans/QUEUE.md`: marker comments around the Pieces table. The + hand-written header stays above the markers, minus the "Current owner + priority" paragraph, which `after` replaces (Q8). The parked table's items + become `parked` rows (Q9). The log of table changes stays below the + markers, frozen as history (5.12). +- Row 7 (weekly ledger run) leaves the queue (Q9). A's scope excludes + `packages/ledger`, so A leaves a one-line hand-written pointer to the + weekly routine below the markers. E moves the routine into + `packages/ledger/README.md` and removes the pointer. +- `docs/TOOLS.md`: usage lines. This file carries other owners' uncommitted + changes; add a scoped patch the way #1511 did. + +### B + +- `AGENTS.md` lines 112–117 (Cadence) and 190 (pointer). +- `agents/{darkwing,dewey,filbert,rocko,sage}/CONTEXT.md`: each has a line + sending seats to `docs/plans/CURRENT.md` "to reconcile ownership and + existing gates" (darkwing/dewey/filbert/sage line 18, rocko line 15). + Researcher has no such line. The brief says `agents/*/AGENTS.md`, but no + seat has one; the files are `CONTEXT.md`. +- Out of B's scope, noted for Sage: `agents/filbert/SOUL.md` and + `CONTEXT.md` still name Darkwing as team lead. + +### D + +- `packages/queue/src/{queue,cli}.mjs`: `move ID in-review` builds and + posts the request comment, then records the comment id on the row. +- `scripts/gitea-api.sh` is unchanged if the identity comes from + `MOSAIC_GITEA_CREDENTIAL_FILE`, which it already honors (see Q4). +- `packages/queue/README.md`, `docs/TOOLS.md`. + +### E + +- `packages/ledger/src/ledger.mjs` (new `queueChecks`), `src/cli.mjs` + (section above the weekly table, `--json` key), `tests/ledger.test.mjs`, + `README.md` (counting rules for the new section, and the weekly routine + that was row 7). +- One more Gitea call, `state=open`, one page; a full page fails as today + (Q7). Referenced issues absent from that list count as closed. +- Reads `docs/plans/queue.json` through the queue validator by relative + import (`../../queue/src/queue.mjs`). There are no workspaces, so a bare + `@mosaic/queue` import would not resolve (R14). +- Reads registrations with `loadRegistrations` imported relatively from + `packages/control-board/src/scan.mjs`, the way scan.mjs already imports + `../../seat/src/seat.mjs`. It is not moved, so no ownership expands. + Liveness classification is E's own code (R9, 7.1). + +## 3. Tests that prove each piece + +### A +- Schema: every field and type, `id` uniqueness, `id` never reused after + deletion (the CLI never deletes a row; test that no verb can), `blockedReason` + required iff blocked, unknown field refused, owner outside the allowed set + refused, `after` naming a missing id or itself refused, a dependency cycle + refused, `required` a boolean, `state: "required"` refused. +- Transitions: a table-driven test with one fixture per allowed edge and one + per refused edge. Refusals must write nothing (file hash unchanged) and + print exactly one stderr line with exit 2. Includes in-review→in-progress + (allowed), and parking a row with `required: true` (refused). +- Authority: a seat moving another seat's row is refused. `jason` and `sage` + may move any row (Q5). `coordinator` as an owner may move only its own + rows. A missing identity (no `MOSAIC_AGENT_NAME`, no `--by`) is refused. +- `next`: picks in-progress, then in-review, then briefed, each by id, not + file order (shuffle the fixture array); skips rows whose `after` + dependencies are not done (Q8); never returns `parked`; prints `nothing` + with exit 0; the reviewer case (5.11). +- Render: byte-stable (render twice, compare), idempotent over an unchanged + file, `|` and newlines in text escaped. Only the marker region changes, + so a hash of the header and footer bytes is unchanged. +- `render --check`: exits 2 when the table between the markers was edited by + hand. +- Concurrency and recovery: superseded by 7, R1 and R3 acceptance lists + (races through both checkout paths, kill at every step, dead lock holder, + disk errors, hand-edited valid JSON, forks). +- Migration: a fixture of today's QUEUE.md table migrates to a valid + `queue.json`. The rendered table matches a reviewed golden file. +- Dispatch: `scripts/mosaic launch` and `seat task` produce the same + argv/env capture as before (reuse the capture pattern in + `scripts/test-darkwing-launch.mjs`). +- C: `add --brief missing/path.md` refused, `add --brief docs/plans/X.md#Section` + accepted when the file exists (the section is not verified; say so in the + README). +- `scripts/test-queue.sh` runs the above plus `render --check` against the + real `docs/plans/queue.json`, so the committed queue is validated by a suite. + +### B +- `grep -n "CURRENT.md" agents/*/CONTEXT.md` shows no "what now" pointer. + AGENTS.md cadence names the command. +- `agents//launch.sh --check` still passes for every seat, since + context files feed the launch snapshot. + +### D +- A fake Gitea tool, injected the way `readIssues(root, range, tool)` + already allows in ledger. `move in-review` posts exactly one comment, + built from row fields and `--candidate`. A post failure leaves the row + unchanged, exits 1 and prints one stderr line. No file is created under + `docs/plans/reviews/` or `agents/*/work/`. +- Identity: the comment is posted with the credential file the ruling names. + The test asserts that the default `~/secrets/mosaic.gitea.json` is refused. + +### E +- Fixtures: queue.json, an open-issue list and a registrations dir. One + test per rule, both pass and fail, including a multi-row issue (5.10). + A seat with no registration appears on its own labeled line and is not + counted as a violation (Q6). A full page from the `state=open` call + fails the run (Q7). Violations appear above the table + in text and under a `queue` key in `--json`. Zero violations prints a + "queue: 0 violations" line, not nothing, so an absent check is + distinguishable from a clean one. + +### Gate G demonstration + +Setup (Sage or Darkwing, before Jason watches): +1. One real row in `briefed` state, owned by the test seat, with an existing + brief written from BRIEF-TEMPLATE, and no other row that seat owns in + in-progress, in-review or briefed. +2. The seat starts with no prior session: `agents//launch.sh --fresh` + in its tmux pane. The Pi launcher exports `MOSAIC_AGENT_NAME` and writes + the registration, so the board shows the seat. A T3 thread does neither + (Q2). + +Run: Jason sends exactly `run \`scripts/mosaic queue next\` and do it` (Q1). + +Pass evidence: superseded by 7, R5. The earlier version relied on the +ledger's Table 2 count, the git log and "first file read is the brief". +Rocko showed that all three can pass with extra human help or no work. + +## 4. Questions + +### Open, for Jason (Q1–Q4; Sage is holding these until the next-phase target) + +**Q1. The `mosaic` name collides.** On this host, `mosaic` on PATH is +`~/.npm-global/bin/mosaic` 0.0.50-next, the estate CLI. It already has a +`queue` command group ("Manage Mosaic job queues": list, drain, pause, +stats…), so `mosaic queue next` prints `error: unknown command 'next'` and +`mosaic queue list` would run the estate's job-queue listing. The repo CLI +is reachable only as `scripts/mosaic` (its own header says so). +*Recommend:* Gate G's sentence becomes ``run `scripts/mosaic queue next` +and do it``. It needs no PATH change and does not touch `~/.mosaic` or the +npm-global install. Renaming the repo CLI is the alternative, but it is +larger and gains nothing until the estate CLI is retired. + +**Q2. Which runtime runs Gate G?** Development runs in T3 now. T3 threads +set no `MOSAIC_AGENT_NAME`, write no registration, and are not on the board. +This thread confirms it: the variable is absent. `queue next` with no seat +argument therefore cannot know who is asking. +*Recommend:* run Gate G on a Pi launcher (`agents//launch.sh --fresh`), +which sets identity and registers, so Jason can watch from the board as the +brief says. Separately, decide whether T3 seats should export an identity. +Until then, `next` with no seat and no env refuses rather than guessing. + +**Q3. Who commits `queue.json`?** The brief says the file is committed, and +the CLI writes it on every verb. In one shared checkout, an uncommitted +`queue.json` gets swept into whichever seat commits next. That is the exact +failure the brief cites. QUEUE.md is dirty in the tree right now. +*Recommendation changed in round 2 (R2): no automatic commit.* The first +version recommended `git commit -- docs/plans/queue.json docs/plans/QUEUE.md` +after each write. It was withdrawn for three reasons: +- A path-limited commit takes the whole file, so another author's + uncommitted edit to QUEUE.md's hand-written header goes into the queue's + commit. +- An ordinary `git commit -a` from another seat does not honor the queue + lock and can still take the queue files between a write and its commit. +- My claim that "the validator means invariant 8 holds" was false. Schema + validation is not the applicable suite. + +New recommendation: the CLI never commits. The lead commits the queue +files (`queue.json`, `queue.log.jsonl` and the rendered table) at gate +points, by explicit path, after `scripts/test-queue.sh` passes, as QUEUE.md +is committed today. The journal (7.1) keeps every operation between +commits, so no history depends on commit timing. If Jason still wants +automatic commits, R2's preconditions are the minimum: +- the queue paths are clean against the last queue revision; +- nothing is staged on them; +- no merge or rebase is in progress; +- the branch is `refactor`; +- HEAD is unchanged since the read; +- test-queue.sh passes on the exact prospective content. + +Even then, other seats' ordinary commits cannot be prevented. That option +needs Jason's yes. + +**Q4. Whose Gitea identity posts review requests (Piece D)?** +`scripts/gitea-api.sh` defaults to `~/secrets/mosaic.gitea.json`, which +authenticates as `jason.woltje` (checked with `GET user`). Earlier rounds +on #1507 and #1509 established "no Jason-default issue writes". Seat tokens +live under `~/.mosaic/fleet/agents//secrets/`, the fleet tree being +retired, and Jason's jarvis permission covers git, not issue comments by +arbitrary seats. +*Recommend:* D posts only with an explicit per-seat credential file passed +through `MOSAIC_GITEA_CREDENTIAL_FILE`. Where that comes from is Jason's +call (new repo-native seat tokens, or read-only use of the fleet token +files). Until he rules, D refuses to post with the default file. D is +blocked on this; A, B, C and E are not. + +### Decided by Sage as lead, 2026-09-26 (Q5–Q9) + +Recorded from Sage's message to Filbert's T3 thread. Each question keeps +its original reasoning, followed by the decision. + +**Q5. Who may move any row?** The brief says `coordinator` and `jason`. +`coordinator` was the Claude #1509 session. Sage has led since 2026-09-26. +*Decided:* privileged identities are `jason` and `sage`. `coordinator` +stays an owner label, for the #1509 rows, with no override authority. + +**Q6. Piece E's "live registration" check and T3 seats.** Every seat now +working in T3 has no registration, so the check would report every T3 row +as a violation. +*Decided:* "no registration" goes on its own labeled line and is not +counted as a violation. No T3 liveness probe; the T3 guide forbids building +Mosaic features on the T3 tools. +*Amended by Sage per R9 (2026-09-26):* E keeps five states separate: +explicitly unsupported runtime (T3), missing expected registration, invalid +record, stale record, and live matched record. Only T3 gets the labeled +exemption. E prints a coverage line. Zero violations with liveness untested +is not a full pass. Accepting that reduced gate is Jason's call (7.2). + +**Q7. Piece E's dates and fetch rule.** "First run Monday 2026-09-21" has +passed. The ledger's one-call rule (issues updated since the start date, +one page of 50) misses open issues that were not updated in range, such as +#1503. +*Decided:* the first run is the Monday after E is approved. E adds one +more call (`state=open`, one page, a full page fails as today). ~~Referenced +issues absent from that list count as closed.~~ +*Amended by Sage per R8 (2026-09-26):* an issue absent from the open list is +**unknown**, not closed. A done row needs positive closure evidence; +without it, E reports incomplete evidence, never success. `--no-issues` +makes the issue checks unknown, not zero violations. + +**Q8. The owner-priority override.** QUEUE.md's header carries a "Current +owner priority" paragraph that overrides row order. That is state in +prose, which the brief exists to remove. Rows 9–13 also "start when row 6 +is done", a dependency the schema cannot express. +*Decided:* add `after: [ids]` to the schema. `next` skips a row until its +dependencies are done, and orders in-progress, then in-review, then briefed, +each by id. No free-text priority field. + +**Q9. Rows the schema cannot hold.** Row 7 (weekly ledger run) is +recurring: it is never done and must never change. The parked table's five +items have no owner or issue. +*Decided:* row 7 moves to `packages/ledger/README.md` as the weekly +routine (sequenced across A and E; see 2.A). The parked items become rows +with state `parked`, owner `unassigned` and issue null. + +## 5. Where the brief contradicts the code or itself + +Sage decided the fixes in items 3 and 4 (in-review→in-progress, `required` +as a flag), and they are marked **Decided**. Items 1 and 2 are Q1 and Q2. +After round 2 (R11), items 4 (remainder), 6, 7, 10 and 13 change owner +requirements and are in 7.2 for Jason; they are not for A's round. Items +5, 9, 11, 12 and 14 are revised in section 7 and are builder detail. + +1. **`mosaic queue next` resolves to the wrong program** (Q1). Verified on + this host. +2. **Gate G assumes a launcher identity that T3 seats lack** (Q2). Verified: + `MOSAIC_AGENT_NAME` is set only by `scripts/agent.sh`, + `scripts/agent-host-dev.sh`, `agents/rocko/launch.sh` and the discord + engine child. +3. **`required` as a state loses information.** Rule: required→in-progress + only. After that move, nothing records that the row was required. E's + "required row older than 14 days" can then only see rows never started, + and "required rows cannot be parked" is enforced only by the absence of + a path. **Decided (Sage):** `required: true` is a boolean on the row, + alongside the normal state machine, and `required` is no longer a state. + `parked` is refused while the flag is set. Proposed detail: only `jason` + can clear it, following QUEUE.md's existing "only Jason moves" rule for + required rows. +4. **No way back from review.** The listed chain + queued→briefed→in-progress→in-review→waiting-on-jason→done has no + in-review→in-progress edge. Every changes-requested round (#1511 R1→R2, + #1512 R1 provenance) would need `blocked`. It also forces every piece + through `waiting-on-jason`, while rows 14–20 closed without an owner step. + **Decided (Sage):** add in-review→in-progress. Still proposed: + in-review→done where the row's gate is not Jason's, made per-row with a + `gateOwner` field or a `waiting-on-jason` requirement flag. +5. **`blocked→previous` needs storage.** Add `blockedFrom`, set on entry and + cleared on exit. +6. **`queued` is unreachable if `add` requires a brief.** State `queued` + means "no brief yet", but C makes `add` refuse a missing brief. *Fix:* + `--brief` is optional on `add`, and must exist if given. queued→briefed + requires an existing brief. +7. **Parked rows "never change" vs "needs Jason to reopen"** (QUEUE.md's + parked table). *Fix:* parked→queued/briefed with `--by jason` only. +8. **`--by jason` is a claim, not proof.** The CLI cannot check who typed it. + Every agent commits as the same git user. `packages/seat` handles + `taskSetBy` the same way and says so ("authorizes nothing"). Iron-clad + point 5 ("only Jason moves them") is not enforceable by the CLI. *Fix:* + state this in the README, and have E list every change to a required or + parked row, with its `updatedBy`, for Jason to confirm weekly. A real + check needs a signed approval, which is out of this brief's scope. +9. **Rendered view with hand edits.** Nothing stops a hand edit between the + markers, and the next write silently erases it. *Fix:* `render --check` + in `scripts/test-queue.sh` fails on drift. +10. **E's issue rules break on multi-row issues.** "Every open issue with + #N in a row has that row not done": #1509 is open (checked) and rows 14, + 15, 17, 19 and 20 are done. That is five violations on day one, and the + same shape exists for #1503 (rows 1, 22) and #1508 (rows 9–13). *Fix:* + per issue, open ⇔ at least one row not done, and closed ⇒ all rows done. +11. **Owner and issue fields hold one value; today's rows hold several.** + Examples: "dewey; filbert reviews", "#1511 code phase; #1512 pilot". + *Fix:* add `reviewer` (seat or null) and `issues: [int]`. `next` for a + reviewer returns rows in `in-review` where they are the reviewer. That + is how Filbert's review work would reach Filbert through the command. +12. **State cells are paragraphs.** Rows 6, 15, 17 and 23–25 carry 375–775 + characters of history in State; seven more rows exceed 150. The schema's `note` is one line. + *Fix:* migration keeps a short note, and the history stays where it + already is (CURRENT.md, issue comments). The journal is the change log, + and its genesis entry keeps each original row verbatim (7, R13; this + replaces "Q3's per-write commits"). The hand "Log of table changes" is + frozen as history. +13. **Piece D's pass criterion is already trivially met.** Since 2026-09-13, + review receipts have gone to `agents/darkwing/work/*/r*-review.md`, not + `docs/plans/reviews/` (161 entries, last change ea00ec66/11659cf2). The + side channel moved; it did not close. *Fix:* the criterion counts new + review files anywhere, including `agents/*/work/`. +14. **The review request needs a candidate identity the row does not have.** + Current practice pins a frozen snapshot plus a SHA-256 manifest. + *Fix:* `move ID in-review --candidate `, + required, included verbatim in the comment. +15. **Ownership text is stale.** The brief names "Coordinator: the Claude + session" and darkwing as builder. Sage leads now and has confirmed the + brief's split for when the rows open (section 0). + +## 6. Scope held + +No source, record or queue edits were made for this plan, and nothing was +committed or pushed. The only repository writes this session are one line +appended to `docs/SESSIONS.md` (registration) and this file. + +## 7. Disposition of Rocko's adversarial review (round 2) + +Source: `agents/rocko/work/queue-as-data-adversarial-2026-09-26.md`, +SHA-256 `df433f932f6e8687fdb2ad6d707d133b633a245ac39293362ce6bde6942cbf5c`, +verified before reading. It reviewed this plan at `59d5a22f…1fdb`. + +I reproduced Rocko's two probes. `messageKind` classifies the T3 +`[from: sage (…) -> to: …]` header as `human`, and +`import.meta.resolve("@mosaic/seat")` throws `ERR_MODULE_NOT_FOUND`. I also +checked R13's fresh-clone concern against today's rows. Five briefs the +queue points at are untracked in git: relaunch-activity (row 6), +task-attribution (row 6), internal-development-bootstrap (row 16), +discord-board-row (row 18) and board-attention-status (row 22). + +**No finding is rejected.** Ten are accepted as written and four (R1, R3, +R6, R7) are accepted with a modification, each stated below. "Needs Jason" marks a +change to an owner requirement or gate. The items are collected in 7.2. + +### 7.1 Findings + +**R1 (High), two-file write without a crash contract: accept-modified.** +Rocko is right that an atomic rename protects each file, not the pair or +the sequence. +Plan change: +- *Truth and views.* The journal `docs/plans/queue.log.jsonl` is the + record of operations, `queue.json` is the validated snapshot, and the + rendered table is a view. `list` and `next` read only `queue.json`, and + only after verify. No reader ever takes QUEUE.md as current. +- *Revision stamps.* Each snapshot carries `revision` and the SHA-256 of + its journal tail. The rendered table's begin marker carries the same + revision and hash, so anyone can see a stale view. +- *Lock.* The lock is a directory at `$(git rev-parse --git-common-dir)/mosaic-queue.lock`, + realpath-resolved. The checkout and its symlink + `~/src/mosaic-stack-dev-test` therefore share one lock, and nothing new + is added at the repository root. Every verb, including `list`, `next` + and standalone `render`, takes the lock before reading. +- *Stale locks.* The owner file records the PID and its + `/proc//stat` start time. A lock is stale only when that PID is dead, + or alive with a different start time. It is never stale because a + deadline passed. Waits are bounded (10 s), then the verb refuses and + prints the owner. +- *Write order under the lock:* + 1. Verify the current state (R3). + 2. Append the journal entry and fsync. + 3. Write the `queue.json` temp file, fsync, rename, fsync the directory. + 4. Render the table and rename it into place. + 5. Acknowledge on stdout. +- *Recovery on entry, under the lock:* + - Journal tail present, snapshot equal to the tail's parent: roll + forward by replaying the tail. + - Snapshot current, view stale: re-render. + - Anything else: refuse, and print the verify output with a recovery + instruction. +- *Retries.* `add` refuses when an identical non-terminal row exists + (same piece, owner and brief) and prints that row's id. A `move` to the + state the row already holds reports the earlier journal entry and exits + 0. A retry after an unacknowledged crash therefore applies at most once. +- *Durability.* The README states that only acknowledged operations + survive a host crash, and only because of the fsyncs above. +Modification: I use provable stale-owner detection and do not introduce a +kernel `flock`, because Node has none built in, and spawning `flock(1)` +would tie the lock to the bash wrapper and not to the CLI the tests call. +Acceptance: Rocko's list as written (add/add, move/note and render/write +races through both paths; a kill after each step; injected disk errors; a +dead lock holder; a reused PID). + +**R2 (High), Q3's path-limited commit takes other authors' work: accept. +This changes my Q3 recommendation to "no automatic commit"** (see Q3). +Plan change: +- A has no commit step, not even behind a flag. +- The lead commits queue files by explicit path after + `scripts/test-queue.sh` passes on the exact content being committed. +- The invariant-8 claim is withdrawn. The checks that apply to a + queue-data commit are `scripts/test-queue.sh`: `node --test + packages/queue/tests/`, verify, and `render --check`. A commit that also + changes code adds that code's suites. +- Package tests are not exempt because they lack a shell wrapper; + test-queue.sh runs them. +If Jason chooses automatic commits anyway, R2's preconditions and +acceptance list apply as written. + +**R3 (High), checks validate appearance, not provenance: accept-modified.** +Plan change: +- `queue verify` replays the journal from its genesis entry (the reviewed + migration, R13) and requires the replay to equal `queue.json` byte for + byte. Each entry is checked against its parent for authority (R4), legal + transitions, immutable ids and no deletions. +- The high-water id is the maximum id ever issued, and retired ids stay as + tombstones. Row 7 is recorded as retired in genesis. +- Every mutation runs verify first and refuses to overwrite unexplained + drift in `queue.json` or between the markers. Missing or duplicate + markers are refused. +- A fork (two entries with the same parent, for example after a merge or + from a second clone) blocks writes until the lead reconciles it with a + new journal entry. A second-clone writer is out of scope and is detected, + not supported. +- A fresh clone can run verify from committed files alone. +Modification: the honest boundary goes into the README and the #1508 gate +comment in Rocko's words: this is a cooperative writer with drift detection. +Code running as the same host user cannot stop that user rewriting both +the data and the journal. No hooks. +Acceptance: Rocko's list as written. + +**R4 (High), identity attribution used as authorization: accept. Needs +Jason (trust model and the required-row rule).** +Plan change: a permission matrix is added to A. "Claimed" means from +`--by` or `MOSAIC_AGENT_NAME`, recorded in the journal as a claim. + +| Verb or field | Who (claimed) | +|---|---| +| `add` | any identified seat for its own rows; privileged (`jason`, `sage`) for any owner | +| `move` (ordinary) | row owner or privileged | +| `assign`, `reviewers`, `after`, `gateOwner`, `issues` | privileged only; `after` on a required row: `jason` only | +| `required` set | privileged; `required` clear: `jason` only | +| park / unpark | `jason` only, refused while `required` | +| `note` | owner, a reviewer of the row, or privileged; never on a done row | +| done rows | immutable; nothing changes them | + +Also: `assign` cannot be used by a seat on its own row to launder a move. +A non-required row cannot be added to a required row's `after` (so +required work cannot be subordinated, R6). + +"Only Jason moves required rows" (brief point 5) conflicts with the brief's +own "required→in-progress". I read "moves" as reorder or park, which +matches QUEUE.md's header ("cannot be parked or reordered below queued rows; +only Jason moves it"). **Needs Jason:** confirm that reading. + +**Needs Jason:** on this host every seat runs as the same Unix user, and +the default Gitea credential authenticates as jason.woltje. No human gate +is available that the CLI could verify. Recommend: Jason accepts the +cooperative trust model for #1508, with detection through the journal +and E's weekly list of every protected change, as the brief's scope. +Enforcement would need a separate credential or user boundary, which is +outside #1508. No `roles/*.json` change is implied. + +**R5 (High), Gate G can pass with extra help or no work: accept.** +Plan change, replacing the old pass evidence: +- *Pin before the run:* the fresh session file path, the launch snapshot + under `.pi/state//launches/` and its hash, the `queue.json` + revision, the brief's hash, and the start time. +- *Coaching audit:* diff the launch snapshot against committed SOUL, + CONTEXT and USER inputs. Any row- or task-specific text is a fail. + Generic governance context is allowed. +- *Input audit:* after Jason's instruction, **any** further user-role entry + in that session, in any format, fails the gate until Jason's pass or fail. + That covers board replies, agent relays, the T3 header and tmux sends. + The ledger's `messageKind` is not used for the count: it classifies the + T3 header as human and a board-routed correction as board. +- *Claim check:* the journal entry moving the row to in-progress must carry + the pinned launch's `MOSAIC_LAUNCH_INCARNATION`. +- *First work action:* at least one concrete action from the brief's + "What ships" in that session (an edit or a command), not only reads. + Normal prerequisite reads before the brief are allowed. +- *Verdict:* Jason's observed pass or fail stays authoritative. A commit + is not required and not evidence. +- *Checker:* `packages/queue/src/gate-g.mjs`, a read-only evidence + checker, with negative-control fixtures that must fail: a board + correction, a relayed hint, a resumed session, a helper moving the row, + and reads with no work action. +- *Selection* among competing eligible rows is tested in unit tests, not + by the single-row demo. +Follow-up outside #1508, for Sage: `messageKind` also counts the T3 header +as human in the weekly Table 2 number (row 7). That is a ledger bug in its +own right. + +**R6 (High), dependencies and `next` do not stop unauthorized starts: +accept-modified. Needs Jason (the row-9 start condition, only as a brief +wording check).** +Plan change: +- *Predicates at transition time.* `move` checks the `after` and authority + predicates under the lock, the same predicates `next` uses. A seat that + bypasses `next` is refused. +- *Claims.* Moving to in-progress records the claim: seat plus + `MOSAIC_LAUNCH_INCARNATION` when present. in-progress→in-progress is + refused. `next` reports a row claimed by another incarnation as + `resume (claimed by )` and does not return it as fresh work. +- *Action role.* `next` returns an action with the row: `implement`, + `resume`, `review` or `wait`. An author whose row is in-review gets + `wait`. A reviewer gets `review` only when it has no receipt for the + current round (R12). +- *Dependency conditions.* `after` entries carry a condition. The default + is `done`, and `settled` means done or blocked. Row 9 is `after: + [{id: 6, when: "settled"}]`, which matches the brief's "Gate F or + blocked". Sage decided `after`, so this is a refinement of that decision. +- *Ordering.* The in-progress > in-review > briefed ordering (Sage, Q8) + differs from AGENTS.md's "first row you own in any of those states". B + updates the cadence text in the same commit, so the two cannot disagree. +- *Limitation.* `after` cannot rank two rows that are both active. The + README says so, and no priority field is added (Q8). + +**R7 (High), post-then-record cannot promise one review request: +accept-modified.** +Plan change: no exactly-once promise is made, because a remote side effect +cannot be rolled back. D promises at most one request per operation id, +with reconciliation: +- The operation id is formed from row, revision, round and candidate + digest. +- An intent entry is written to the journal and fsynced before the POST. +- The comment embeds ``. +- On an uncertain outcome, D looks up the issue's comments for that marker + before any retry. If the lookup fails, it refuses and prints the recovery + step. No blind retry. +- After Q4, D compares `GET user` against the account expected for the + seat before posting; a different path alone is not proof of identity. +- No token is read or provisioned before Q4. +- Local-commit failure no longer applies (R2). +Acceptance: Rocko's list, with a fake transport that can accept a POST and +then drop its response. + +**R8 (High), absent from the open list does not mean closed: accept (Sage +amended Q7).** Needs Jason (the call budget). +Plan change: +- Absent means unknown. +- Positive evidence comes from `GET issues/N` for each referenced issue + absent from the open list. That is currently 3 (#1504, #1505, #1506). + The response must be an issue, not a PR, and must exist. +- The lookups are bounded at 20, then E reports incomplete. +- A done row without positive closed evidence is reported as incomplete. +- `--no-issues` prints `queue issue checks: not run`. +The per-issue lookups break the ledger's one-call boundary from the #1506 +brief. **Needs Jason:** allow the bounded extra lookups, or accept +"incomplete" as the permanent result for done rows. +Acceptance: Rocko's list as written. + +**R9 (High), the liveness exemption can turn missing evidence green: +accept (Sage amended Q6).** Needs Jason (the reduced gate; Sage is adding +it to his list). +Plan change: +- *Five states:* `exempt-runtime` (T3, declared per run with + `--unsupported-runtime SEAT` and printed), `missing`, `invalid`, `stale` + (dead PID, or a PID alive with a different start time) and `live`. +- *Matching* uses repository root, layout `repo` and seat, never seat name + alone. Duplicates across layouts are reported. +- *Errors.* Scanner errors propagate as `invalid`. +- *Coverage line:* `liveness: N live, N exempt, N missing, N invalid, + N stale`. With any exempt or untested seat, E says "reduced gate". +Acceptance: Rocko's list as written. + +**R10 (Medium), history E needs is not in the schema: accept.** +Plan change: +- Rows gain `createdAt` and `requiredSince` (UTC). +- E ages required rows from `requiredSince` and only while non-terminal, + so `note` no longer resets the age. +- The journal (R1) is the history. E lists every protected change from it, + not from `updatedBy`. +- Violation first-observed evidence is the dated E run posted on #1508. + Remediation is the journal entry's UTC timestamp on the same day. +- None of this depends on commits (R2). Corrections are new journal entries. +- The journal lives in `docs/plans/`, not in BUILD-LOG, SESSIONS or any + runtime log. + +**R11 (High), some section-5 fixes change owner requirements: accept.** +Plan change: +- Section 5's intro no longer says "settle in A's round" for these items. + They are listed in 7.2 with the original rule, the proposed rule, the + authority and the acceptance change. +- *5.6:* I take Rocko's reading. `--brief` stays required on `add` (the + brief's rule is kept). `queued` means the brief exists but is not + accepted, and `briefed` means accepted. That changes QUEUE.md's + definition of queued ("no brief yet"), so it needs Jason. +- *Blocking:* `any→blocked` applies to non-terminal rows only. done and + parked stay immutable, which keeps the brief's "never change". + blocked→blocked is refused; the reason is updated with `note`. This is a + reading, not a change. +- *5.10, issue closure:* rows gain `closesIssue: true` on the row whose + completion gates closure (for example row 22 targets #1503 while row 1 + keeps broader MVP acceptance, so row 1 would carry it). Where rows and + issue state disagree, E reports a mismatch for disposition. It does not + demand closure. Needs Jason. +- *Reviewers:* the single `reviewer` field becomes `reviewers: [seat]` + with per-reviewer receipts, so row 6's Darkwing and Dewey lanes migrate + intact. + +**R12 (Medium), no immutable review candidate or receipt lifecycle: +accept.** +Plan change: +- `--candidate` must be a commit SHA present in the repository, or a + regular, repository-contained manifest file. `/tmp` and other + out-of-repository paths are refused. +- The journal and the comment record the resolved content digest. +- The row gains `reviewIssue` (defaults to the first of `issues`, and is + explicit when there are several) and `review: {round, requests[], + receipts[]}`. Prior rounds are kept. +- Test: request, changes requested, new candidate, approval, with exact + pins in each round. +- The brief's criterion (no new files under `docs/plans/reviews/`) stays + as written. My stronger "no review files anywhere" (5.13) is a separate + proposal for Jason, and it does not replace durable receipts. The + receipts are the issue comments. + +**R13 (Medium), brief validation and migration too shallow: accept. +Needs Jason (row 8 has no brief).** +Plan change: +- *Briefs:* a brief must be a regular file (symlinks refused) whose + realpath is inside the repository and which is tracked by git + (`git ls-files --error-unmatch`). +- *Five current briefs are untracked* (listed above). They must be + committed before migration, or their rows cannot be migrated as + `briefed`. That commit is Sage's to schedule. +- *Anchors:* `path#Heading` is validated. The heading must occur exactly + once in the file. +- *Migration map:* a reviewed row-by-row map preserves owner, reviewer + lanes, gate, required, dependencies, the historical id and every open + boundary. It lives in `agents//work/`. The journal genesis entry + keeps each original QUEUE.md row verbatim (`legacy`), so no history + depends on having been copied elsewhere. +- *Parked items* map explicitly, and their "Where" references become briefs. +- *Row 8* ("none yet") has no brief, which breaks iron-clad point 3. + **Needs Jason:** accept a stub brief written by the lead, or keep row 8 + out of the queue until it has one. + +**R14 (Medium), package reuse and acceptance wiring: accept.** +Plan change: +- Relative imports only (2.E updated). No workspaces and no lockfile change. +- Test commands: `node --test packages/queue/tests/`, + `node --test packages/ledger/tests/`, `scripts/test-queue.sh`, and + `node scripts/test-darkwing-launch.mjs` and + `node scripts/test-rocko-launch.mjs` for the dispatch change. +- *C* acceptance adds two owner-accepted briefs, recorded on #1508. +- *D* acceptance is one full review round (R12). *E* acceptance is a dated + ledger receipt with remediation evidence. +- *B* acceptance: inspect a fresh launch snapshot for the new cadence + text, then Gate G. +- *Order:* C-with-A is the brief's own wording ("as part of Piece A's + review round"). D/E parallel departs from "A to E in order", so section 1 + now follows the brief. **Needs Jason** only if Q4 holds D and E is wanted + first. + +### 7.2 Needs Jason (in addition to Q1–Q4) + +| # | Original rule | Proposed rule | Acceptance change | From | +|---|---|---|---|---| +| J1 | Iron-clad point 5: only Jason moves required rows | Confirm "moves" means reorder or park; seats may start and progress required rows they own | Permission matrix in R4 | R4 | +| J2 | CLI enforces authority | Accept a cooperative trust model with journal detection and E's weekly list; enforcement needs a boundary outside #1508 | README and gate comment state the boundary | R4, R3 | +| J3 | QUEUE.md: queued = "no brief yet" | queued = brief exists, not accepted; `add` always requires a brief | States text in QUEUE.md header | R11 (5.6) | +| J4 | Brief: parked rows never change; QUEUE.md: Jason reopens | Unpark by `jason` only, refused while required | Transition table | R11 (5.7) | +| J5 | Chain passes through waiting-on-jason | in-review→done allowed where `gateOwner` is not Jason | Transition table | R11 (5.4 remainder) | +| J6 | E: open issue ⇒ its rows not done; done ⇒ closed | `closesIssue` row gates closure; disagreement is a mismatch for disposition | E rules and fixtures | R11 (5.10) | +| J7 | D: no new files under docs/plans/reviews/ | Optionally also no review files under agents/*/work/; receipts are issue comments | D pass criterion | R11, R12 (5.13) | +| J8 | Ledger one-call boundary (#1506) | Bounded per-issue lookups (≤20) for positive closure evidence, else "incomplete" | E fetch budget | R8 | +| J9 | Every row has a brief that exists | Row 8: stub brief by the lead, or leave it out of the queue | Migration map | R13 | + +Also held for Jason by Sage: the reduced liveness gate (R9), and E before D +if Q4 holds D (R14). + +Decided by Sage and **not** needing Jason: +- `required` as a flag. It alters the brief's schema text but keeps its + semantics. +- in-review→in-progress. +- Q5–Q9 as amended. +- The `settled` dependency condition, a refinement of Q8 that matches the + brief's own "Gate F or blocked". + +## 8. The active specification (round 6) + +Round 6 answers Rocko's round-5 report, +`agents/rocko/work/queue-as-data-adversarial-r5-2026-09-26.md`, SHA-256 +`3b031a707555960dc69cc274fef6c39c0d8c0ebad539a5e8ae0c0c2545f4177e`, +verified before reading. It reviewed round 5 at `889f2566…ae3d3`, found +F2, F3 and F5 resolved, and raised two medium findings, G1 and G2. 8.19 +maps them. + +Round 5 answered Rocko's round-4 report, +`agents/rocko/work/queue-as-data-adversarial-r4-2026-09-26.md`, SHA-256 +`fcb8933d515bcf98c509d17cb7c5bb384f2bce844b2b87eab6f83e67658efcff`, +verified before reading. It reviewed round 4 at `14dccfd0…e63f`. He +accepted the lead choices and raised five residuals, F1–F5. 8.18 maps each +one. Round 5 also folds in Jason's credential ruling for D (8.9). + +Round 4 answered Rocko's round-3 report, +`agents/rocko/work/queue-as-data-adversarial-r3-2026-09-26.md`, SHA-256 +`13a328045fafc3195305b8ae44f0524620a13d5d06b9e86866c59b3956c6a274`, +verified before reading. It reviewed round 3 at `cfdaa3fe…1ff21f`, plus the +delta to `124b6f9e…0d26`. He accepted the architecture and Sage's rulings, +and raised T1–T8. 8.17 maps each finding to the text that answers it. + +Round 3 answered Rocko's round-2 report, +`agents/rocko/work/queue-as-data-adversarial-r2-2026-09-26.md`, SHA-256 +`b3a2d72ae32775edc835ab6399f99dc870fe17a4833d76b6972a72e1da0e70f4`, +verified before reading. It reviewed this plan at `94922cc5…a27cc5`. + +Sage's direction for this round: this is a queue for one host and a handful +of seats. It fails closed, and recovery happens only through explicit manual +verbs, modelled on the Discord connector's run lock (#1509 reclaim-race +rounds 3–5, `packages/discord/src/journal.mjs`). There is no automatic +recovery. + +**This section is the specification.** Where sections 1–7 differ from it, +this section wins. They remain only as the record of how the design got +here (S11). A builder reads the brief and this section, nothing else. + +### 8.0 What round 3 removes, and one departure from Sage's direction + +Removed: +- The separate journal file. The operation log moves inside `queue.json` + (8.2). +- All automatic recovery: roll-forward, re-render, stale-lock reclaim. +- Worktree and clone support, and fork reconciliation (S6). +- `MOSAIC_LAUNCH_INCARNATION`, and any process-start comparison for + registrations (S8). +- Retries matched by tuple equality (S4). +- The ban on a required row depending on a non-required row (S7). +- The optional stronger D criterion (J7, which Sage dropped). +- The Gate G checker package. Gate G now uses a checklist (8.11). +- The separate generated-table file idea. The brief's markers in QUEUE.md + stay. + +**Departure, stated plainly.** Sage directed `queue repair` for a torn +journal tail (S2). I propose no journal file at all. Operations are +recorded inside `queue.json`, which is written only by temp file, fsync +and rename. +- A file replaced by rename cannot be torn. The partial-append case does + not arise, so there is nothing for a repair verb to handle. +- It is also closer to the brief's "One file, `docs/plans/queue.json` + … It is the queue." + +Sage's other S2 rules carry over unchanged (8.5): +- invalid data refuses every verb; +- acknowledgement means a printed receipt; +- an op that was recorded but never acknowledged stands and is flagged. + +**Decided by Sage 2026-09-26:** accepted. There is no journal file and no +`queue repair`. An invalid `queue.json` refuses every verb. Recovery is a +manual, reviewed procedure (8.5), not a plain `git restore`. + +Round 4 (T1) narrows one round-3 claim. A rename makes a new version +visible, but it isn't durable until the directory fsync succeeds, and it +isn't acknowledged until the receipt prints. 8.5 now keeps those three +points separate. + +### 8.1 Files + +- **A, with C inside it.** + - `packages/queue/`: `package.json`, `src/queue.mjs` (data, checks, + transitions, lock), `src/cli.mjs`, `README.md`, `tests/`. + - `scripts/mosaic` dispatches `queue` to the queue CLI and passes + everything else through unchanged. + - `scripts/test-queue.sh` runs `node --test packages/queue/tests/`, then + `scripts/mosaic queue verify`. + - `scripts/queue-commit.sh` is the lead's commit procedure (8.12). + - `scripts/git-hooks/pre-commit` is the queue guard, which + `queue-commit.sh --install-hook` installs (8.12). + - `docs/plans/queue.json` is created by genesis (8.2). + - `docs/plans/QUEUE.md`: the table between the two markers becomes + generated. The header stays hand-written and loses its priority + paragraph (Q8). + - `docs/plans/BRIEF-TEMPLATE.md` (C). + - The lock's process-identity helpers are imported from + `packages/discord/src/journal.mjs` by relative path: `processStart`, + `bootId`, `pidAlive`, `validStart`, `validBoot`. The control board + already imports that module the same way, and nothing in the discord + package changes. +- **B:** the AGENTS.md cadence line, changed to name `scripts/mosaic queue + next` (8.8). The context files that point at CURRENT.md for what to do next + change as well: line 18 of `agents/{dewey,filbert,darkwing,sage}/CONTEXT.md` + and line 15 of `agents/rocko/CONTEXT.md` ("Read docs/plans/CURRENT.md to + reconcile ownership and existing gates"). Line 30 of + `agents/darkwing/CONTEXT.md` describes what CURRENT.md records, not what + to do next. It stays unless the review of B says otherwise. That makes one + commit, which a fresh launch snapshot verifies (8.14). +- **D:** `packages/queue/src/review.mjs` and the `review` verbs. It posts + only through `scripts/gitea-api.sh` with `MOSAIC_GITEA_CREDENTIAL_FILE`, + set to the per-seat token file Jason ruled on (Q4, 8.9). +- **E:** `packages/ledger/src/queue-checks.mjs` and the ledger CLI hook. It + imports `../../queue/src/queue.mjs` and the control board's registration + loader by relative path. + +### 8.2 Data: one file, one log + +`docs/plans/queue.json` holds `{version, canonicalRoot, revision, rows, +log}`. The brief's "array of rows" becomes the `rows` member. This file is +new, so no reader of the old shape exists. + +**Serialization is deterministic:** +- UTF-8, `\n` line endings, one trailing newline; +- two-space indentation; +- keys in schema order; +- rows sorted by id, and log entries in the order they were appended. + +The CLI refuses any file that does not re-serialize byte for byte, so a +formatting-only hand edit is caught too. + +**Row fields:** + +| Field | Meaning | +|---|---| +| `id` | never reused | +| `piece` | one line | +| `owner` | seat name, `jason`, `coordinator` or `unassigned` | +| `issues` | list of issue numbers | +| `closes` | the issues this row gates, a subset of `issues` | +| `state`, `previousState` | current state; the state `blocked` returns to | +| `required`, `requiredSince` | flag; ISO time or `"unknown"` | +| `gate`, `gateOwner` | the gate sentence and who holds it | +| `brief` | `{path, anchor, blob}` | +| `after` | list of `{id, when: "done" \| "settled"}` | +| `reviewers` | list of seats | +| `review` | `{issue, rounds: [...]}` or null | +| `claim` | `{seat, op}` or null | +| `note`, `blockedReason` | text | +| `createdAt`, `updatedAt`, `updatedBy` | times are ISO; `createdAt` may be `"unknown"` | + +**Log entry:** `{rev, op, verb, args, by, at, semantics, result, viewSha}`. +- `args` is canonical. +- `by` is the claimed actor. +- `semantics` is the rule version this entry was checked under. +- `viewSha` is the SHA-256 of the table body rendered for that revision. + +**Genesis** is `log[0]`, and it is the only entry of its kind. It is created +by `queue genesis --op ID --root PATH --branch NAME --map PATH`, which only a +privileged actor may run. Genesis refuses if any of the following holds: +- `docs/plans/queue.json` exists in the working tree or in HEAD; +- the witness file exists (below); +- `--root` differs from the realpath of the toplevel, found as in 8.3; +- `--branch` differs from the branch HEAD currently names; +- the map is not a committed blob in HEAD. + +Genesis is the only verb that runs before `canonicalRoot` exists, and these +arguments are its reviewed input. It records: +- the QUEUE.md table rows and parked entries verbatim; +- the starting rows as normalized in the reviewed migration map, with that + map's blob id (`agents//work/queue-migration-map.md`); +- row 7 retired, and the id high-water mark; +- `canonicalRoot` and `branch` (8.3). + +Genesis writes through 8.5, so it is durable once the directory fsync +succeeds and it has written the witness. + +**Genesis is committed before anything else happens (F3).** Until HEAD +contains `docs/plans/queue.json`, every mutating verb except genesis +refuses with `genesis not committed`. The exceptions are `sync` and +`render`, which log nothing. The first queue commit is therefore genesis +alone (8.12). + +**Witness (T2).** `/.git/mosaic-queue.head` holds +`{revision, logDigest, fileSha, at}` for the last write whose directory +fsync succeeded. `logDigest` is the SHA-256 of the canonical serialization +of `log[0..revision]`. The witness is a check value, not a second store: it +holds no rows and no operations, and nothing can be rebuilt from it. It is +there so that git replacing `queue.json` with an older valid file is +detected. Replay alone cannot see that (Rocko's Schedule B). A missing +witness refuses (8.5). + +Legacy `createdAt` and `requiredSince` come from cited evidence or are +`"unknown"`. Migration time never stands in for them, and no approval is +invented: legacy done rows carry no receipts. + +**Replay** starts from genesis. It applies each entry under that entry's +`semantics` version, checks the actor and transition against the state +before it, and requires the result to equal `rows`. +- Replay never reads briefs, the roster or registrations. A row's brief + identity is the blob recorded at write time. +- Whether today's briefs still hold is a separate check + (`verify --current`, 8.13). +- There is no rotation or archive in #1508. At about 40 rows and a few + hundred entries a year, the file stays small. + +### 8.3 Canonical checkout only (S6) + +Every verb, reads included, refuses unless all four of these hold: +- The realpath of `git rev-parse --show-toplevel`, run from the process's + working directory, equals `canonicalRoot`. +- Every later git query runs as `git -C rev-parse + --path-format=absolute …`, so paths never resolve against the wrong + working directory (git 2.55 here). After realpath, `--git-dir` and + `--git-common-dir` are the same path. A linked worktree fails this. +- `git symbolic-ref -q HEAD` names the genesis `branch`. A detached HEAD or + another branch refuses. Moving the queue to another branch is a reviewed + migration, like moving the root. +- The CLI's own file lies under `canonicalRoot`. The one exception is + `verify --snapshot` (8.12), which reads only the bytes it is given. + +The symlink `~/src/mosaic-stack-dev-test` resolves to the root and works. A +worktree, or a clone at any other path carrying the same committed +`queue.json`, refuses. The tests cover a linked worktree and a second clone. + +No configured canonical root exists today. `config.json` has only +`configVersion`, `environment`, `dataRoot` and `execution`, and it is +user-authored. So the root is recorded in the reviewed genesis entry, and no +config change is needed. Moving the checkout later needs a reviewed +migration. It is out of scope. + +Only one host is supported. The lock records the host name and the boot id, +and a lock from another host classifies as `unknown` (8.4). + +### 8.4 Lock (S1, T3) + +The lock is the file `/.git/mosaic-queue.lock`. It is not +committed, and it is not a root file. + +**Owner record:** `{pid, start, boot, host, op, verb, at}`. `start` and +`boot` use the same syntax and validation as the Discord connector. If this +process's identity cannot be read from `/proc`, the CLI refuses to take the +lock. + +**Acquire:** +1. Create `/.git/mosaic-queue.lock...tmp` with + `O_CREAT|O_EXCL|O_WRONLY`, mode 0600. + - Write the whole record, looping on short writes and checking the + total. Then fsync and close. + - Read the file back and compare it with the record. Any mismatch or + error removes the temp file and refuses. +2. `link()` the temp file to the lock path. + - EEXIST means the lock is held. That is the same exclusivity as an + `O_EXCL` open. + - Any other `link` error refuses. + - On success, `stat` the temp file and keep its `(dev, ino)`. After a + hard link, that is also the lock's inode. + - Remove the temp file whatever the result. + + Sage accepted `link()` in place of a literal `O_EXCL` open (2026-09-26). + An empty or unparsable lock file, which the CLI can't produce, counts as + `invalid`. +3. If the unlock gate (below) exists, release the lock and refuse. + +**Classification (T3).** The lock record and the gate record are classified +the same way. The tests are taken in this order, and the first one that +applies decides: + +| # | Test | State | +|---|---|---| +| 1 | Record missing, empty or unparsable, or `start`/`boot` fail validation | `invalid` | +| 2 | This process can't read its own `/proc` start or `boot_id` | `unknown` | +| 3 | `host` is not this host | `unknown`, whatever the local pid says | +| 4 | `boot` is not the current boot id | `mismatch`: a previous boot on this host, so the owner can't be running | +| 5 | Same boot, and the pid is not alive | `dead` | +| 6 | Same boot, pid alive, but its `/proc` start is unreadable | `unknown` | +| 7 | Same boot, pid alive, and its start differs | `mismatch`: the pid was reused, and the process holding it now is never signalled | +| 8 | Same boot, pid alive, and its start matches | `live` | + +This order differs from the Discord helper's `ownerState`, which looks at +pid death before anything else and doesn't know about hosts. The queue +imports only the low-level readers (`processStart`, `bootId`, `pidAlive`, +`validStart`, `validBoot`). It does not import `ownerState`. + +**On EEXIST:** retry every 100 ms for up to 10 s, then classify and refuse: + +| Owner state | Result | +|---|---| +| `live` | "held by `` `` since ``; retry the same op later" | +| `dead` or `mismatch` | "run `scripts/mosaic queue unlock` once nothing is running" | +| `unknown` | refuse; `unlock` refuses too; diagnose by hand | +| `invalid` | refuse; inspect by hand | + +Nothing is ever removed because of its age or because of an earlier +inspection. If the host is renamed, locks from before the rename classify as +`unknown`, so the queue stays unavailable until someone diagnoses it. That +is accepted. + +**Release:** the holder `stat`s the lock path and reads the record. It +unlinks only if the `(dev, ino)` and every record field equal what it +acquired. Otherwise it leaves the file alone and reports it. Only `unlock` +removes another process's lock, and `unlock` refuses a live owner. + +**`queue unlock`** is manual, any actor may run it, and it never tries to +take the lock itself. +1. Create the gate `/.git/mosaic-queue.unlock` with the same + temp-and-`link()` steps, using the unlocker's own identity record. + - If the gate exists, refuse, classify the gate record, and print the + result. +2. With the gate held, classify the lock. + - `live`, `unknown` or `invalid`: refuse and remove the gate. + - `dead` or `mismatch`: unlink the lock. +3. Remove the gate, and print the record that was removed. + +Why two actors can't both win: +- A writer publishes its lock, then checks for the gate. +- If that check came before the gate existed, `unlock` classifies later, + sees the writer's published live record, and refuses. +- If the check came after, the writer sees the gate and releases. +- `unlock` never acts on an inspection made before it held the gate, and + two unlockers cannot both hold the gate. + +**A stale gate** from a killed unlocker blocks writers with a message that +names it. `queue unlock --check-gate` is read-only: it classifies the gate +record by the table above and prints the state. A person removes the gate +by hand, only after `--check-gate` says `dead` or `mismatch`, and only +once no queue command is running. `unknown` +and `invalid` gates are left for diagnosis. + +**Who takes the lock:** every mutation, `render`, `verify` and `sync` (8.5). +`list`, `next` and `show` read without the lock. A rename replaces each +file whole, so each read sees one complete revision. Two files read one +after the other are not an atomic pair, though (F2). Unlocked reads +therefore follow this rule: +1. **Order.** Read the witness first, then `queue.json`. The writer + publishes `queue.json` before the witness (8.5 steps 8 and 10). So a + file read after the witness is never older than it unless history really + was lost. A normal write in progress cannot make an unlocked reader see + the file behind the witness. +2. **Ahead.** A file ahead of the witness prints `rev N visible, not + confirmed durable`. An unlocked reader can't tell a writer that is still + publishing from a crash leftover, so it calls it neither. +3. **Adverse.** Any other result takes the lock and repeats 8.5 steps 1–2 + before it reports anything. That covers an invalid file, a file that + doesn't extend the witness, and a missing witness. This includes + `accept-history`, which writes the file before the witness. If the lock + can't be taken, the read refuses as 8.4 does, naming the holder. An + unlocked read never reports lost history on its own, and never suggests + `accept-history`. + +Only a locked caller reports an unconfirmed tail as something to fix, +because under the lock no writer can still be publishing. Nothing holds the +lock across network I/O (8.9) or across git (8.12). + +**Tests:** +- a kill between the temp write and the link: no lock appears; +- a short or failed temp write: refused, no lock; +- a `link` error other than EEXIST: refused; +- a paused holder: others wait 10 s, then refuse `live`; +- two concurrent unlockers: one refuses on the gate; +- a writer publishing during an unlock, in both orders; +- a reused pid within one boot: `mismatch`, and the process is never + signalled; +- the same pid and start on a different boot: `mismatch`; +- a foreign host with no such local pid: `unknown`, and unlock refuses; +- unreadable `/proc`: `unknown`; +- a stale gate whose pid is now reused: `--check-gate` says `mismatch`; +- a delayed release by a dead owner, after unlock and a new owner: the + inode check keeps the new lock. + +### 8.5 Write path, durability, acknowledgement and manual recovery (S2, S3, T1, T2) + +**Supported platform.** Linux, with `docs/plans/` and `.git/` on a local +ext4, xfs or btrfs filesystem (tmpfs is allowed for tests). The CLI checks +`fs.statfsSync(...).type` and refuses anything else. This checkout is ext4. +Temp files are created in the same directory as their target, so a rename +never crosses filesystems. It is assumed that rename replaces the target +atomically on these filesystems, and that fsync of a directory fd persists +the renamed entry. + +**Three points, kept separate (T1):** + +| Point | When | Meaning | +|---|---|---| +| visible | the rename returns | readers see the new revision | +| durable | the directory fsync succeeds and the witness is written | the revision survives a host crash, within the platform assumptions | +| acknowledged | the receipt prints | the caller may treat the op as done | + +**A mutation runs these steps under the lock:** +1. **Checks.** Run the canonical checks (8.3). Read `queue.json`, and keep + its bytes and its `stat` result (`dev, ino, size, mtime_ns`). The file + must parse, match the schema, re-serialize byte for byte and replay to + `rows`. `log[0]` must be the only genesis entry, op ids must be unique + and `revision` must equal the last `rev`. Any failure refuses. +2. **Witness.** Compare the file with the witness: + - It matches (same revision and `logDigest`): continue. + - It extends the witness, meaning the prefix up to the witness revision + has the same `logDigest` and later entries follow: this is an + unconfirmed tail. Some earlier op became visible but was never + confirmed durable. + - If this call is a retry of an op in that tail, or is `queue sync`: + fsync `queue.json`, fsync `docs/plans/`, write the witness, then + continue. + - Otherwise refuse, and name the tail ops and `queue sync`. + - It does not extend the witness (a lower revision, or a different + prefix): history was lost or replaced. Refuse every verb except + `accept-history` (below), and name the witness revision. + - No witness: refuse, and name `queue accept-history`. The one exception + is a file holding only genesis, which `sync` accepts. +3. **Lookup (8.6).** If the op id is in the log, print its recorded receipt + and stop. + - This happens before any view check, so a retry is answered even while + the table is stale, with a one-line warning (T4). + - Reads and lookups never fsync. A lookup reaches this step only after + step 2 has confirmed durability. +4. **View.** Check that the QUEUE.md table is current. If it is stale or + unknown, refuse the new op (see "Outcomes"). +5. **Compute.** Compute the new rows and the log entry, and validate both. +6. **Temp file.** Create `queue.json..tmp` with `O_EXCL`, write all + bytes (looping on short writes and checking the total), fsync, and + close. On any failure, unlink the temp file and refuse. Nothing has + changed. +7. **Unchanged check.** `stat` and hash `queue.json` again. They must equal + step 1's values. If they don't, something wrote outside the lock, git + most likely: unlink the temp file and refuse. A window remains between + this check and the rename. A lock that git ignores cannot close it, and + the README says so. +8. **Rename.** Rename the temp file over `queue.json`. **The op is now + visible.** A rename failure unlinks the temp file and refuses. +9. **Directory fsync.** fsync `docs/plans/`. **The op is now durable.** If + the fsync fails: + - print `uncertain rev N: visible, durability not confirmed + ()` and exit 3; + - print no receipt, and write neither the witness nor the view; + - never write an older version back. + + The next call finds the unconfirmed tail at step 2. +10. **Witness.** Write the witness: temp file, fsync, rename, directory + fsync. If that fails, print `uncertain rev N: durable, witness not + updated`, exit 3, and print no receipt. +11. **View.** Render the table body. Write QUEUE.md by temp file, fsync, + rename and directory fsync, replacing only the bytes between the + markers. + - Everything outside the markers is copied from the bytes read at step + 4. + - If QUEUE.md has changed since step 4, refuse the view write. The op + stays recorded, and the view is stale. + - The same check-to-rename window as step 7 applies, and is stated. +12. **Receipt.** Release the lock and print `ok rev N row R + →`. **Only now is the op acknowledged.** + +**Recorded but not acknowledged:** +- **Killed between step 8 and step 10.** The next call finds an unconfirmed + tail and refuses new ops until `queue sync` runs or the op is retried. + `sync` prints `durable now, never acknowledged: by at