Files
stack/agents/filbert/work/queue-as-data-plan-2026-09-26.md
T
jason.woltjeandClaude Opus 5.5 f539466fcb feat(queue): Piece D, reviews as issue comments, raw per-seat token helper (row 12, #1508)
queue move ID in-review posts the review request as a Gitea comment and
review record reads verdicts back, so reviews stop being files in
docs/plans/reviews/. On a comment round, in-review to waiting-on-jason
now needs every listed reviewer's approval for the current round, the
same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads
the raw per-seat token files (lead decisions 37 to 39): config built and
checked before curl starts, export attribute cleared, fixed base URL.
test-queue.sh skips its live checks outside the canonical root.

Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302)
and corrected the plan (293747cd). Rocko reviewed the helper (e896192f,
2096b0a3), and Sage's lead check passed under decision 38. Manifest
b402fb38, 19 files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 10:07:29 -05:00

128 KiB
Raw Blame History

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/<seat>/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/<seat>/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/<seat>/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/<seat>/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 <commit-or-manifest-path>, 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/<pid>/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/<seat>/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 <incarnation>) 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 <!-- mosaic-queue-op: <id> -->.
  • 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/<author>/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/<author>/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). <canonicalRoot>/.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 <toplevel> 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 <canonicalRoot>/.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 <canonicalRoot>/.git/mosaic-queue.lock.<pid>.<random>.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 <verb> <op> since <at>; 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 stats 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 <canonicalRoot>/.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.<op>.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 <op> rev N: visible, durability not confirmed (<errno>) 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 <op> 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 <op> rev N row R <from>→<to>. 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: <op> by <seat> at <time> for each tail op.
  • Killed between step 10 and step 12. The op is durable, and the stale view flags it: "rev N (op X by S at T) is recorded but the table shows rev N−1; it may never have been acknowledged. Tell S, then run queue render."
  • Killed after the view but before the receipt. Nothing flags it. The caller learns the outcome by retrying the same op id or by running queue show.
  • Unlocked reads. They follow the order and recheck rule in 8.4. A read that sees a revision past the witness prints rev N visible, not confirmed durable.
  • D. Its network step starts only after its own intent reached step 10 (8.9).

Outcomes:

Condition Effect Fix
All checks pass proceed —
queue.json invalid every verb refuses, reads included the manual recovery below
unconfirmed tail new ops refuse; retries of tail ops and sync confirm durability first queue sync, or retry the op
history lost (the file doesn't extend the witness) every verb refuses except accept-history the manual recovery below
view stale (the body matches the viewSha of an earlier log entry, a genuine older render) new ops and verify refuse and name the unshown ops; retries answer; reads warn and work a person looks, then runs queue render
view unknown (the body matches no logged render; markers missing, duplicated or out of order) every verb that writes QUEUE.md refuses, render included a person restores the table with git, or re-applies the edit as CLI ops, then runs render

Manual recovery for an invalid file or lost history (T2). No verb repairs anything. The steps, in order:

  1. Copy the current queue.json bytes to agents/<lead>/work/queue-recovery-<date>/ before touching anything. Restoring HEAD would discard every op written since the last queue commit.

  2. Find the lost ops. They are the entries after the last committed revision, taken from:

    • the preserved bytes;
    • git stash list and the reflog;
    • the receipts that seats hold;
    • issue comments carrying mosaic-queue-op markers.
  3. Account for external effects. For every lost review request, check whether its comment exists. A lost op id is no longer deduplicated, and reusing it could post again.

  4. Put a valid file in place: the restored version with the lost ops re-applied under new op ids, or a reviewed hand-built file.

  5. Run queue accept-history --op ID --reason TEXT.

    • Only a privileged actor may run it.
    • It appends an entry recording the old witness (revision and logDigest, or "absent"), the new revision and the reason, then rewrites the witness.
    • It is the only verb allowed while history is lost, or while the witness is missing.
    • Before running, it prints the lost revision range and the warning "ops in that range are no longer deduplicated", and it needs --yes.

    A reset accepted this way does not keep the at-most-once guarantee for the lost range. The log entry says so.

The git side of the protocol (T2). This part is cooperative, stated in the README and in B's cadence text. While the working queue.json has ops that are not yet committed, or any review attempt is unresolved (8.9), no one runs checkout, stash, restore, reset or a branch switch that touches docs/plans/queue.json or QUEUE.md. Only the lead does such an exceptional restore, and only by the recovery steps above.

Commits follow the same cooperative rule (F1):

  • Seats commit with plain git commit and never --no-verify, so the queue guard runs (8.12).
  • Merge, rebase, cherry-pick, revert and am skip that guard. On the queue branch they are the lead's.
  • Nobody disables, replaces or overrides the guard. That means no core.hooksPath in any scope and no -c core.hooksPath= on a command. It also means no environment that changes which config git reads (GIT_CONFIG_*), no edits to .git/hooks/pre-commit, and no change to its mode (G2).
  • "Lead's" and "nobody" are protocol, not enforcement. The witness detects lost history afterwards; it can't prevent a same-user git write. That trust limit is accepted.

Step 7 catches ordinary interference. The witness catches rollback after the fact. Neither can stop an uncooperative git command, because git doesn't honour the lock. That limit comes with the trust model, and nothing here pretends otherwise. A passing replay shows the file is consistent with itself. It does not show that no history was lost. The witness is the only thing that speaks to that.

queue render takes the lock, runs steps 1–2, and rewrites the body only when the view is stale. When the view is current it does nothing. It never overwrites an unknown view.

queue sync --op ID takes the lock, runs steps 1–2 (which confirm any unconfirmed tail), prints what became durable, and logs nothing.

render --check and verify never write, and neither do list, next or show. Hand edits stay refused.

Tests. File operations go through an injectable layer, so each fault can be injected separately:

  • a short write, ENOSPC, and a file fsync failure: nothing visible, temp file removed;
  • a rename failure;
  • a directory fsync failure: uncertain, exit 3, no receipt, witness and view untouched, and a later retry or sync confirms it;
  • a witness write failure;
  • a kill at each step boundary, using a child killed with SIGKILL:
    • before the rename, nothing is recorded;
    • after the rename, before the witness: the tail refuses new ops, and sync names the op;
    • after the witness, before the view: the stale refusal names the op;
    • after the view, before the receipt: a retry returns the receipt;
  • a same-op retry while the view is stale: it answers;
  • git interference:
    • git checkout -- docs/plans/queue.json between steps 1 and 7: step 7 refuses;
    • git stash of a valid newer pair, restoring an older valid pair: history lost, every verb refuses, and accept-history works only with --yes and a reason;
    • a deleted witness: refuses, after the locked recheck;
  • unlocked reads racing a writer (F2):
    • a reader paused between the witness read and the file read while a writer completes steps 8–10: no lost-history report;
    • a test hook forcing the file-then-witness order: the locked recheck prevents a false report;
    • a writer paused before and after the witness rename;
    • a true rollback, reported only after the locked recheck;
    • an accept-history in progress;
  • a hand edit to queue.json that is still valid JSON: replay mismatch;
  • a formatting-only edit;
  • a genuine stale view compared with an edited view that carries the same old marker;
  • a current marker over a changed body;
  • missing or duplicate markers;
  • a header edit during a write;
  • verify and render --check leave bytes and mtimes unchanged.

SIGKILL tests exercise process death only. Power loss is not tested. The host-crash claims rest on the platform assumptions above, and the README says so.

8.6 Operation ids (S4)

Every mutating verb requires --op ID, matching ^[a-z0-9][a-z0-9._-]{7,71}$, so at most 72 characters. Every op id in the log matches ^[a-z0-9][a-z0-9._-]{7,79}$. The 8 characters in between leave room for REQOP.outcome (8.9, F5). The CLI never makes one up. The caller chooses the id before the first attempt and passes the same id on every retry, for example --op dewey-row9-start-2026-09-26. Because the id is written into the caller's own command, a killed CLI cannot lose it.

Op id in the log? Result
No a new operation, checked against the current state
Yes, same verb and canonical arguments no change; print the recorded receipt with "already recorded at rev N", exit 0, whatever has happened since
Yes, different verb or arguments refuse, exit 2

Identity is the op id alone. No state or tuple is compared. The lookup runs at step 3 of 8.5, after the durability checks and before any view check. A retry is therefore answered even while the table is stale (T4). Op ids ending in .outcome are reserved for the outcome entries that D writes itself (8.9), and callers may not use them. Tests:

  • a 72-character request op records its 80-character outcome;
  • a 73-character caller op is refused;
  • a caller op ending in .outcome is refused.
  • A new operation asking for in-progress→in-progress is refused as an illegal transition. R1's "moving to the current state succeeds" is withdrawn.
  • A retried add returns the id it first allocated, even after the row was reassigned or finished.

Tests:

  • Rocko's S4 schedule: a lost result, then another writer changes the row, then the retry returns the recorded receipt and opens no second round;
  • an add retried after reassignment and after done;
  • a reused id with a different payload;
  • a missing --op.

8.7 States, transitions, permissions: one matrix (S7, R4)

The states are the brief's. required is a flag (Sage). queued means a brief exists but has not been accepted, and briefed means it has been accepted (J3, Sage). Every add needs a brief, and it creates a queued row.

What add may set (T8).

  • An ordinary seat adds only rows it owns, and owner is itself. It may set piece, brief (required, 8.13), issues and note.
  • Defaults for everything else:
    • closes = issues;
    • gateOwner = jason, so the brief's route through waiting-on-jason applies unless a privileged actor changes it;
    • after = [];
    • reviewers = [];
    • required = false.
  • A privileged actor may also set owner, gateOwner, after, reviewers and required on add.

"Privileged" means jason or sage (Q5). Actors are claimed through --by or MOSAIC_AGENT_NAME, and a verb with neither is refused. This is the cooperative trust model (J2).

Transition Who Condition
queued→briefed privileged —
briefed→in-progress owner every after entry satisfied; records claim
in-progress→briefed (release) claimant, or privileged clears claim
in-progress→in-review claimant once D is built, for a row with reviewers, this move is the review request, and it takes --candidate (8.9). Before D, or with no reviewers, it opens the round with request: none
in-review→in-progress owner, or privileged changes requested (Sage)
in-review→waiting-on-jason owner, or privileged with D built, on a comment round: refused while any attempt is unresolved, and it needs the approving receipts of every listed reviewer for that round, the same checks as in-review→done (8.9). Before D, or on a round with request: none: no condition
waiting-on-jason→done jason; or sage with --evidence citing Jason's approval the evidence reference is logged
in-review→done the row's gateOwner, or privileged refused when gateOwner is jason (that path runs through waiting-on-jason). --evidence must name the current round and its candidate digest: with D built, the approving receipts of every listed reviewer for that round (8.9); before D, a comment id together with the round's candidate digest, which must match. Logged (J5)
any non-terminal→blocked owner, or privileged reason required; records previousState; blocked→blocked refused (update the reason with note)
blocked→previousState owner, or privileged —
queued or briefed→parked jason refused while required
parked→queued (unpark) jason the brief is re-accepted through queued→briefed (J4)
done→anything refused —

Field edits, on add as well as afterwards:

Field Who
owner privileged. An ordinary seat may add only rows it owns.
gateOwner, after, reviewers privileged
issues, brief set on add by the adder; afterwards privileged only (a brief change is a re-pin, 8.13)
closes set to issues at genesis and on add; privileged may narrow it only with --reason, which is logged (J6)
setting required privileged; refused on a parked row
clearing required; changing after on a required row jason
note owner, a listed reviewer, or privileged; never on a done or parked row
review receipt a listed reviewer, for the current round, citing that round's candidate (8.9)

Claim lifecycle (T8). Invariant: claim is null, or claim.seat equals owner.

Event claim
briefed→in-progress set to {seat: owner, op}
in-progress→in-review, in-review→in-progress, in-review→waiting-on-jason kept
→blocked, and blocked→previousState kept unchanged
release (in-progress→briefed) cleared
assign by a privileged actor on a claimed row owner and claim.seat both become the new seat, and claim.op becomes the assign op. The review round continues
→done, by any allowed actor cleared
parked, unparked never claimed: parking happens only from queued or briefed

Permission comes from this matrix alone. A claim adds no refusal of its own, so a gate owner who isn't the row's owner can complete it through the J5 edge. "Claimed by X" is only the wording of the refusal a non-owner gets when they attempt an owner transition.

Prerequisites. after holds owner-approved prerequisites of any shape, including a required row depending on a non-required one. Row 9 is after: [{id: 6, when: "settled"}], where settled means done or blocked. Prerequisites are checked only on the move into in-progress. Later transitions do not recheck them, so a parent that leaves blocked afterwards strands nothing.

Rulings in this matrix (Sage, 2026-09-26, 8.15):

  • J1: owners progress their required rows. Reordering, parking and clearing required are Jason-only.
  • J4: only jason unparks, and a parked row returns to queued.
  • J5: in-review→done is allowed where gateOwner is not Jason.

8.8 next and claims (S8, R6)

A claim is the seat name and nothing more: claim: {seat, op}. No incarnation and no process identity are recorded. The claim lifecycle is in 8.7.

  • If another seat attempts an owner transition, the CLI refuses with "claimed by X". X runs release, or a privileged actor reassigns the row.
  • Two sessions of the same seat are one claimant. The queue cannot tell them apart and does not try. This limit is stated in the README.

next [SEAT]. SEAT defaults to MOSAIC_AGENT_NAME. With neither, the verb refuses (Q2). It returns one action, lowest id first within each class, in Q8's order:

  1. resume: an in-progress row claimed by SEAT.
  2. review: an in-review row where SEAT is a listed reviewer and has no receipt for the current round.
  3. start: a briefed row owned by SEAT whose after is satisfied. If the working brief no longer matches the pinned blob (8.13), next still names the row, marked brief differs from pinned blob; ask the lead to re-pin, and start refuses.
  4. wait: only when nothing above matches and SEAT owns an in-review row.
  5. nothing.

The author's in-review rows are not actionable, so they give wait only when there is nothing else. That is how I apply Q8 to the author's side. queued, blocked, parked and done rows are never returned. B's cadence text is written to this list.

8.9 Review requests, Piece D (S9, S10, R7, R12, T4)

D posts only with an explicit per-seat credential file (MOSAIC_GITEA_CREDENTIAL_FILE) and refuses the default file (Q4).

Credentials (Jason, 2026-09-26, relayed by Sage). The live round reads each seat's own token in place, read-only, at ~/.mosaic/fleet/agents/<seat>/secrets/gitea-mosaicstack-<seat>.token (mode 0600). There are no copies and no writes under ~/.mosaic. The lead posts as jarvis under the same rule, reading ~/.mosaic/fleet/agents/jarvis/secrets/gitea-mosaicstack-jarvis.token in place (Sage, 2026-09-26). That is the Gitea API token. The push helper has its own route, and this plan doesn't change it.

  • D never opens the file. gitea-api.sh reads it.
  • D only stats it: it must be a regular file, owned by the user, mode 0600. The path must name the acting identity: the seat, or jarvis for the lead.
  • Scopes as ruled: darkwing and dewey hold write:repository, and filbert and rocko hold write:issue.
  • It is unverified whether write:repository alone lets a token post an issue comment. If it doesn't, the POST gets a 403, which is failed: a definite result with nothing posted. So it fails safe. Nobody spends a real post to check this beforehand (Sage, 2026-09-26). A 403 for darkwing or dewey in the first live round is an expected finding, not a defect in D. It is recorded with the round, and the scope question goes to Sage.

The tests use a fake transport and read no token.

Verbs:

  • move ID in-review --op OP --candidate <commit|manifest>. Per the brief, this move posts the request. It opens round n and its first attempt.
  • review request ID --op OP: another attempt in the current round, with the round's candidate. It is allowed only when every earlier attempt in the round is failed or abandoned.
  • review resolve ID --op OP --attempt REQOP --posted <commentId>: the owner or a privileged actor.
  • review abandon ID --op OP --attempt REQOP --reason TEXT --yes: privileged only.
  • review record ID --op OP --verdict approve|changes --comment <id>.
  • review verify-commit ID <commit|tree>, which is read-only.

Candidate. A candidate is one of:

  • a commit reachable from a local branch or tag;
  • a manifest listing repository paths and their SHA-256, in the form of today's CANDIDATE.sha256.

The candidate is frozen for the round. A new candidate needs a new round: in-review→in-progress, then move in-review again.

The manifest text goes into the comment and into the log. The queue does not keep the source bytes of an uncommitted candidate. verify-commit checks that every manifest path has the approved digest in the given commit or prospective tree (8.12). If the approved bytes have changed or are gone, integration is blocked, and the fix is a new round. A missing candidate is never quietly replaced by a new one. A commit candidate stays retrievable only while some ref keeps it, and the README says so.

Attempts. Each attempt is keyed by its request op and lives in round.attempts[]. The candidate digest and the op never change after the intent is written.

State Meaning New request on this row?
requesting intent is durable; no outcome recorded yet refused
posted a comment id was confirmed, by the transport or by resolve not needed
failed definite non-acceptance (below) allowed, with a new op
uncertain the transport outcome is unknown refused
abandoned a privileged actor gave up on it, accepting the risk of a duplicate allowed, with a new op
conflict a late transport outcome disagrees with an earlier manual resolution refused

Request steps:

  1. Intent. Run the full 8.5 path under the lock. The entry, with op REQOP, moves the row or opens the attempt, recording the candidate digest, issue, round and requesting.

    • If any attempt on the row is requesting, uncertain or conflict, refuse.
    • If the write ends uncertain (exit 3), stop: nothing is sent. Only a write that reached 8.5 step 10 goes on to the network.
  2. Pre-send checks, without the lock:

    • the credential file is set, is not the default, and passes the stat checks above;
    • GET user matches the expected login: the seat's own for a seat, and jarvis when the lead posts. The queue actor stays the lead; only the login is jarvis. It runs in its own process group under the same 30 s deadline as the POST, and a timeout here is a pre-send failure.

    A failure here means the POST was never started, so the outcome is failed.

  3. POST. Run gitea-api.sh in its own process group. After 30 s, kill the whole group with SIGKILL. The helper has no timeout of its own, and killing its curl child does not undo a request the server has already received. The body carries <!-- mosaic-queue-op: REQOP -->, the round and the manifest. The helper prints the body on stdout and HTTP <code> on stderr, and it keeps the token off argv, stdout and stderr. D records only the status and the comment id.

    • HTTP 201 with a parseable comment id: posted.
    • HTTP 400, 401, 403, 404 or 422, meaning the endpoint answered and refused: failed.
    • Anything else is uncertain:
      • the helper's "request failed", because curl's exit does not say whether the body was sent;
      • a kill at the deadline;
      • a 5xx or any other code;
      • a 2xx without a comment id.
  4. Outcome. Take the lock and run the 8.5 path. Append the entry REQOP.outcome, referencing the attempt.

    • If the attempt is still requesting, it takes the new state.
    • If a person already resolved it:
      • agreement (resolved posted X, transport posted X) is recorded as confirmation;
      • disagreement (for example, abandoned while the transport reports 201) sets conflict and records both.
    • If the lock can't be taken, print uncertain REQOP: transport said <x>, not recorded, and exit 3. The attempt stays requesting.

Nothing is resent automatically. Replay, verify, list, next, a retry of the same op and a new op all leave an unresolved attempt alone. The CLI may print the issue URL and the marker as a hint. It never decides.

Resolution by a person:

  • --posted <id>. The CLI makes one GET for that comment, under the same 30 s process-group deadline. It checks the comment belongs to the row's issue, carries the marker for REQOP, and names the round's candidate digest. If everything matches, the attempt becomes posted. If anything differs, or the GET fails, the command refuses and the attempt is unchanged.
  • There is no not-posted. A missing comment doesn't prove that a delayed request will never arrive. The only definite failures are the CLI's own pre-send failures and the listed 4xx codes.
  • abandon. Privileged only, with a reason and --yes. It marks the attempt abandoned and the round duplicateRisk: true. For that round, the report makes no at-most-one claim.
  • Terminal states. Resolving an attempt that is posted, failed or abandoned refuses. A conflict accepts one more resolution, by either verb.

Receipts. review record is accepted from a listed reviewer, for the current round, citing the comment id and the candidate digest that was approved. A mismatch refuses. Every round is kept in review.rounds[]. On a comment round, the receipts gate both moves out of in-review toward done: in-review→done, and in-review→waiting-on-jason, which is the route for a Jason-gated row. Each needs an approval from every listed reviewer and no unresolved attempt.

Tests use a fake transport:

  • a kill:
    • before the POST;
    • after the server accepted it, before the outcome write;
    • while reacquiring the lock;
  • posted; each listed 4xx; a 5xx; "request failed"; a timeout kill; a 201 without an id;
  • a new-op request while an attempt is requesting or uncertain: refused;
  • a same-op retry after a stale view: it answers, and nothing is sent;
  • a late POST after abandon: conflict;
  • a late outcome after resolve --posted with the same id and with a different id;
  • resolve --posted with the wrong issue, marker, round or candidate;
  • a pre-send GET user mismatch, and a GET user timeout;
  • the lead: a fake-transport success with login jarvis and actor lead, and a refusal when the login is anything else, sage included;
  • the credential stat checks: a wrong mode, another seat's path, the default file;
  • request → changes → new candidate → approval, with exact pins at every round.

In no path does the fake transport see a second POST for one attempt.

8.10 Ledger queue checks, Piece E (R8, R9, S8, S10, J8)

The checks come from the brief:

  • a row naming an open issue is not done;
  • the owner of an in-progress or in-review row has a registration;
  • the issues a done row closes are closed;
  • a required row older than 14 days is listed.

How I read the one-call restriction. The rule is in the #1506 Piece 3 brief: 2026-09-12_control-board-mvp.md, Boundaries, "no network beyond the one Gitea call". It bounds the ledger's metric sources, and the ledger README calls the full-page refusal "the cost of the brief's one-call boundary". Piece E, which Jason approved under #1508, asks about "every open Gitea issue" the queue references. The metric call cannot answer that, because it returns issues updated in range, and an open issue nobody touched that week is absent. So E cannot exist inside one call. I read Jason's approval of E as adding a separate, bounded queue-check budget. The metric source is unchanged.

Budget, per run:

Calls Purpose
1 Metrics, unchanged.
1 Queue checks: state=open, limit 50. A full page makes the queue issue checks "incomplete".
up to 10 Queue checks: GET issues/N for issues in some row's closes that are neither in the open list nor shown closed in the metric page. Issues beyond 10 are "unknown (budget)", and the run is incomplete.

That is at most 12 calls. --no-issues makes 0 calls and prints queue issue checks: not run.

  • An issue absent from the open list is unknown. Closed needs positive evidence: a closed entry in the metric page, or a GET that returns a closed issue that is not a PR.
  • Today the extra calls would be at most 3 (#1504, #1505, #1506; section 7, R8).

Closure (J6, decided by Sage). closes equals issues at genesis and on add, which is the brief's literal rule. An issue is expected closed once every row whose closes includes it is done. The lead may narrow closes only with a logged reason (8.7), for example so that row 22 does not close #1503 while row 1 remains. Other disagreements print for disposition.

Liveness (S8, R9, T7). A registration carries a pid and startedAt, which is the registration time. It holds no process-start identity, so nothing is ever reported as "live" or "verified".

Class Meaning
exempt declared on this run with --unsupported-runtime SEAT (T3); printed
missing no registration for (canonical root, layout repo, seat)
invalid fails validation, or the scan errors
pid-unknown the registration is valid but its pid is null, missing or malformed
pid-gone the recorded pid is not alive
pid-present the recorded pid is alive, reported as "pid present (identity not verified)"; a reused pid looks the same

The coverage line reads: liveness: N pid-present (unverified), N exempt, N pid-unknown, N missing, N invalid, N pid-gone.

Age. A row's age runs from requiredSince. A legacy row marked "unknown" was required no later than genesis. So once genesis is more than 14 days old, the row is overdue for certain, and it is listed as "age ≥ N days (legacy lower bound)". Until then its age is undecidable.

Result (T7). E prints one of three results, and never a full pass:

  • fail: any known violation that has not been remediated. That covers a missing, invalid or pid-gone owner, and any issue or age violation.
  • incomplete: no known violation, but something couldn't be decided. That covers:
    • an issue check that was unknown (budget), hit a full page, or was not run;
    • a pid-unknown owner;
    • an undecidable legacy age.
  • reduced pass: nothing known and nothing undecided. The liveness evidence is pid presence only, and it may include exempt seats.

A verified full pass would need process identity in registrations. That is out of scope for #1508, as Sage ruled that incarnation is dropped. Sage's acceptance of the reduced gate means a reduced pass meets E. It says nothing about verified liveness.

Remediation (S10). A violation's identity is (check, row, issue). "Moved within the day" means a second dated E run on the same UTC day no longer reports that identity, and both runs are posted on #1508. A row edit alone is not remediation. A note can never clear a registration finding, because that check reads registrations, not rows.

8.11 Gate G (S8, R5, T6)

A Pi seat is launched with --fresh through its launcher (Q2) while Jason watches the board. Jason's instruction is "run scripts/mosaic queue next and do it" (Q1). The check is manual. Sage posts this checklist on #1508, with evidence, and Jason's observation is the final gate.

  1. Interval and pins.

    • The interval starts at Jason's instruction entry and ends at the first successful result of the concrete work action (item 4). Input after the interval ends, such as Jason's verdict or housekeeping, is outside the audit.

    • At the cutoff, pin:

      • the session file's path and SHA-256, and its session id;
      • the launch snapshot under .pi/state/<seat>/launches/;
      • the pre-test queue.json revision;
      • the brief blob.
    • Freshness (F4, G1). The header having no parentSession rules out a fork, but it doesn't rule out an ordinary resume. So freshness is proved from the launch itself.

      Pinned Pi writes no session file before the first assistant message. SessionManager._persist keeps the header, the setting entries and the instruction in memory, then writes them all at once with an exclusive create (wx) when the first assistant message arrives (dist/core/session-manager.js, around line 739; Rocko reproduced this in round 5). So the file cannot be pinned before the instruction. Nobody seeds it, and nobody sends a warm-up message to make it appear.

      Before the launch, Sage records:

      • the listings of .pi/state/<seat>/sessions/ and .pi/state/<seat>/launches/;
      • the exact launch command line, with --fresh;
      • the UTC start time.

      At the cutoff, Sage pins the session file and the new launch snapshot. They pass only if, retrospectively:

      • the session file is absent from the pre-launch listing;
      • the launch snapshot directory is the only new entry under launches/;
      • the header's timestamp falls after the recorded start time and before the instruction;
      • the header has no parentSession;
      • the entries before the instruction are only the launch's settings (model_change, thinking_level_change), with no message, compaction or branch summary.

      Pinning the file once it first appears is allowed, as long as the cutoff pin follows.

    • Linear file. The header is session identity, not a node in the entry chain. The first entry after the header has parentId null, and every later entry's parentId is the previous entry's id. A branched file fails the gate, so branching is excluded from the demonstration, and the audited chain is the whole file from the instruction to the cutoff. All 18 recorded session files under .pi/state/*/sessions/ have this shape today.

  2. Context audit. Take the SHA-256 at the start and at the cutoff of every context source:

    • the launch snapshot;
    • the committed SOUL and CONTEXT files;
    • <dataRoot>/user/USER.md, whose hash is recorded privately and whose content never goes into git or the issue;
    • the loaded skills;
    • the brief.

    Any change fails the gate. An edit that is made and then reverted inside the interval is not visible to endpoint hashes. That is covered by the cooperative rule that nobody edits these files during the test, and the report states the bound.

    Row-specific text in these sources fails the gate, with one exception: the queue data and the row's brief, which the seat is meant to discover during the test.

  3. Input audit. Any user-role entry anywhere in the file after the instruction and before the cutoff fails the gate, whatever its format.

  4. Execution trace. The audited chain must show, in order, with ordinary reads between the steps allowed:

    • the next call: a toolCall running scripts/mosaic queue next with the canonical root as its working directory, and its toolResult (matched by toolCallId, isError false) naming the row;
    • the brief read: a successful read of the brief named in that result;
    • the start transition: a toolCall whose actual command is scripts/mosaic queue move <row> in-progress --op <OP> … in the canonical root, with a successful toolResult carrying the receipt. The op, row and actor in the command must equal the log entry that made the transition. That entry's revision must be the first one after the pinned pre-test revision to touch the row, and its time must fall inside the interval. A receipt from a retry ("already recorded") shows a lookup, not the origin, and fails this item. Echoing the op, a dry run, or a failed command fails it as well;
    • the work: a concrete action from the brief's "What ships" with a successful result or a visible effect. Proposing a tool call is not enough.

    If the output was lost or truncated, preserve whatever evidence can be recovered, and declare the link inconclusive. Inconclusive is not a pass.

  5. Verdict. Jason's observation, in one sentence on #1508.

This is cooperative evidence. A helper that runs the same command outside the session isn't excluded. The checklist does rule out the observable non-execution cases.

Positive controls (G1). The audit must pass a genuinely fresh session before Gate G relies on it:

  • a unit fixture built with the pinned SessionManager, with no model: a new session, then settings, the instruction and a synthetic assistant message. The freshness and linearity rules pass it;
  • the recorded fresh session files under .pi/state/*/sessions/. The linearity and prefix rules pass them.

No pre-instruction model turn is needed for either.

Negative controls, reviewed by hand against recorded or constructed session files. Each one must fail item 4:

  • an op that is only echoed;
  • a start command with a nonzero or error result;
  • a seat reusing an op a helper recorded earlier;
  • a forked session (parentSession present);
  • an ordinary resumed session: no parentSession, but it holds earlier messages or was already in the pre-launch listing;
  • a session with a compaction entry before the instruction;
  • a branched file, including one where the start sits on an abandoned branch;
  • a file whose first entry after the header points at the header's id, or at any other entry;
  • a brief that was never read;
  • a work command that failed.

Selection among competing rows is covered by unit tests, not by this demonstration.

8.12 Committing the queue (S5, T5, F1, F3)

The CLI never commits (Q3). The lead runs scripts/queue-commit.sh -m MSG. It uses a temporary index, as Sage directed, and commits exactly the tested bytes. Nothing anyone else has staged is swept in. A pre-commit guard stops ordinary commits from reverting the queue.

The queue guard (F1). scripts/git-hooks/pre-commit is versioned. It refuses any git commit whose index entries for docs/plans/queue.json or docs/plans/QUEUE.md differ from HEAD's (git diff --cached --quiet HEAD -- <both paths>). Its message names the fix: git reset -q -- docs/plans/queue.json docs/plans/QUEUE.md. Only queue-commit.sh changes those paths in a commit, and it uses commit-tree, which runs no hooks.

  • The guard is installed as <canonicalRoot>/.git/hooks/pre-commit by queue-commit.sh --install-hook, which is privileged.

    • It copies HEAD's blob, mode 0755.
    • It refuses if a different pre-commit hook exists or core.hooksPath is set.
  • Active, not just present (G2). Matching bytes are not enough. With the same bytes but no executable bit, git skips the hook, and Rocko reproduced the revert from queue 12 to 10. A later core.hooksPath selects a different hook while the checked file stays the same. So on every invocation, queue-commit.sh checks all of these:

    • .git/hooks/pre-commit is a regular file, not a symlink, owned by the user and executable, and its bytes equal HEAD's scripts/git-hooks/pre-commit;

    • git config --show-scope --get-all core.hooksPath is empty in every scope;

    • the canary. Git itself runs the hook it would run for a commit. With a new temporary index read from H, git hook run pre-commit must exit 0. After one queue entry in that index is changed to a different blob, it must exit nonzero, and its stderr must carry the guard's own refusal line. A hook git can't find ("cannot find a hook named pre-commit") fails the clean run, so a missing, non-executable or redirected hook refuses.

      I checked this with git 2.55.0 in a scratch repository. An active hook gave a clean exit of 0 and a changed exit of 1. A non-executable hook and an alternate core.hooksPath each gave 1 on the clean run.

    The checks run at step 1, and again just before update-ref in step 7. If either run fails, the script refuses before publishing. They use the lead's normal git environment. A seat's per-command overrides aren't visible to them, which is why 8.5 forbids those overrides.

Why this closes Rocko's schedule. After update-ref publishes C, the shared index still holds H's queue entries, and those differ from C's.

  • Any ordinary git commit is refused by the guard until step 8 reconciles those two entries.
  • A commit whose guard ran before update-ref loses at its own HEAD update. git commit updates HEAD with the parent it read as the expected old value, so it fails with "cannot lock ref 'HEAD': is at C but expected H".
  • The reverse order is safe too: if that commit lands first, the lead's update-ref fails.

I checked all three cases with git 2.55.0 in a scratch repository on 2026-09-26. So there is no window in which an ordinary commit can put the old queue on top of C.

If step 8 can't run, the guard is the maintenance hold Rocko asked for, as long as it stays active under 8.5's rule. Ordinary commits stay refused, each naming the fix, until someone reconciles. No queue lock is held across tests or git.

Sage's narrower proposal. Of its three parts, the pre-commit check is the one that closes F1, and it is adopted. The other two don't close it, and they are not adopted:

  • The shared index empty of everything else. The check runs before update-ref, and a seat can stage and commit after it. Even with an otherwise clean index, the index's queue entries are H's, so any commit in the window would revert the queue.
  • Under the queue lock. Ordinary committers never take the queue lock.

The existing guard in step 1, that no queue path is staged, stays.

Bootstrap (F3):

  1. The queue implementation lands through normal, review-gated source integration: packages/queue, the scripts/mosaic dispatch, queue-commit.sh, the guard and test-queue.sh. HEAD then has the code and no queue.json.
  2. queue-commit.sh --install-hook, which is privileged.
  3. queue genesis (8.2) in the working tree.
  4. queue-commit.sh --genesis -m MSG, the first queue commit.
    • It is allowed only when HEAD has no docs/plans/queue.json and the snapshot's log holds only the genesis entry. Genesis's canonicalRoot and branch must match 8.3, and its migration-map blob id must exist in HEAD.
    • --genesis with a base present refuses, and so does a missing base without --genesis.
    • No op can come in between, because every mutating verb refuses until genesis is committed (8.2).

Steps:

  1. Guard.

    • Run the active-guard checks above: file, mode, bytes, core.hooksPath and the canary.
    • Refuse if the shared index has staged changes to either queue path (git diff --cached --quiet HEAD -- docs/plans/queue.json docs/plans/QUEUE.md). Seats never stage queue files.
    • The branch must be the genesis branch.
    • Record H=$(git rev-parse HEAD), and whether H:docs/plans/queue.json exists (git cat-file -e). Absent requires --genesis; present forbids it.
  2. Snapshot. scripts/mosaic queue snapshot --out DIR takes the lock and runs 8.5 steps 1–2. It requires a confirmed tail and a current view, copies both files' bytes into DIR (a new mktemp -d outside the repository), and releases the lock. The two files come from one revision because they are read under the lock.

  3. Base (F3). The script writes the base into DIR from the canonical object database. Unless --genesis is set, that is git -C <root> cat-file blob H:docs/plans/queue.json > DIR/base.json. It also writes DIR/base.id with H and H's tree id. The validator reads only files in DIR. It runs no git, and resolves nothing from its working directory.

  4. Verify with HEAD's code. Unpack git -C <root> archive H into a new temp directory. There, run its node --test packages/queue/tests/ and its queue verify --snapshot DIR --base-file DIR/base.json, or --base-absent for genesis. It runs with GIT_CEILING_DIRECTORIES set, so no repository is found around it. Snapshot verify checks only this:

    • the given pair is valid and the table is its render;
    • the log extends the base's log, or is genesis alone when the base is absent.

    It does not certify live witness continuity (F2). That is canonical verify's job, under the lock. Dirty working copies of the queue code play no part.

  5. Blobs. B1=$(git hash-object -w DIR/queue.json), and B2 the same for DIR/QUEUE.md.

  6. Tree. In a temporary index at a path that doesn't exist yet:

    • export GIT_INDEX_FILE=$(mktemp -d)/index;
    • git read-tree $H, which creates it;
    • git update-index --add --cacheinfo 100644,$B1,docs/plans/queue.json --cacheinfo 100644,$B2,docs/plans/QUEUE.md;
    • T=$(git write-tree).

    Git 2.55 also accepts an empty file as an index, but the plan doesn't rely on that. Then check that git diff-tree -r --name-only $H $T lists only those two paths.

  7. Commit.

    • C=$(git commit-tree $T -p $H -F msgfile).
    • Run the active-guard checks again. If they fail, refuse; nothing has been published.
    • git update-ref -m queue-commit refs/heads/<branch> $C $H. This fails if the branch has moved since step 1. If it does, nothing is lost: start again.
    • commit-tree runs no hooks, the guard included. There are no other hooks today.
  8. Reconcile the shared index.

    • Check again that git rev-parse HEAD is C. If it isn't, which needs a bypass, stop, report, and don't touch the index.
    • Check that the shared index's two entries (git ls-files -s) still equal H's. For genesis, queue.json must be absent. If they differ, someone staged a queue path: stop and report.
    • Run git reset -q -- docs/plans/queue.json docs/plans/QUEUE.md. It points those two entries at C's blobs and touches nothing else.
    • If the index is locked by another git process, print that command and exit 3. The guard holds ordinary commits until it runs.

Why not something simpler: git commit --only -- <paths> also builds a temporary index, so it wouldn't sweep in other staged files either. But it commits the working-tree bytes as they are at commit time, which may no longer be the tested snapshot, and it runs hooks and the editor.

Review-gated source is integrated the same way. The prospective tree comes from H plus the blobs of the approved manifest paths.

  • review verify-commit ID $T must pass on that tree before commit-tree.
  • The suites run from git archive $T unpacked, not from the working tree.
  • If the approved bytes are no longer on disk, integration refuses (8.9).

Ordinary source commits by seats go through the guard. The only way it refuses them is a queue path that is staged or not yet reconciled.

Committing still needs its own authorization. This procedure doesn't give it.

Tests, in a scratch repository:

  • F1:
    • pause right after update-ref, then make an ordinary commit of unrelated staged source. The guard refuses it. After step 8 it commits, and the queue stays at C's revision;
    • a commit whose guard ran before update-ref: its HEAD update fails;
    • step 8 while index.lock is held: exit 3, and ordinary commits are refused until the printed command runs;
    • HEAD changed before update-ref: refuse;
    • a shared-index change during the procedure is not committed;
    • a queue path staged after update-ref: step 8 stops and touches nothing;
    • a missing or different hook: queue-commit.sh refuses;
    • a hook with the same bytes but no executable bit, a symlinked hook, and a newly set core.hooksPath in the local and the global scope. Each makes queue-commit.sh refuse before update-ref;
    • the guard deactivated between step 1 and step 7: refuse at the step-7 recheck;
  • bootstrap (F3):
    • an implementation-only HEAD, then genesis, then the --genesis commit, then a later extending commit;
    • --genesis with a base present refuses;
    • an absent base without --genesis refuses;
    • an op after genesis but before the first commit refuses;
    • the archived validator, run in a directory outside any repository;
  • general:
    • an unrelated staged file stays staged and uncommitted;
    • a queue write after the snapshot is not committed;
    • the committed blobs equal the snapshot bytes;
    • a snapshot whose log does not extend the base refuses;
    • verify-commit on a prospective tree with one changed manifest path fails.

8.13 Brief checks (S10, R13)

On add and on any brief change, the brief must be:

  • a repo-relative path whose realpath is inside canonicalRoot;
  • a regular file, not a symlink;
  • present in HEAD's tree (git cat-file -e HEAD:<path>). A brief that is only staged is refused.

The anchor heading must occur exactly once in the HEAD blob. The row records the blob id.

verify --current rechecks the briefs of non-terminal rows against HEAD and reports drift. Replay never does this. The five untracked briefs (section 7, R13) must be committed before genesis.

The working brief (T8). A seat reads the brief from the working tree, not from HEAD. So start computes the git blob id of the working file's bytes in-process, as SHA-1 over blob <len>\0 plus the bytes, with no git write. It refuses if that differs from the pinned blob, and next flags the row (8.8). A re-pin is a privileged brief change. The new version has to be committed first, and the row then records the new blob.

8.14 Acceptance checklist

  • A:

    • tests for every transition and refusal in 8.7;
    • the op-id rules in 8.6;
    • every lock schedule in 8.4 and write schedule in 8.5;
    • the canonical checks in 8.3, with worktree, second-clone, detached-HEAD and wrong-branch fixtures;
    • genesis refusals (8.2), witness, sync and accept-history (8.5);
    • the claim lifecycle, add defaults and J4/J5 cases (8.7);
    • the working-brief check (8.13);
    • the queue-commit.sh tests, including the guard and bootstrap (8.12);
    • the unlocked-read race tests (8.4, 8.5);
    • byte-stable render;
    • next ordering among competing rows.

    scripts/test-queue.sh passes. Genesis runs from the reviewed migration map. The README states:

    • the cooperative trust boundary and the git side of the protocol (8.5);
    • the platform assumptions and that power loss is untested;
    • the unacknowledged-op rule;
    • the same-seat claim limit;
    • the check-to-rename windows.
  • C: BRIEF-TEMPLATE.md, plus two owner-accepted briefs recorded on #1508.

  • B: the AGENTS.md cadence line and the five CONTEXT.md lines (8.1) name scripts/mosaic queue next. A fresh launch snapshot shows the new text. Then Gate G (8.11), including the negative controls.

  • D: the 8.9 tests. Then one full live round on a real issue with the per-seat tokens read in place (8.9), with zero new files under docs/plans/reviews/.

  • E: the 8.10 tests with a fake gitea-api.sh, covering each result level and the legacy age bound, and node --test packages/ledger/tests/ passing. One dated run with the coverage line and its result level is posted on #1508.

  • Order: A with C, then B, then Gate G. D and E come after A, and E may go before D (Sage, 2026-09-26). D's live round uses the per-seat tokens Jason ruled on (8.9).

8.15 Decisions

Jason told Sage to decide as lead. Every item below is decided by Sage 2026-09-26; nothing in this plan waits on Jason except D's live posting round (Q4). Round 5: Jason ruled on D's credentials the same day (8.9), so nothing waits on him now.

Item Decision Where
Q1 Gate G's instruction is "run scripts/mosaic queue next and do it" 8.11
Q2 Gate G runs on a Pi launcher with --fresh; next without identity refuses 8.8, 8.11
Q3 the CLI never commits; the lead commits the queue files after test-queue.sh 8.12
Q4 D posts only with an explicit per-seat credential file (MOSAIC_GITEA_CREDENTIAL_FILE) and refuses the default file. Where seat tokens come from is Jason's; the live posting test waits for him, the build does not. Round 5: he ruled that each seat's token is read in place (8.9) 8.9, 8.14
J1 owners progress their required rows; reorder, park and clearing required are Jason-only 8.7
J2 cooperative trust model; claimed actors, detection through replay and E 8.7, README
J3 briefs mandatory; queued = brief exists, not accepted 8.7
J4 only jason unparks, to queued 8.7
J5 in-review→done allowed where gateOwner is not Jason, with logged evidence 8.7
J6 closes = issues; narrowing needs a logged reason 8.7, 8.10
J7 dropped —
J8 12 calls at most, 0 with --no-issues, "unknown (budget)" past the limit 8.10
J9 Sage writes a parked stub brief for row 8, which keeps id 8 8.2
Reduced liveness gate accepted, labelled reduced 8.10
E before D allowed 8.14
8.0 single file accepted; no journal, no queue repair; directory fsync on every write 8.0, 8.5
8.4 link() accepted in place of a literal O_EXCL 8.4
8.3 root in genesis accepted; no config key 8.3

Two points where I resolved Sage's wording:

  • For J4 and J5, "as proposed in 7.2" replaced the 8.7 build defaults that referenced the brief (parked terminal; in-review→done refused). The 7.2 proposals are what the matrix now says.
  • J4's return state is queued, not the pre-park state, so a possibly stale brief is re-accepted before work resumes.

Round 4: choices of mine for Sage to confirm. None of these needs a new owner ruling. Each is a lead-level specification choice made in answer to T1–T8:

  • The witness file .git/mosaic-queue.head (8.2). It is a check value, not a second store. Without it, a rollback that loses ops can't be detected, which is what you asked for in T2.
  • Pinning the branch alongside the root (8.3). Moving the queue to next or main later is a reviewed migration.
  • accept-history is privileged, needs --yes and a reason, and gives up deduplication for the lost range (8.5).
  • No not-posted resolution. A privileged abandon records the risk of a duplicate instead (8.9).
  • The failed-4xx allowlist: 400, 401, 403, 404 and 422. Everything else is uncertain (8.9).
  • add defaults: gateOwner is jason, and reviewers is empty (8.7).
  • The op suffix .outcome is reserved (8.6).
  • The filesystem allowlist: ext4, xfs, btrfs, and tmpfs for tests (8.5).
  • E never prints a full pass. Its best result is reduced pass (8.10).
  • queue-commit.sh uses commit-tree, which skips hooks. There are none today (8.12).

Sage confirmed all of the above on 2026-09-26.

Round 5: lead-level choices for Sage to confirm. None of these needs an owner ruling.

  • The queue guard is a pre-commit hook in the canonical checkout (8.12). It runs on every seat's git commit, and refuses only when a queue path is staged or not yet reconciled. Of Sage's proposal, this part is adopted. The clean-index and queue-lock parts are not, because they don't close F1.
  • Genesis is committed before any op (8.2), with a --genesis exception for an absent base (8.12).
  • Unlocked reads read the witness first and recheck under the lock before any adverse report. There is no retry loop (8.4).
  • Gate G requires a linear session file and proves freshness from the launch (8.11).
  • Caller op ids are capped at 72 characters (8.6).
  • "There are none today" (above) no longer holds once the guard is installed. commit-tree skips the guard on purpose, and 8.12 step 8 plus the guard cover that gap.
  • D only stats the credential file, and never opens it. The lead's jarvis token follows the seat rule (8.9).

Sage confirmed the round-5 choices on 2026-09-26. She also set the jarvis Gitea token path, and ruled that a write:repository 403 in the first live round is an expected finding (8.9).

Round 6. No new lead-level choice. Two of Rocko's corrections:

  • Gate G's pins are taken before the launch and at the cutoff, never on a session file before the instruction;
  • the guard is checked for being active on every invocation.

8.16 Where each round-2 finding is resolved

Finding Resolved in
S1 lock 8.4
S2 torn append 8.0 (one file, rename only), 8.5
S3 view drift and hand edits 8.5
S4 retry identity 8.6
S5 commit integration 8.12
S6 one history, genesis, replay 8.2, 8.3
S7 prerequisites, field permissions, closure 8.7, 8.10
S8 identity evidence 8.8, 8.10, 8.11
S9 at most one POST 8.9
S10 candidates, briefs, remediation 8.9, 8.13, 8.10
S11 contradictory body this section is the specification (banner at the top)

The round-2 modifications:

  • R1's lock is replaced by 8.4, which never reclaims automatically.
  • R3's cooperative writer stays, restricted to the canonical checkout.
  • R6's claims become seat names with no incarnation.
  • R7 becomes 8.9's uncertain state, with no resend.

8.17 Where each round-3 finding is answered

Finding Answered in
T1 visibility vs durability 8.0 note, and 8.5: the platform, the three points, steps 6–10, uncertain exit 3, sync, the fault-injection tests, and the statement that power loss is untested; D sends only after a durable intent (8.9 step 1)
T2 git rollback 8.2 witness; 8.5 step 7 unchanged check, "history lost" refusal, manual recovery replacing "restore with git", accept-history, and the git side of the protocol; 8.3 branch pin; 8.12 step 3 (committed history only grows)
T3 lock and gate classification 8.4: classification order with host before pid, --check-gate, release by inode and full record, unlock never takes the lock, O_EXCL temp with checked writes, other link errors refuse
T4 requesting and outcomes 8.9: the attempt states, where requesting blocks as uncertain does; REQOP.outcome entries; conflict; resolve --posted verified by GET; abandon in place of not-posted; the 4xx allowlist; process-group kill; move in-review as the request. 8.5 step 3 and 8.6: lookup before the view check
T5 shared-index commit 8.12: temporary index, blobs from the tested snapshot, commit-tree plus update-ref against the expected HEAD, tests from git archive, prospective-tree verify-commit
T6 Gate G execution 8.11: interval and pins, the ancestry chain, toolCall/toolResult matching, a first transition inside the interval, the work result, the context exception, hash bounds, the negative controls
T7 unverified liveness 8.10: pid-unknown; the results fail, incomplete and reduced pass, with no full pass; the legacy age lower bound
T8 internal consistency 8.7 add rules and claim lifecycle, with J5 precedence and settlement; 8.13 working-brief check; 8.2 genesis bootstrap; 8.3 git -C with absolute paths; 8.1 and 8.14 B's context files

8.18 Where each round-4 finding is answered

Finding Answered in
F1 HEAD/shared-index gap 8.12: the queue guard, why it closes the schedule, step 8 reconciliation with its rechecks, the guard as maintenance hold, the tests. 8.5: the commit side of the cooperative protocol. 8.1: the hook file
F2 unlocked reads 8.4: witness-first order, and a locked recheck before any adverse report. 8.5: the unlocked-reads bullet and the race tests. 8.12 step 4: snapshot verify does not certify witness continuity
F3 bootstrap and base 8.2: genesis committed before any op. 8.12: the bootstrap sequence, --genesis, the base written from the object database, the validator isolated from any repository, the temporary index at a fresh path
F4 Gate G freshness 8.11: freshness proved from the launch, a linear file, input audited across the whole file, new negative controls
F5 op-id length 8.6: caller ids ≤ 72, log ids ≤ 80, boundary tests
Rocko's T3 note (quiescence) 8.4: gate removal only once no queue command is running
Rocko's T4 note (bounded GETs) 8.9: GET user and resolve --posted under the 30 s process-group deadline
Jason's credential ruling 8.9 credentials; 8.14 D

8.19 Where each round-5 finding is answered

Finding Answered in
G1 Gate G pins evidence Pi hasn't written yet 8.11: pre-launch receipts (listings, command line, start time), the retrospective audit at the cutoff, the header exempt from the entry chain with the first entry's parentId null, positive controls with no pre-instruction model turn, and a new negative control
G2 matching bytes don't prove the guard runs 8.12: file, mode, bytes, every-scope core.hooksPath and the git hook run canary, at step 1 and before update-ref, with tests. 8.5: disabling or overriding the guard added to the forbidden bypasses, and the trust limit stated
Lead login (Rocko's note) 8.9: the expected login is jarvis for the lead, whose actor stays the lead, with a success and a wrong-login test

Log

  • 2026-09-26: first version, sent to Sage.

  • 2026-09-26: Sage's decisions folded in (Q5–Q9, fixes 5.3 and 5.4). Q1–Q4 marked as the open set for Jason. Section 0 added.

  • 2026-09-26: round 2. Section 7 disposes of Rocko's R1–R14 (ten accept, four accept-modified, none rejected); 7.2 lists J1–J9 for Jason. Q3 recommendation changed to no automatic commit. Q6 and Q7 amended by Sage. Body edits in sections 0–5 point at section 7.

  • 2026-09-26: round 3. Section 8 added as the active specification after Rocko's round 2 (revise, b3a2d72a…e70f4). It follows Sage's direction: manual recovery verbs, a lock with no automatic reclaim and an unlock gate, op ids as the only retry identity, canonical checkout only, no incarnation, at most one review POST. One departure: the log lives inside queue.json, so there is no journal file and no repair verb (8.0). The J8 budget and reading are in 8.10. Banner and section 0 note added; sections 1–7 are unchanged and superseded where they differ.

  • 2026-09-26: Sage's round-3 rulings recorded (8.15): 8.0 single file, link() lock and genesis root accepted, J8 budget accepted, and Q1–Q4, J1, J2, J4, J5, J6, the reduced liveness gate and E before D decided by Sage as lead. 8.0, 8.4, 8.7, 8.10 and 8.14 updated to match. J4 and J5 follow 7.2's proposals rather than the earlier build defaults.

  • 2026-09-26: round 4. Rocko's round 3 (revise, 13a32804…a274, T1–T8) answered in section 8: durability kept separate from visibility, a rollback witness with manual accept-history, host-first lock classification, attempt states for review requests with no not-posted, a temporary-index commit procedure, Gate G tied to its execution trace, E result levels with no full pass, and the add, claim, brief and genesis consistency fixes. 8.17 maps T1–T8. Lead-level choices are listed in 8.15.

  • 2026-09-26: round 5. Rocko's round 4 (revise, fcb8933d…efcff, F1–F5) answered in section 8:

    • a pre-commit queue guard plus shared-index reconciliation (F1), checked in a scratch repository;
    • witness-first unlocked reads with a locked recheck (F2);
    • a genesis-first bootstrap and an isolated validator given its base (F3);
    • Gate G freshness proved from the launch, with a linear file (F4);
    • caller op ids capped at 72 characters (F5).

    Jason's per-seat credential ruling is folded into 8.9. 8.18 maps the findings.

  • 2026-09-26: round 5 follow-up. Sage confirmed the round-5 choices. 8.9 now names the jarvis Gitea token path under the seat rule, and treats a write:repository 403 in the first live round as an expected finding.

  • 2026-09-26: round 6. Rocko's round 5 (revise, 3b031a70…177e, G1, G2) answered in section 8:

    • Gate G freshness proved from pre-launch receipts and a retrospective audit, because Pi writes no session file before the first assistant message. The header is out of the entry chain (G1);
    • the guard checked as active on every invocation, including a git hook run canary (G2), checked in a scratch repository;
    • the lead's expected login is jarvis. 8.19 maps the findings.
  • 2026-09-27: correction from Filbert's Piece D review round 1 (C1), ruled by Sage. The transition table let in-review→waiting-on-jason pass with no reviewer approvals, so a Jason-gated row could close without its reviewers. That move now carries the in-review→done checks on a comment round; the table and 8.9's receipts paragraph say so.