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]>
2414 lines
128 KiB
Markdown
2414 lines
128 KiB
Markdown
# 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 `stat`s the lock path and reads the record. It
|
||
unlinks only if the `(dev, ino)` and every record field equal what it
|
||
acquired. Otherwise it leaves the file alone and reports it. Only `unlock`
|
||
removes another process's lock, and `unlock` refuses a live owner.
|
||
|
||
**`queue unlock`** is manual, any actor may run it, and it never tries to
|
||
take the lock itself.
|
||
1. Create the gate `<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 `stat`s 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 `stat`s 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.
|