From c6f90c47c198c54c8eba9637860eea68b1b7039a Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 13 Oct 2026 17:02:13 -0500 Subject: [PATCH] docs: Researcher row 107 packet, retry-order test and refusal text for queue add Co-Authored-By: Claude Sonnet 5.5 --- agents/researcher/work/queue-107/build.md | 42 +++++ .../work/queue-107/candidate-manifest.sha256 | 11 ++ agents/researcher/work/queue-107/n1-mutant.sh | 36 ++++ agents/researcher/work/queue-107/n1-run.txt | 5 + .../researcher/work/queue-107/n1/control.out | 14 ++ .../researcher/work/queue-107/n1/control.rc | 1 + .../researcher/work/queue-107/n1/mutant.out | 38 ++++ .../researcher/work/queue-107/n1/mutant.patch | 30 ++++ agents/researcher/work/queue-107/n1/mutant.rc | 1 + .../researcher/work/queue-107/node-suite.out | 166 ++++++++++++++++++ agents/researcher/work/queue-107/row107.patch | 86 +++++++++ .../researcher/work/queue-107/test-queue.out | 36 ++++ 12 files changed, 466 insertions(+) create mode 100644 agents/researcher/work/queue-107/build.md create mode 100644 agents/researcher/work/queue-107/candidate-manifest.sha256 create mode 100755 agents/researcher/work/queue-107/n1-mutant.sh create mode 100644 agents/researcher/work/queue-107/n1-run.txt create mode 100644 agents/researcher/work/queue-107/n1/control.out create mode 100644 agents/researcher/work/queue-107/n1/control.rc create mode 100644 agents/researcher/work/queue-107/n1/mutant.out create mode 100644 agents/researcher/work/queue-107/n1/mutant.patch create mode 100644 agents/researcher/work/queue-107/n1/mutant.rc create mode 100644 agents/researcher/work/queue-107/node-suite.out create mode 100644 agents/researcher/work/queue-107/row107.patch create mode 100644 agents/researcher/work/queue-107/test-queue.out diff --git a/agents/researcher/work/queue-107/build.md b/agents/researcher/work/queue-107/build.md new file mode 100644 index 000000000..02bf92221 --- /dev/null +++ b/agents/researcher/work/queue-107/build.md @@ -0,0 +1,42 @@ +# Row 107 (#1601) build notes + +Researcher, 2026-10-13. Brief: `docs/plans/2026-10-13_landing-follow-ups.md`, "Queue add: pin the retry order and drop the row history from the refusal". Base: `aedc5db8f`. Reviewer: Filbert. Covers Filbert's N1 and N2 on row 101 (#1595). + +Candidate change: `row107.patch` (two files). Built and run in `/mnt/storage/scratch/researcher/q107`, detached at `aedc5db8f`, `node_modules` linked to the canonical one. + +## N2: refusal text + +`packages/queue/src/store.mjs`: the privileged-add refusal no longer ends in "(row 100 got jason by default)". It still names `--gate-owner SEAT` and says why: a privileged add has no default gate owner, so a row never gets a gate nobody meant. The code comment above the check dropped "row 100" too and now says the check stays below step 3. + +`packages/queue/tests/gate-owner.test.mjs`: the CLI refusal test keeps `/--gate-owner/` and `/no default gate owner/` and adds `doesNotMatch(r.err, /row \d+/)`. + +Nothing else in `src`, `tests`, `README.md` or `docs/TOOLS.md` cites row 100 (grep). `SEMANTICS` stays 2; no change to the log format, to `applyEntry`, or to what any reader loads. + +## N1: the retry-order test + +New test in `gate-owner.test.mjs`: "a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing". + +1. Scratch repo with committed genesis. +2. `addUnder(doc, "sage", addArgs(), 2)` appends a `sage` add with `gateOwner: null` the way the pre-row-101 write path left it (`applyEntry`, no write-time check). +3. The test writes queue.json, the QUEUE.md table body (`render` between the markers) and the witness `mosaic-queue.head` (revision, `logDigest`, `fileSha`) so all three sit at the add's rev. The witness state is `match`, so the retry reaches step 3 without a tail confirmation. +4. It retries the same op and arguments through the CLI as `sage` with no `--gate-owner`. Expected: exit 0, stdout `(already recorded at rev N)`, queue.json and QUEUE.md byte-identical, revision unchanged. +5. It then sends a fresh op id with the same arguments and expects the refusal (exit 2, names `--gate-owner`, queue.json unchanged), so the test also shows the check was not removed. + +## Mutant (the kill) + +`n1-mutant.sh ` runs `gate-owner.test.mjs` twice and restores `store.mjs` on exit. The mutant moves the privileged-add check block above `const prior = ...` (step 3). Output is in `n1/`; run summary in `n1-run.txt`. + +- control, real `store.mjs`: exit 0, 6 tests, 6 pass. +- mutant, check hoisted above step 3: exit 1, 6 tests, 5 pass, 1 fail. The failing test is `a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing`. The other five still pass, so the hoist is caught by this test alone. +- `n1/mutant.patch` is `git diff` of the mutated tree against the base, so it also shows the candidate's own edits; the mutation is the added hunk at `@@ -501`. +- The mutant was applied to the worktree file only and restored; the candidate patch does not contain it. + +## Suites (all in the q107 worktree, output teed) + +- `node --test packages/queue/tests/*.test.mjs`: 156 tests, 156 pass, 0 fail (`node-suite.out`). +- `scripts/test-queue.sh`: 28 passed, 0 failed (`test-queue.out`). The `queue verify and render --check` case was skipped in my tree because q107 is not the queue's canonical root (`/mnt/storage/src/mosaic-stack`); this is Filbert's N3 from row 101. That case is covered by Sage's canonical-root gate rerun, which is the evidence of record. I did not run suites or apply the patch in the canonical checkout. +- Slot: granted by Sage at 22:00:29Z, released ("clear") at 22:01:30Z; no process of mine left in q107. + +## Scope + +Owned files only: `packages/queue/src/store.mjs` (message and comment), `packages/queue/tests/gate-owner.test.mjs`, this packet. No change to who may pass `--gate-owner` or to the default for non-privileged seats. diff --git a/agents/researcher/work/queue-107/candidate-manifest.sha256 b/agents/researcher/work/queue-107/candidate-manifest.sha256 new file mode 100644 index 000000000..8d4e0aa50 --- /dev/null +++ b/agents/researcher/work/queue-107/candidate-manifest.sha256 @@ -0,0 +1,11 @@ +1114c6fd7a86f235b29f727ab485b5b77b362894480f5e1d0dc86b884f8a776e agents/researcher/work/queue-107/build.md +b3fce13c13d5fbf7fd1981d15254aac01a0d335ed9a1d1a17e425cbb0b1ab477 agents/researcher/work/queue-107/n1/control.out +9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa agents/researcher/work/queue-107/n1/control.rc +fc3c901a167922f108e5c23934ec81cbc40227f580d3d363be3d883dd01dbe4d agents/researcher/work/queue-107/n1/mutant.out +2b42a00a0ebeec520e0c24b0d4a0261b003f8e026432974f6f3b393ce621fb80 agents/researcher/work/queue-107/n1/mutant.patch +4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 agents/researcher/work/queue-107/n1/mutant.rc +0dca6a0081b2b2f11b045bb9bfdd05ce7e7cc795c7765bb2e9601105fd8218cb agents/researcher/work/queue-107/n1-mutant.sh +91ac6bccad182d908e8b73654adc97ea1a462d9c15b27ce7ee692dc7e76757af agents/researcher/work/queue-107/n1-run.txt +bb6836557510677a0ca683b2527607bed1f6fe30b508c727941db160da13b818 agents/researcher/work/queue-107/node-suite.out +3411454aa0ed20990d8c7d28bf3f98b9e92232d1991d5d3bbf9c1bf5ecdb345d agents/researcher/work/queue-107/row107.patch +41f4e3d5972a1c5d4c72e0ab82390da03c6cf5205ce2546348acc7c0bfd6c5f8 agents/researcher/work/queue-107/test-queue.out diff --git a/agents/researcher/work/queue-107/n1-mutant.sh b/agents/researcher/work/queue-107/n1-mutant.sh new file mode 100755 index 000000000..e5e9b110a --- /dev/null +++ b/agents/researcher/work/queue-107/n1-mutant.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Row 107 (#1601) N1: the retry-order test must fail when the privileged-add gate-owner +# check is hoisted above step 3 (the step that answers a recorded op with its receipt). +# Runs only gate-owner.test.mjs, twice, in the given worktree: +# control real store.mjs -> passes +# mutant the check block moved above step 3 -> the retry test fails +# Usage: n1-mutant.sh Restores store.mjs on exit. +set -u +WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd) +S=packages/queue/src/store.mjs; T=packages/queue/tests/gate-owner.test.mjs +cd "$WT" || exit 2 +BAK=$(mktemp -d "$OUT/.bak.XXXXXX") +cp "$S" "$BAK/store" +restore() { cp "$BAK/store" "$S"; rm -rf "$BAK"; } +trap restore EXIT +hoist() { node -e ' +const fs = require("fs"), f = process.argv[1]; +let s = fs.readFileSync(f, "utf8"); +const start = s.indexOf(" if (verb === \"add\" && PRIVILEGED.has(actor) && cargs.gateOwner === null) {"); +if (start < 0) process.exit(3); +const end = s.indexOf(" }\n", start) + " }\n".length; +const block = s.slice(start, end); +s = s.slice(0, start) + s.slice(end); +const anchor = " const prior = cur.doc.log.find((e) => e.op === op);\n"; +if (!s.includes(anchor)) process.exit(4); +fs.writeFileSync(f, s.replace(anchor, block + anchor)); +' "$S"; } +run() { # name + setsid -w timeout -k 10 240 node --test "$T" > "$OUT/$1.out" 2>&1; echo "$?" > "$OUT/$1.rc" + echo "$1: exit $(cat "$OUT/$1.rc"); $(grep -E '^ℹ (tests|pass|fail)' "$OUT/$1.out" | tr '\n' ' ')" +} +run control +hoist || { echo "hoist edit failed ($?)"; exit 3; } +git diff -- "$S" > "$OUT/mutant.patch" +run mutant +grep -E '^✖|not ok' "$OUT/mutant.out" | head diff --git a/agents/researcher/work/queue-107/n1-run.txt b/agents/researcher/work/queue-107/n1-run.txt new file mode 100644 index 000000000..ab1ece52f --- /dev/null +++ b/agents/researcher/work/queue-107/n1-run.txt @@ -0,0 +1,5 @@ +control: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0 +mutant: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1 +✖ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (127.177591ms) +✖ failing tests: +✖ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (127.177591ms) diff --git a/agents/researcher/work/queue-107/n1/control.out b/agents/researcher/work/queue-107/n1/control.out new file mode 100644 index 000000000..0062a6288 --- /dev/null +++ b/agents/researcher/work/queue-107/n1/control.out @@ -0,0 +1,14 @@ +✔ an old privileged add with no gate owner still replays through loadDoc, and gets jason (4.671764ms) +✔ a non-privileged add gets jason through the replay path and can't name a gate owner (1.665168ms) +✔ the CLI refuses a privileged add without --gate-owner, for jason and sage, and writes nothing (176.548832ms) +✔ the CLI add with --gate-owner carries the named owner and records a log entry (136.177338ms) +✔ the CLI add by a non-privileged seat gets jason and can't name a gate owner (191.868872ms) +✔ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (174.100123ms) +ℹ tests 6 +ℹ suites 0 +ℹ pass 6 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 734.354952 diff --git a/agents/researcher/work/queue-107/n1/control.rc b/agents/researcher/work/queue-107/n1/control.rc new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/agents/researcher/work/queue-107/n1/control.rc @@ -0,0 +1 @@ +0 diff --git a/agents/researcher/work/queue-107/n1/mutant.out b/agents/researcher/work/queue-107/n1/mutant.out new file mode 100644 index 000000000..a4ad765cc --- /dev/null +++ b/agents/researcher/work/queue-107/n1/mutant.out @@ -0,0 +1,38 @@ +✔ an old privileged add with no gate owner still replays through loadDoc, and gets jason (4.982127ms) +✔ a non-privileged add gets jason through the replay path and can't name a gate owner (1.631153ms) +✔ the CLI refuses a privileged add without --gate-owner, for jason and sage, and writes nothing (176.95643ms) +✔ the CLI add with --gate-owner carries the named owner and records a log entry (144.322128ms) +✔ the CLI add by a non-privileged seat gets jason and can't name a gate owner (183.776289ms) +✖ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (127.177591ms) +ℹ tests 6 +ℹ suites 0 +ℹ pass 5 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 691.866887 + +✖ failing tests: + +test at packages/queue/tests/gate-owner.test.mjs:123:1 +✖ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (127.177591ms) + AssertionError [ERR_ASSERTION]: queue: sage must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant + + + 2 !== 0 + + at TestContext. (file:///mnt/storage/scratch/researcher/q107/packages/queue/tests/gate-owner.test.mjs:142:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: 2, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/researcher/work/queue-107/n1/mutant.patch b/agents/researcher/work/queue-107/n1/mutant.patch new file mode 100644 index 000000000..5b092f368 --- /dev/null +++ b/agents/researcher/work/queue-107/n1/mutant.patch @@ -0,0 +1,30 @@ +diff --git a/packages/queue/src/store.mjs b/packages/queue/src/store.mjs +index a28a60e87..c8983201d 100644 +--- a/packages/queue/src/store.mjs ++++ b/packages/queue/src/store.mjs +@@ -501,6 +501,9 @@ function mutateAs(ctx, { verb, op, args, by, yes = false, derived = false }) { + const cmp = witnessStep(ctx, loc, cur, { verb, op, args: cargs }); + for (const e of cmp.confirmed) res.err.push(`durable now, never acknowledged: ${e.op} by ${e.by} at ${e.at}`); + // Step 3: a recorded op answers with its receipt, before any view check. ++ if (verb === "add" && PRIVILEGED.has(actor) && cargs.gateOwner === null) { ++ throw refuse(`${actor} must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant`); ++ } + const prior = cur.doc.log.find((e) => e.op === op); + if (prior) { + if (prior.verb !== verb || !sameJson(prior.args, cargs)) { +@@ -528,12 +531,10 @@ function mutateAs(ctx, { verb, op, args, by, yes = false, derived = false }) { + if (view.state === "unknown") throw refuse(unknownMessage(view)); + if (view.state === "stale" && verb !== "accept-history") throw refuse(staleMessage(view, cur.doc.log)); + // A privileged add names its gate owner. There is no default for it: the +- // Jason default put a gate nobody meant on row 100. This check lives in the ++ // Jason default put a gate nobody meant on a row. This check lives in the + // write path, not in applyEntry, so replay of old entries is unchanged and an +- // older reader still loads the log. +- if (verb === "add" && PRIVILEGED.has(actor) && cargs.gateOwner === null) { +- throw refuse(`${actor} must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant (row 100 got jason by default)`); +- } ++ // older reader still loads the log. It stays below step 3: a retry of an add ++ // recorded before this check answers with its receipt. + // Step 5. + const resolved = resolveFor(ctx, loc, cur, verb, cargs, cmp); + const rev = cur.doc.revision + 1; diff --git a/agents/researcher/work/queue-107/n1/mutant.rc b/agents/researcher/work/queue-107/n1/mutant.rc new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/agents/researcher/work/queue-107/n1/mutant.rc @@ -0,0 +1 @@ +1 diff --git a/agents/researcher/work/queue-107/node-suite.out b/agents/researcher/work/queue-107/node-suite.out new file mode 100644 index 000000000..78c838940 --- /dev/null +++ b/agents/researcher/work/queue-107/node-suite.out @@ -0,0 +1,166 @@ +✔ F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C (1160.516162ms) +✔ F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update (1016.036713ms) +ℹ git commit -e: index.lock free during the editor +✔ F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update (951.36439ms) +ℹ git commit -e -- src.txt: index.lock held during the editor +✔ F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs (973.318494ms) +✔ F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published (1014.016398ms) +✔ F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7 (992.976006ms) +✔ F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard (1254.346199ms) +✔ F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved (788.923115ms) +✔ F1: a shared-index change during the procedure is not committed (918.041028ms) +✔ F1: a queue path staged after update-ref: step 8 stops and touches nothing (948.02172ms) +✔ F1: a missing or a different hook refuses (631.47127ms) +✔ F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref (1049.638162ms) +✔ F1: the canary refuses a hook that git would not run (522.529338ms) +✔ F1: the guard deactivated after step 1 is refused at the step-7 recheck (782.173979ms) +✔ bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit (753.841973ms) +✔ bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch (810.329931ms) +✔ bootstrap: the archived tests and validator run outside any repository (761.830901ms) +✔ general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes (746.450594ms) +✔ general: a queue write after the snapshot is not committed (1110.578813ms) +✔ general: a snapshot whose log does not extend the base refuses (798.282483ms) +✔ general: install-hook privilege, repair of a same-bytes hook, and its refusals (152.033699ms) +✔ general: environment overrides, a linked worktree and usage (529.556287ms) +✔ general: a queue path staged before the run refuses at step 1 (543.563654ms) +✔ general: HEAD's queue tests failing in the archive refuse (765.47481ms) +✔ genesis document serializes deterministically and replays (5.87449ms) +✔ a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization (3.547015ms) +✔ a tampered result, receipt or viewSha fails replay (3.497006ms) +✔ op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries (0.235882ms) +✔ add: defaults for an ordinary seat, privileged extras, refusals (5.244619ms) +✔ matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied (6.750751ms) +✔ matrix: release, review round, changes requested and waiting-on-jason (16.727748ms) +✔ matrix J5: in-review→done by the gate owner with evidence naming the current round (12.682772ms) +✔ review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it (28.926564ms) +✔ the row schema refuses a round with a null issue, and the A1 review shape (P2) (2.736256ms) +✔ matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not (1467.124307ms) +✔ matrix: blocked keeps the claim and returns only to previousState (3.907751ms) +✔ matrix J4: parking is Jason's, refused while required; unpark returns to queued (2.90147ms) +✔ field edits: who may change what (5.294628ms) +✔ set issues keeps a logged narrowing of closes (N10) (2.804741ms) +✔ text the table shows refuses \ and <, everywhere it enters (N8) (2.000961ms) +✔ every accepted text renders to nine cells on every row (N8) (30.571059ms) +✔ genesis: the map refuses an owner among its row's reviewers; replay doesn't (2026-10-04) (0.793724ms) +✔ times and dates must be calendar values, not just the shape (2026-10-04) (3.078446ms) +✔ replay holds every op id to the caller's rule (N11) (8.50338ms) +✔ note: owner, listed reviewer or privileged; empty clears (2.004834ms) +✔ assign moves the claim with the owner; done clears it (4.410813ms) +✔ render is byte-stable and escapes pipes (0.818246ms) +✔ view classification: current, genuine stale, edited stale marker, changed current body, markers (1.198739ms) +✔ next: resume, then review, then start, then wait, then nothing; lowest id first (29.278564ms) +✔ canonical args make a retry's identity independent of list order (0.350682ms) +✔ manifests, headings and blob ids (0.47764ms) +✔ the migration map: one queue-map block, exact keys (0.882947ms) +✔ every call but `queue` reaches the seat CLI exactly as before A2 (566.22895ms) +✔ no argument reaches the app entry in packages/cli, and only that call (63.365341ms) +✔ `queue` reaches the queue CLI with the rest of the arguments (201.909087ms) +✔ the pre-A2 fixture is the script A2 changed (0.336944ms) +✔ an old privileged add with no gate owner still replays through loadDoc, and gets jason (7.994016ms) +✔ a non-privileged add gets jason through the replay path and can't name a gate owner (2.72104ms) +✔ the CLI refuses a privileged add without --gate-owner, for jason and sage, and writes nothing (236.817559ms) +✔ the CLI add with --gate-owner carries the named owner and records a log entry (188.21768ms) +✔ the CLI add by a non-privileged seat gets jason and can't name a gate owner (243.477248ms) +✔ a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing (237.313987ms) +✔ acquire publishes the record by link; release removes only its own lock (5.212363ms) +✔ a kill between the temp write and the link leaves no lock (43.494937ms) +✔ a short or failed temp write refuses and leaves no lock and no temp (2.624798ms) +✔ a link error other than EEXIST refuses (1.309002ms) +✔ an error after the link releases the lock: unreadable gate, failing temp stat (3.14569ms) +✔ a release that fails on a gate path is reported, never a stack trace (P1) (4.231373ms) +✔ a paused holder: another writer waits 10 s, then refuses naming it live (10058.249377ms) +✔ two concurrent unlockers: the second refuses on the gate (21.483744ms) +✔ a writer publishing during an unlock, lock first: unlock sees it live and refuses (1.851943ms) +✔ a writer publishing during an unlock, gate first: the writer releases and refuses (1.618574ms) +✔ a gate swapped while held is left in place and reported, on success and on refusal (N1) (1.794562ms) +✔ a reused pid within one boot is mismatch; unlock removes the lock and never signals the process (2.823939ms) +✔ the same pid and start on a different boot is mismatch (0.373891ms) +✔ a foreign host is unknown whatever the local pid says; unlock refuses (44.510416ms) +✔ unreadable /proc: classification is unknown and acquire refuses (0.743157ms) +✔ invalid records: empty, unparsable, wrong keys, bad start or boot (0.206369ms) +✔ a stale gate blocks writers; --check-gate says mismatch for a reused pid (4.00565ms) +✔ a delayed release by a dead owner, after unlock and a new owner: the inode check keeps the new lock (2.266631ms) +✔ release checks the inode too: a byte-identical lock file with a new inode is left in place (1.013708ms) +✔ unlock refuses a live, unknown or invalid lock, and does nothing without one (3.888697ms) +✔ the migration map validates and renders the golden genesis table (5.056552ms) +✔ the marked QUEUE.md holds every row and parked item between its markers (1.682231ms) +✔ map-check reports each kind of drift (4.452864ms) +✔ a request posts once as the requester; a retry sends nothing (588.246636ms) +✔ each transport answer maps to posted, failed or uncertain (8.9 step 3) (3087.462323ms) +✔ the pre-send checks: GET user must name the requester, under the deadline (993.977027ms) +✔ the lead's request refuses a token for login sage (550.1461ms) +✔ the credential file: the seat's own, 0600, no symlink, never the shared default (462.941491ms) +✔ an unresolved request blocks a new request, a new round, waiting-on-jason and done (1469.113758ms) +✔ a same-op retry after a kill sends nothing, even with a stale view (2046.75194ms) +✔ a held lock at the outcome exits 3 and names what the transport said (570.663248ms) +✔ late outcomes: after an abandon, and after a resolve with the same or another id (1362.555726ms) +✔ resolve checks the comment: issue, markers, round, candidate and author (1344.707318ms) +✔ the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token (451.072055ms) +✔ validateRow checks a request round's shape, which every replayed entry must keep (307.192139ms) +✔ request, changes, a new candidate, approval: every round pinned; no review files (1256.758482ms) +✔ a row with no reviewers opens a round that posts nothing (741.565414ms) +✔ verify-commit: a prospective tree must hold exactly the candidate's paths (1034.962311ms) +✔ semantics: v1 entries replay as before; review entries need v2 (330.693796ms) +✔ set reviewers refuses the row's owner (2026-09-28) (230.69334ms) +✔ the owner records no verdict, even as a listed reviewer (308.665271ms) +✔ a request comment over the length limit is not sent (345.51297ms) +✔ a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing (347.875176ms) +✔ a Jason-gated row reaches waiting-on-jason only on every reviewer's approval (1635.990297ms) +✔ genesis: refusals before anything is written (439.101931ms) +✔ genesis: the map must be committed, well formed, with committed briefs and seats (590.247148ms) +✔ genesis: markers, a stray witness, once only; a retry returns the receipt (495.392063ms) +✔ genesis: a file holding genesis alone with no witness is confirmed by sync or a retry (328.594176ms) +✔ canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works (635.666025ms) +✔ --by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12) (474.953911ms) +✔ op ids: missing, too long, reserved; a retry answers; another payload refuses (584.708665ms) +✔ a retried add returns the id it first allocated, after reassignment and after done (627.693947ms) +✔ Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round (440.744317ms) +✔ the review issue and the evidence round through the CLI (lead decision 23, 8.7) (863.793641ms) +✔ claims and add defaults through the CLI; candidates are manifests or reachable commits (608.937414ms) +✔ add, set reviewers and assign refuse the row's owner as a reviewer (408.855897ms) +✔ the working-brief check: a changed working copy refuses the start and flags next (497.090166ms) +✔ next: resume first, then nothing for an idle seat; needs a seat (289.503248ms) +✔ view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes (510.629935ms) +✔ view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers (934.097135ms) +✔ a hand edit to queue.json refuses every verb, reads included (488.221374ms) +✔ verify and render --check leave bytes and mtimes unchanged (404.658418ms) +✔ render is byte-stable across runs and repositories (203.602713ms) +✔ snapshot and verify --snapshot (636.263411ms) +✔ usage errors exit 4 (497.787835ms) +✔ a short write, ENOSPC or a file fsync failure: nothing visible, temp removed (187.731033ms) +✔ a rename failure: nothing visible, temp removed (127.211351ms) +✔ a directory fsync failure: uncertain, exit 3, no receipt; the tail refuses new ops; a retry confirms (152.518642ms) +✔ a directory fsync failure, then sync names the op (302.927242ms) +✔ a witness write failure: uncertain, durable, exit 3; the view is untouched (141.780309ms) +✔ the .git fsync after the witness rename fails: uncertain, exit 3, the witness says so (129.406875ms) +✔ confirming a tail fsyncs queue.json and docs/plans before the witness; either failure changes nothing (141.047729ms) +✔ the docs/plans fsync after the view rename fails: the op stands, the view is written, a warning says so (117.72615ms) +✔ a lock swapped while held is left in place and reported, on a receipt and on a refusal (132.583163ms) +✔ a lock that cannot be released after an op is reported, on a receipt and on a refusal (131.591064ms) +✔ unlock prints a swapped gate's warning on stderr, the result on stdout (141.894444ms) +✔ a view write that fails keeps the op and reports a stale view (102.742368ms) +✔ SIGKILL before the rename: nothing recorded; the retry removes the leftover temp (583.429567ms) +✔ SIGKILL after the rename, before the witness: the tail refuses new ops and sync names the op (600.515277ms) +✔ SIGKILL after the witness, before the view: the stale refusal names the op (577.661942ms) +✔ SIGKILL after the view, before the receipt: the retry returns the receipt (583.325984ms) +✔ git checkout between steps 1 and 7: step 7 refuses and nothing is written (128.099045ms) +✔ git stash restoring an older valid pair: history lost; accept-history needs privilege, a reason and --yes (806.733373ms) +✔ a deleted witness: refused after the locked recheck; accept-history records it absent (196.479122ms) +✔ a header edit during a write: the op stands, the view write is skipped with a warning (126.412901ms) +✔ a reader paused between the witness and the file while a writer finishes: no lost-history report (125.46719ms) +✔ rows: every row as show prints it, in list order; writes nothing; same notes and refusals as list (163.26959ms) +✔ file-then-witness order forced by a hook: the locked recheck prevents a false report (126.237949ms) +✔ a writer paused before and after the witness rename: readers see a tail, then a match (131.365251ms) +✔ a true rollback is reported only after the locked recheck; a held lock names its holder instead (494.995973ms) +✔ an accept-history in progress: an unlocked reader waits on the lock and never reports lost history (444.09088ms) +✔ the platform check refuses other filesystems (121.837372ms) +✔ tmpfs passes only a test layer that allows it (N5) (140.459083ms) +✔ unlock keeps a multi-line lock record on stdout (P3) (151.239913ms) +ℹ tests 156 +ℹ suites 0 +ℹ pass 156 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 20037.145217 diff --git a/agents/researcher/work/queue-107/row107.patch b/agents/researcher/work/queue-107/row107.patch new file mode 100644 index 000000000..8c47bac74 --- /dev/null +++ b/agents/researcher/work/queue-107/row107.patch @@ -0,0 +1,86 @@ +diff --git a/packages/queue/src/store.mjs b/packages/queue/src/store.mjs +index a28a60e87..4a26e2058 100644 +--- a/packages/queue/src/store.mjs ++++ b/packages/queue/src/store.mjs +@@ -528,11 +528,12 @@ function mutateAs(ctx, { verb, op, args, by, yes = false, derived = false }) { + if (view.state === "unknown") throw refuse(unknownMessage(view)); + if (view.state === "stale" && verb !== "accept-history") throw refuse(staleMessage(view, cur.doc.log)); + // A privileged add names its gate owner. There is no default for it: the +- // Jason default put a gate nobody meant on row 100. This check lives in the ++ // Jason default put a gate nobody meant on a row. This check lives in the + // write path, not in applyEntry, so replay of old entries is unchanged and an +- // older reader still loads the log. ++ // older reader still loads the log. It stays below step 3: a retry of an add ++ // recorded before this check answers with its receipt. + if (verb === "add" && PRIVILEGED.has(actor) && cargs.gateOwner === null) { +- throw refuse(`${actor} must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant (row 100 got jason by default)`); ++ throw refuse(`${actor} must pass --gate-owner SEAT on add: a privileged add has no default gate owner, so a row never gets a gate nobody meant`); + } + // Step 5. + const resolved = resolveFor(ctx, loc, cur, verb, cargs, cmp); +diff --git a/packages/queue/tests/gate-owner.test.mjs b/packages/queue/tests/gate-owner.test.mjs +index b3407dbc6..b01e5723b 100644 +--- a/packages/queue/tests/gate-owner.test.mjs ++++ b/packages/queue/tests/gate-owner.test.mjs +@@ -3,10 +3,11 @@ + // scratch repository. Replay is exercised through loadDoc, which runs the real + // replay path over a log that holds an old privileged add with no gate owner. + import assert from "node:assert/strict"; +-import { readFileSync } from "node:fs"; ++import { readFileSync, writeFileSync } from "node:fs"; ++import { join } from "node:path"; + import { test } from "node:test"; + import { +- applyEntry, buildDoc, canonArgs, genesisReceipt, genesisRows, loadDoc, parseMigrationMap, render, rowsArray, serialize, sha256, ++ applyEntry, buildDoc, canonArgs, genesisReceipt, genesisRows, loadDoc, logDigest, parseMigrationMap, render, rowsArray, serialize, sha256, splitView, + } from "../src/queue.mjs"; + import { QueueError } from "../src/errors.mjs"; + import { MAP_ROWS, cli, genesisCommitted, mapText, scratchRepo } from "./helpers.mjs"; +@@ -82,6 +83,7 @@ test("the CLI refuses a privileged add without --gate-owner, for jason and sage, + assert.equal(r.code, 2, r.err); + assert.match(r.err, /--gate-owner/); + assert.match(r.err, /no default gate owner/); ++ assert.doesNotMatch(r.err, /row \d+/, "the refusal states the rule, not a row's history"); + assert.deepEqual(readFileSync(repo.queuePath), queueBefore); + assert.deepEqual(readFileSync(repo.viewPath), viewBefore); + assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).revision, revBefore); +@@ -113,3 +115,39 @@ test("the CLI add by a non-privileged seat gets jason and can't name a gate owne + assert.match(refusedRun.err, /only a privileged actor may set gateOwner on add/); + assert.deepEqual(readFileSync(repo.queuePath), before); + }); ++ ++// Step 3 of the write path answers a recorded op before the gate-owner check ++// runs. An add recorded before row 101 has a null gate owner, so a retry of it ++// carries none either; hoisting the check above step 3 would refuse that retry. ++// Every other retry test passes --gate-owner, so this one pins the order. ++test("a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing", (t) => { ++ const repo = scratchRepo(t); ++ genesisCommitted(repo); ++ const before = JSON.parse(readFileSync(repo.queuePath, "utf8")); ++ const doc = addUnder(before, "sage", addArgs(), 2); ++ const entry = doc.log.at(-1); ++ assert.equal(entry.args.gateOwner, null); ++ assert.equal(entry.by, "sage"); ++ // The state the old write path left: queue.json, the view and the witness all at the add's rev. ++ const bytes = Buffer.from(serialize(doc)); ++ const parts = splitView(readFileSync(repo.viewPath, "utf8")); ++ writeFileSync(repo.queuePath, bytes); ++ writeFileSync(repo.viewPath, parts.head + render(doc.rows, entry.rev) + parts.tail); ++ writeFileSync(join(repo.gitDir, "mosaic-queue.head"), `${JSON.stringify({ revision: entry.rev, logDigest: logDigest(doc.log, entry.rev), fileSha: sha256(bytes), at: entry.at })}\n`); ++ const queueBefore = readFileSync(repo.queuePath); ++ const viewBefore = readFileSync(repo.viewPath); ++ ++ const retry = ["add", "--op", entry.op, "--piece", "Gate row", "--gate", "g", "--brief", "docs/plans/brief-b.md#Queue"]; ++ const r = cli(repo, retry, { by: "sage" }); ++ assert.equal(r.code, 0, r.err); ++ assert.match(r.out, new RegExp(`\\(already recorded at rev ${entry.rev}\\)`)); ++ assert.deepEqual(readFileSync(repo.queuePath), queueBefore); ++ assert.deepEqual(readFileSync(repo.viewPath), viewBefore); ++ assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).revision, entry.rev); ++ ++ // The check still bites a new op with the same arguments. ++ const fresh = cli(repo, ["add", "--op", "add-gate-fresh-1", ...retry.slice(3)], { by: "sage" }); ++ assert.equal(fresh.code, 2, fresh.err); ++ assert.match(fresh.err, /--gate-owner/); ++ assert.deepEqual(readFileSync(repo.queuePath), queueBefore); ++}); diff --git a/agents/researcher/work/queue-107/test-queue.out b/agents/researcher/work/queue-107/test-queue.out new file mode 100644 index 000000000..64edc92e6 --- /dev/null +++ b/agents/researcher/work/queue-107/test-queue.out @@ -0,0 +1,36 @@ +toolchain: node v26.8.1, git version 2.55.0 + +OK syntax: packages/queue/src/cli.mjs +OK syntax: packages/queue/src/errors.mjs +OK syntax: packages/queue/src/io.mjs +OK syntax: packages/queue/src/lock.mjs +OK syntax: packages/queue/src/queue.mjs +OK syntax: packages/queue/src/review.mjs +OK syntax: packages/queue/src/store.mjs +OK syntax: packages/queue/tests/commit.test.mjs +OK syntax: packages/queue/tests/data.test.mjs +OK syntax: packages/queue/tests/dispatch.test.mjs +OK syntax: packages/queue/tests/gate-owner.test.mjs +OK syntax: packages/queue/tests/helpers.mjs +OK syntax: packages/queue/tests/lock.test.mjs +OK syntax: packages/queue/tests/migration.test.mjs +OK syntax: packages/queue/tests/review.test.mjs +OK syntax: packages/queue/tests/store.test.mjs +OK syntax: packages/queue/tests/write.test.mjs +OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs +OK syntax: packages/queue/tests/fixtures/kill-at.mjs +OK syntax: packages/queue/tests/fixtures/lock-child.mjs +OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh +OK syntax: scripts/queue-commit.sh +OK syntax: scripts/git-hooks/pre-commit +OK syntax: scripts/mosaic +OK queue-commit.sh, the guard and scripts/mosaic are executable +OK packages/queue declares no dependencies +ℹ tests 156 +ℹ pass 156 +ℹ fail 0 +OK node --test packages/queue/tests/ +OK scripts/mosaic queue help +skip queue verify and render --check: this checkout (/mnt/storage/scratch/researcher/q107) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) + +queue suite: 28 passed, 0 failed