From 579c65306c25b75cdc48c370245779201946771a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:36:00 +0800 Subject: [PATCH 1/7] feat(ledger): let a recommended-execution-queue row be corrected, and record three owner decisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The queue was the one table in docs/outstanding-issues.md that no writer could edit. `done` pruned rows and nothing else touched them, so a re-grade recorded against an Open-items row left the queue advertising the old acuity forever — and the queue is what an operator reads to decide what to start. The live instance (#M6JNR8): queue row 1 still presented #231 as A1 "immediate approved live investigation" into a retrieval budget the measurements had already closed. Adds `updateQueueRow` plus a `queue` inbox action (`npm run issues:queue`), so the correction travels through the same immutable-request + serial-reconcile transaction as every other ledger mutation — option (a) of the two the row offered, not the audited hand-edit path the guard exists to refuse. - Acuity, Capability, When, Estimate and Outcome are editable. Order and the ID(s) cell are not: Order is renumbered on every close, so a hand-set value would be discarded, and changing queue membership is an add or a done. - Queue edits fingerprint the QUEUE row (`queueRowFingerprint`), not the Open-items row, so an unrelated detail edit does not invalidate a pending re-grade and a concurrent queue edit does. - `mutationConflicts` namespaces queue targets separately, so re-grading an item and updating its detail row can land in one reconcile batch. Also queues the owner's decisions of 2026-08-22, with their documentation: - #CCZ4HB accepted as-is — CodeRabbit's cap stays, review from it is intermittent. AGENTS.md now records the measurements, that the Codex connector still reviews, and that no required check may be weakened to compensate. - #1K6T35 accepted as-is — PITR stays off, ~24 h recovery point accepted. The remediation plan's unsatisfiable "PITR restore point captured before any mutating phase" rule is replaced with what is actually achievable, scoped to this programme so the reindex plan's PITR stop condition is untouched. - #9X40BT measured: zero preview branches currently exist, so the uncapped branching cost is potential rather than active and nothing needs cleaning up. Co-Authored-By: Claude Opus 5 --- .claude/skills/issues/SKILL.md | 8 + AGENTS.md | 24 ++- data/outstanding-issues-snapshot.json | 32 +++- docs/database-remediation-plan.md | 24 ++- .../337fc2e1-c50c-4f80-b865-58c76af4ff81.json | 11 ++ .../54bbb5e0-a573-47e5-9daa-f7eedc4e6b73.json | 11 ++ .../5bdfac3c-a3b4-4f00-869c-9a59cc756e11.json | 11 ++ docs/outstanding-issues-inbox/README.md | 2 +- .../ae7596a8-9064-4554-9530-4d145a02672c.json | 14 ++ .../fac29c48-c558-4249-97ed-c86e61b61b97.json | 11 ++ docs/scripts-index.md | 2 +- package.json | 1 + scripts/check-outstanding-issues.mjs | 27 ++++ scripts/ledger-inbox.mjs | 137 ++++++++++++++++-- scripts/outstanding-issues.mjs | 133 ++++++++++++++++- 15 files changed, 429 insertions(+), 19 deletions(-) create mode 100644 docs/outstanding-issues-inbox/337fc2e1-c50c-4f80-b865-58c76af4ff81.json create mode 100644 docs/outstanding-issues-inbox/54bbb5e0-a573-47e5-9daa-f7eedc4e6b73.json create mode 100644 docs/outstanding-issues-inbox/5bdfac3c-a3b4-4f00-869c-9a59cc756e11.json create mode 100644 docs/outstanding-issues-inbox/ae7596a8-9064-4554-9530-4d145a02672c.json create mode 100644 docs/outstanding-issues-inbox/fac29c48-c558-4249-97ed-c86e61b61b97.json diff --git a/.claude/skills/issues/SKILL.md b/.claude/skills/issues/SKILL.md index 039da9303..f088ee03b 100644 --- a/.claude/skills/issues/SKILL.md +++ b/.claude/skills/issues/SKILL.md @@ -66,6 +66,13 @@ Parse the intent from natural language too — the exact syntax is a convenience ID and its one-line outcome. Reconciliation moves the row to **Resolved / archive** and updates any recommended-queue references. - **`/issues update `** — queue an immutable `update` request for an open canonical ID. +- **`/issues queue =`** — queue an immutable `queue` request that corrects this + item's **Recommended execution queue** row: `--acuity`, `--capability`, `--when`, `--estimate`, + `--outcome`. Use it whenever a re-grade or a change of timing lands on the Open-items row, because + the queue is what an operator actually reads to decide what to start, and until ledger `#M6JNR8` + it could only be pruned on close — so a stale A1 entry could keep sending sessions at closed work. + Order and the `ID(s)` cell are not editable: Order is renumbered automatically on every close, and + changing membership is an `add` or a `done`, not an edit. - **`/issues capture`** — scan the current session for recommendations, follow-ups, deferrals, and unfixed problems that surfaced but were not recorded. Propose them as a numbered list and add the confirmed ones (dedupe against existing rows first — do not re-add something already tracked). @@ -86,6 +93,7 @@ paragraph; put the smallest next action in **Detail / next action**. npm run issues:add -- --pri P2 --type issue --summary "…" --detail "…" --source "…" npm run issues:done -- '#151' --outcome "Resolved 2026-07-31 by PR #1494. …" npm run issues:update -- '#151' --detail "…" +npm run issues:queue -- '#151' --acuity A3 --when "After the next release" ``` Each command creates one validated UUID JSON file under `docs/outstanding-issues-inbox/`; it does diff --git a/AGENTS.md b/AGENTS.md index e812b2458..b16cfa431 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -924,6 +924,27 @@ shell command with `CLAUDE_ALLOW_PR_FOLLOW=1`, or delete the marker the deny mes Sessions that never create a PR are untouched, so `Run PR` sweeps, `pr-ci-fix` work, and review sessions on someone else's PR still function normally. +## Automated review coverage (owner decision, 2026-08-22) + +CodeRabbit's included allowance is exhausted and the organisation has reached its usage spending +cap, so it declines most reviews with "Review limit reached" — measured on PRs #2113, #2252, #2255, +#2256, #2263 and #2278 between 2026-08-18 and 2026-08-22. The owner has decided to **leave the cap +as it is and accept that CodeRabbit review is intermittent**, rather than raise it (`#CCZ4HB`). + +What that means in practice, and what it does not mean: + +- The ChatGPT Codex connector still reviews and is unaffected by CodeRabbit's cap — it commented on + #2278 in the same run CodeRabbit skipped. Automated review is therefore reduced, not absent. +- Draft PRs are skipped by CodeRabbit outright, so a PR that stays in draft gets nothing from it + even when allowance is available. Undrafting mid-CI also cancels the in-flight run. +- **Do not weaken, skip, or relax any required check to compensate.** The required gates are now + carrying more of the load, not less, and the whole point of accepting reduced bot review is that + the deterministic checks stay strict. +- Clinical-risk and RAG-surface diffs still require their PR-body preflight sections in full; those + are enforced by `scripts/pr-policy.mjs` and are unaffected by review-bot availability. + +Reducing PR churn (below) remains the cheapest way to get more value from the allowance that exists. + ## PR bundling (reduce one-task-one-PR churn) Every `newtask`/`handoff` cycle mints a dedicated `claude/` branch and PR, so a @@ -1020,7 +1041,8 @@ named PR). Future process only. stale head that already shares hot files with the open queue. - The legacy `docs/branch-review-ledger.md` and `docs/outstanding-issues.md` are **serial-only**: normal PRs must not add rows there. `npm run ledger:append` creates an immutable review record; - `npm run issues:add|update|done` creates one immutable inbox request. One fresh-base, + `npm run issues:add|update|queue|done` creates one immutable inbox request (`queue` corrects a + recommended-execution-queue row; see ledger `#M6JNR8`). One fresh-base, cross-worktree-locked `npm run issues:reconcile` operation applies landed requests to the canonical issue ledger. `check:ledger-write-discipline` rejects direct table-row edits, changed request records, deleted requests, and a canonical issue diff that does not exactly diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index b35932a90..c62f1df04 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -10,7 +10,7 @@ "p2": 43, "p3": 33, "queued": 11, - "pending": 32, + "pending": 37, "resolved": 359 }, "queue": [ @@ -883,6 +883,12 @@ "summary": "#99KVDF: Resolved 2026-08-21 by accepting the measured stop decision: do not retry folding AnswerCard support text into VerificationNotice because the 4.5px saving does not remove a row.", "created_at": "2026-08-21" }, + { + "request_id": "337fc2e1-c50c-4f80-b865-58c76af4ff81", + "action": "done", + "summary": "#1K6T35: OWNER DECISION 2026-08-22: leave point-in-time recovery OFF and deliberately accept a worst-case recovery point of roughly 24 hours on the live clinical corpus. This row required either enabling the paid add-on or accepting the RPO and amending the plan's unsatisfiable standing rule; the second was chosen and the rule is amended in docs/database-remediation-plan.md under 'Recovery point — owner decision, 2026-08-22', which now requires confirming and recording the latest daily backup timestamp before a mutating phase, stating what a restore would lose, and having a written tested reversal path for anything that rewrites or drops data or policy objects. Scope is explicitly this remediation programme only: the rag-ingestion-reindex plan's stop-before-production-reindex-when-PITR-disabled condition is untouched.", + "created_at": "2026-08-22" + }, { "request_id": "3552c196-bf4f-49c8-a39b-28f8876d8caf", "action": "done", @@ -925,6 +931,18 @@ "summary": "#CHW9N3: Resolved 2026-08-22: the loading contract retains its separate fourteen-route inventory for consolidated and standalone homes, while a dedicated assertion now requires every entry in exported standaloneModeHomePaths to remain in that inventory; the dashboard root's in-shell pending skeleton stays separately guarded, and documents now has ModeHomeRouteLoading.", "created_at": "2026-08-21" }, + { + "request_id": "54bbb5e0-a573-47e5-9daa-f7eedc4e6b73", + "action": "done", + "summary": "#M6JNR8: Resolved 2026-08-22: scripts/outstanding-issues.mjs gains updateQueueRow and scripts/ledger-inbox.mjs gains a 'queue' action (npm run issues:queue), so a recommended-execution-queue row can be corrected through the same immutable-request + serial-reconcile transaction as every other ledger mutation — option (a) of the two this row offered, not the audited hand-edit path. Acuity, Capability, When, Estimate and Outcome are editable; Order and the ID(s) cell deliberately are not, since Order is renumbered on every close and changing membership is an add or a done. Queue edits carry their own queueRowFingerprint, so they are optimistic against the QUEUE row rather than the Open-items row, and mutationConflicts namespaces them separately so a re-grade and a detail update for the same id can land in one batch. The #231 instance this row was filed for is queued as a 'queue' request in the same PR.", + "created_at": "2026-08-22" + }, + { + "request_id": "5bdfac3c-a3b4-4f00-869c-9a59cc756e11", + "action": "done", + "summary": "#CCZ4HB: OWNER DECISION 2026-08-22: leave the CodeRabbit spending cap as it is and accept that automated review from that bot is intermittent. Option (c) of the three this row offered — not a gate, not a cap raise. Recorded in AGENTS.md 'Automated review coverage (owner decision, 2026-08-22)' with the measurements (skipped on PRs #2113, #2252, #2255, #2256, #2263, #2278), the fact that the ChatGPT Codex connector still reviews and is unaffected, the draft-PR interaction, and the standing prohibition on weakening any required check to compensate. Re-open only if the Codex connector also stops reviewing, or if the owner revisits the billing decision.", + "created_at": "2026-08-22" + }, { "request_id": "65494c5f-927b-4d0d-8564-5933a7c723f3", "action": "update", @@ -979,6 +997,12 @@ "summary": "Cancel request 3552c196-bf4f-49c8-a39b-28f8876d8caf: Duplicate done request created by overlapping command completion; retain 4bc642e9-5fba-4b43-90e8-407fe2c11408.", "created_at": "2026-08-21" }, + { + "request_id": "ae7596a8-9064-4554-9530-4d145a02672c", + "action": "queue", + "summary": "#231: RE-SCOPED 2026-08-22 (was A1 / retrieval-budget investigation). The retrieval-side premise is CLOSED and measured: retrieval costs 955 ms text / 6,720 ms hybrid against a 25,000 ms fast budget (4-27%), so it cannot bind it. What remains is the generation-side R4 residual — chronic ~30 s strong-route provider_timeout on metformin-renal-dosing and valproate-pregnancy, with a safe source-backed extractive fallback already in place — plus the 2026-08-22 Gate E finding that the remaining provider_timeout label cannot be attributed to one mechanism and partly reflects the quality-retry ladder. **Gate:** focused answer-route tests offline first; live probe only with explicit provider approval. **Stop:** do not weaken quality gates to hide timeouts; flag RAG surfaces before edit.", + "created_at": "2026-08-22" + }, { "request_id": "afaa753b-41ae-4bee-8c63-89c649cdcfd5", "action": "done", @@ -1032,6 +1056,12 @@ "action": "cancel", "summary": "Cancel request afaa753b-41ae-4bee-8c63-89c649cdcfd5: Duplicate done request created by overlapping command completion; retain 5201bfa7-4966-4583-abff-02305de82219.", "created_at": "2026-08-21" + }, + { + "request_id": "fac29c48-c558-4249-97ed-c86e61b61b97", + "action": "update", + "summary": "#9X40BT: detail → MEASURED 2026-08-22 (read-only list_branches on sjrfecxgysukkwxsowpy): there are currently ZERO preview branches. The only entry returned is the default 'main' branch, which is the production project itself and is not billable preview compute. So the uncapped exposure this row describes is POTENTIAL, not active — nothing is running to clean up, and no agent-side cleanup step exists to take. The one remaining action is the dashboard setting itself, which no agent can change: Supabase dashboard -> Project Settings -> Integrations -> GitHub -> lower 'Automatic branching' limit from 3 to 1, or disable branching. Prior context stands and is the reason lowering is safe: CI's Migration replay job (db-reset-verify, supabase migration up --local) independently replays the whole chain on every database-touching PR, so preview branches are a second net rather than the only one. STOP unchanged: do not change Supabase project settings without explicit owner approval. PRIOR RECORD: Dashboard read 2026-08-21 (the same read that settled D4) shows Automatic branching ON with limit 3 and 'Supabase changes only' enabled, and the same screen warns that Branching Compute is NOT covered by the organisation's Spend Cap. Preview branches did earn their keep once (the 20260819100200 guard failure on PR #2151 was caught by a preview branch building from the chain alone), so this is a cost/benefit decision, not a cleanup.", + "created_at": "2026-08-22" } ] } diff --git a/docs/database-remediation-plan.md b/docs/database-remediation-plan.md index 7321ea3fe..1d53f529b 100644 --- a/docs/database-remediation-plan.md +++ b/docs/database-remediation-plan.md @@ -36,11 +36,31 @@ against): **Standing rules for every phase.** No hosted mutation without explicit approval for that phase. Never raw-SQL a drift fix — every live change is codified (migration + `schema.sql` mirror + -regenerated `drift-manifest.json`) in the same change. PITR/backup restore point captured before -any mutating phase. Every phase ends with pasted decisive evidence, not exit codes. RAG-protected +regenerated `drift-manifest.json`) in the same change. Every phase ends with pasted decisive +evidence, not exit codes. RAG-protected surfaces (`match_*` RPCs, anything under `src/lib/rag/**`) are flagged before editing and behaviour changes need a live eval-canary pair (36/36, recall 1.0, zero per-case rr regressions). +**Recovery point — owner decision, 2026-08-22 (`#1K6T35`).** This rule used to read "PITR/backup +restore point captured before any mutating phase". It was unsatisfiable: point-in-time recovery is a +paid add-on and is **off** on the live project (`supabase backups list` on 2026-08-19 reported +`pitr_enabled false`, `walg_enabled true`, seven retained daily physical backups). A checklist item +nobody can satisfy reads as met by default whenever nobody checks it, which is exactly what happened +through Phases 1-4. The owner has decided to **leave PITR off and accept a worst case of roughly 24 +hours of data loss** on the clinical corpus, so the rule now states the true position: + +> The only restore point is the most recent daily physical backup, up to ~24 h old. Before any +> mutating phase, confirm the latest backup's completion timestamp and record it, and state in the +> phase's evidence what would be lost by restoring to it. Phases whose every statement has an exact +> one-statement inverse and no data-loss surface (the Phase 4 index work) may proceed on that basis. +> Anything that rewrites or drops data or policy objects must additionally have a written, tested +> reversal path, because there is no restore point finer than the last nightly backup. + +This decision covers **this remediation programme only**. It does not relax the separate +`docs/superpowers/plans/2026-08-20-rag-ingestion-reindex.md` gate, which stops before a production +re-index or backfill when PITR is disabled — that plan mutates the corpus itself and its stop +condition stands until it is revisited on its own terms. + --- ## Phase 0 — Enablement (repo-side, no hosted access; can start immediately) diff --git a/docs/outstanding-issues-inbox/337fc2e1-c50c-4f80-b865-58c76af4ff81.json b/docs/outstanding-issues-inbox/337fc2e1-c50c-4f80-b865-58c76af4ff81.json new file mode 100644 index 000000000..e14cd72b6 --- /dev/null +++ b/docs/outstanding-issues-inbox/337fc2e1-c50c-4f80-b865-58c76af4ff81.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "337fc2e1-c50c-4f80-b865-58c76af4ff81", + "createdOn": "2026-08-22", + "action": "done", + "payload": { + "id": "#1K6T35", + "outcome": "OWNER DECISION 2026-08-22: leave point-in-time recovery OFF and deliberately accept a worst-case recovery point of roughly 24 hours on the live clinical corpus. This row required either enabling the paid add-on or accepting the RPO and amending the plan's unsatisfiable standing rule; the second was chosen and the rule is amended in docs/database-remediation-plan.md under 'Recovery point — owner decision, 2026-08-22', which now requires confirming and recording the latest daily backup timestamp before a mutating phase, stating what a restore would lose, and having a written tested reversal path for anything that rewrites or drops data or policy objects. Scope is explicitly this remediation programme only: the rag-ingestion-reindex plan's stop-before-production-reindex-when-PITR-disabled condition is untouched.", + "baseRowFingerprint": "496215aa7a78b850647da871dcaa360a3fb286bba85e484fa9cb10c3e49d3fe3" + } +} diff --git a/docs/outstanding-issues-inbox/54bbb5e0-a573-47e5-9daa-f7eedc4e6b73.json b/docs/outstanding-issues-inbox/54bbb5e0-a573-47e5-9daa-f7eedc4e6b73.json new file mode 100644 index 000000000..554d12df4 --- /dev/null +++ b/docs/outstanding-issues-inbox/54bbb5e0-a573-47e5-9daa-f7eedc4e6b73.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "54bbb5e0-a573-47e5-9daa-f7eedc4e6b73", + "createdOn": "2026-08-22", + "action": "done", + "payload": { + "id": "#M6JNR8", + "outcome": "Resolved 2026-08-22: scripts/outstanding-issues.mjs gains updateQueueRow and scripts/ledger-inbox.mjs gains a 'queue' action (npm run issues:queue), so a recommended-execution-queue row can be corrected through the same immutable-request + serial-reconcile transaction as every other ledger mutation — option (a) of the two this row offered, not the audited hand-edit path. Acuity, Capability, When, Estimate and Outcome are editable; Order and the ID(s) cell deliberately are not, since Order is renumbered on every close and changing membership is an add or a done. Queue edits carry their own queueRowFingerprint, so they are optimistic against the QUEUE row rather than the Open-items row, and mutationConflicts namespaces them separately so a re-grade and a detail update for the same id can land in one batch. The #231 instance this row was filed for is queued as a 'queue' request in the same PR.", + "baseRowFingerprint": "79e97b657c60f6571e31dab7b2e0e088dd7c34a1dfac53a769532110e1e548ff" + } +} diff --git a/docs/outstanding-issues-inbox/5bdfac3c-a3b4-4f00-869c-9a59cc756e11.json b/docs/outstanding-issues-inbox/5bdfac3c-a3b4-4f00-869c-9a59cc756e11.json new file mode 100644 index 000000000..ee1b11797 --- /dev/null +++ b/docs/outstanding-issues-inbox/5bdfac3c-a3b4-4f00-869c-9a59cc756e11.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "5bdfac3c-a3b4-4f00-869c-9a59cc756e11", + "createdOn": "2026-08-22", + "action": "done", + "payload": { + "id": "#CCZ4HB", + "outcome": "OWNER DECISION 2026-08-22: leave the CodeRabbit spending cap as it is and accept that automated review from that bot is intermittent. Option (c) of the three this row offered — not a gate, not a cap raise. Recorded in AGENTS.md 'Automated review coverage (owner decision, 2026-08-22)' with the measurements (skipped on PRs #2113, #2252, #2255, #2256, #2263, #2278), the fact that the ChatGPT Codex connector still reviews and is unaffected, the draft-PR interaction, and the standing prohibition on weakening any required check to compensate. Re-open only if the Codex connector also stops reviewing, or if the owner revisits the billing decision.", + "baseRowFingerprint": "afc9a2a0680b2801444018a6adaa220b5615b3c157bc8ec9982b75079a20b6c3" + } +} diff --git a/docs/outstanding-issues-inbox/README.md b/docs/outstanding-issues-inbox/README.md index 293ae763b..660c8862c 100644 --- a/docs/outstanding-issues-inbox/README.md +++ b/docs/outstanding-issues-inbox/README.md @@ -1,6 +1,6 @@ # Outstanding-issues inbox -Feature branches never edit `../outstanding-issues.md` directly. Use `npm run issues:add`, `npm run issues:update`, or `npm run issues:done`; each writes one validated JSON request in this directory, so concurrent PRs add different files and merge cleanly. +Feature branches never edit `../outstanding-issues.md` directly. Use `npm run issues:add`, `npm run issues:update`, `npm run issues:queue`, or `npm run issues:done`; each writes one validated JSON request in this directory, so concurrent PRs add different files and merge cleanly. When multiple landed requests mutate the same canonical issue, keep the intended request and add an immutable cancellation request for each rejected mutation: diff --git a/docs/outstanding-issues-inbox/ae7596a8-9064-4554-9530-4d145a02672c.json b/docs/outstanding-issues-inbox/ae7596a8-9064-4554-9530-4d145a02672c.json new file mode 100644 index 000000000..af7d8de16 --- /dev/null +++ b/docs/outstanding-issues-inbox/ae7596a8-9064-4554-9530-4d145a02672c.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "ae7596a8-9064-4554-9530-4d145a02672c", + "createdOn": "2026-08-22", + "action": "queue", + "payload": { + "id": "#231", + "acuity": "A2", + "capability": "Specialist — answer generation + Operator", + "when": "Schedule with the next answer-path work; NOT an immediate live investigation", + "outcome": "RE-SCOPED 2026-08-22 (was A1 / retrieval-budget investigation). The retrieval-side premise is CLOSED and measured: retrieval costs 955 ms text / 6,720 ms hybrid against a 25,000 ms fast budget (4-27%), so it cannot bind it. What remains is the generation-side R4 residual — chronic ~30 s strong-route provider_timeout on metformin-renal-dosing and valproate-pregnancy, with a safe source-backed extractive fallback already in place — plus the 2026-08-22 Gate E finding that the remaining provider_timeout label cannot be attributed to one mechanism and partly reflects the quality-retry ladder. **Gate:** focused answer-route tests offline first; live probe only with explicit provider approval. **Stop:** do not weaken quality gates to hide timeouts; flag RAG surfaces before edit.", + "baseRowFingerprint": "4dcd82f183abb10675b1d47f56271fe76fa6606ca3ea93e072b3ffc6f3f54d9d" + } +} diff --git a/docs/outstanding-issues-inbox/fac29c48-c558-4249-97ed-c86e61b61b97.json b/docs/outstanding-issues-inbox/fac29c48-c558-4249-97ed-c86e61b61b97.json new file mode 100644 index 000000000..47225d694 --- /dev/null +++ b/docs/outstanding-issues-inbox/fac29c48-c558-4249-97ed-c86e61b61b97.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "fac29c48-c558-4249-97ed-c86e61b61b97", + "createdOn": "2026-08-22", + "action": "update", + "payload": { + "id": "#9X40BT", + "detail": "MEASURED 2026-08-22 (read-only list_branches on sjrfecxgysukkwxsowpy): there are currently ZERO preview branches. The only entry returned is the default 'main' branch, which is the production project itself and is not billable preview compute. So the uncapped exposure this row describes is POTENTIAL, not active — nothing is running to clean up, and no agent-side cleanup step exists to take. The one remaining action is the dashboard setting itself, which no agent can change: Supabase dashboard -> Project Settings -> Integrations -> GitHub -> lower 'Automatic branching' limit from 3 to 1, or disable branching. Prior context stands and is the reason lowering is safe: CI's Migration replay job (db-reset-verify, supabase migration up --local) independently replays the whole chain on every database-touching PR, so preview branches are a second net rather than the only one. STOP unchanged: do not change Supabase project settings without explicit owner approval. PRIOR RECORD: Dashboard read 2026-08-21 (the same read that settled D4) shows Automatic branching ON with limit 3 and 'Supabase changes only' enabled, and the same screen warns that Branching Compute is NOT covered by the organisation's Spend Cap. Preview branches did earn their keep once (the 20260819100200 guard failure on PR #2151 was caught by a preview branch building from the chain alone), so this is a cost/benefit decision, not a cleanup.", + "baseRowFingerprint": "711b9f0b05f996932d7a38adfc3782dcce964a7f838f48540c6e8c0771ba8c81" + } +} diff --git a/docs/scripts-index.md b/docs/scripts-index.md index cb83e3ea9..36776e76c 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (262 files) and the `package.json` script surface (266 entries), +Curated map of `scripts/` (262 files) and the `package.json` script surface (267 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/package.json b/package.json index 7450f0e0e..631151fd3 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "issues:add": "node scripts/ledger-inbox.mjs add", "issues:done": "node scripts/ledger-inbox.mjs done", "issues:update": "node scripts/ledger-inbox.mjs update", + "issues:queue": "node scripts/ledger-inbox.mjs queue", "issues:reconcile": "node scripts/ledger-inbox.mjs reconcile", "issues:report": "node scripts/issues-report.mjs", "ledger:lookup": "node scripts/branch-review-ledger.mjs lookup", diff --git a/scripts/check-outstanding-issues.mjs b/scripts/check-outstanding-issues.mjs index d9af1bc08..2c1630b37 100644 --- a/scripts/check-outstanding-issues.mjs +++ b/scripts/check-outstanding-issues.mjs @@ -289,6 +289,33 @@ export function issueRowFingerprint(markdown, issueId) { return createHash("sha256").update(normalized).digest("hex"); } +/** + * Fingerprint the recommended-execution-queue row that cites `issueId`. + * + * The queue is a second table about the same issues, and until ledger #M6JNR8 + * no writer could edit it — only `pruneResolvedIdFromQueue` on close. A queue + * row could therefore contradict its own Open-items row indefinitely, which is + * how `#231` kept presenting as an A1 live investigation after the row itself + * recorded the P1 -> P2 re-grade that closed that cause. + * + * Fingerprinting is what lets a queue edit be optimistic in the same way an + * Open-items edit is: the request records what it read, and reconciliation + * refuses it if the row moved underneath. Returns null when no queue row cites + * the id, and null when more than one does — an ambiguous target must fail + * rather than have a writer guess which row was meant. + */ +export function queueRowFingerprint(markdown, issueId) { + const id = normalizeIssueDisplayId(issueId); + const lines = markdown.split("\n"); + const citations = (parseIssues(markdown).queueCitations ?? []).filter((citation) => citation.id === id); + const uniqueLines = [...new Set(citations.map((citation) => citation.line))]; + if (uniqueLines.length !== 1) return null; + const raw = lines[uniqueLines[0] - 1]; + if (typeof raw !== "string") return null; + const normalized = `| ${cells(raw).join(" | ")} |`; + return createHash("sha256").update(normalized).digest("hex"); +} + export function isValidIssueRowFingerprint(value) { return ISSUE_ROW_FINGERPRINT.test(String(value ?? "")); } diff --git a/scripts/ledger-inbox.mjs b/scripts/ledger-inbox.mjs index 97c18d883..1cf41ba45 100644 --- a/scripts/ledger-inbox.mjs +++ b/scripts/ledger-inbox.mjs @@ -12,19 +12,22 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, s import path from "node:path"; import { fileURLToPath } from "node:url"; -import { addIssue, resolveIssue, updateIssue } from "./outstanding-issues.mjs"; +import { addIssue, resolveIssue, updateIssue, updateQueueRow } from "./outstanding-issues.mjs"; import { ISSUES_PATH, checkIssues, issueRowFingerprint, isValidIssueRowFingerprint, + queueRowFingerprint, } from "./check-outstanding-issues.mjs"; import { isIssueDisplayId, isIssueUlid, issueUlid, issueUlidFromRequest } from "./issue-id.mjs"; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const INBOX_DIR = "docs/outstanding-issues-inbox"; const APPLIED_DIR = path.posix.join(INBOX_DIR, "applied"); -const ACTIONS = new Set(["add", "done", "update", "cancel"]); +const ACTIONS = new Set(["add", "done", "update", "queue", "cancel"]); +// Fields a `queue` request may carry, mirroring updateQueueRow's editable map. +const QUEUE_FIELDS = ["acuity", "capability", "when", "estimate", "outcome"]; const REQUEST_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const RECONCILE_LOCK_NAME = "outstanding-issues-reconcile.lock"; const OWNERLESS_LOCK_GRACE_MS = 5 * 60 * 1000; @@ -54,7 +57,7 @@ export function validateRequest(request) { if (![1, 2].includes(request.version)) problems.push("version must be 1 or 2"); if (!REQUEST_ID.test(request.id ?? "")) problems.push("id must be a UUID"); if (!/^\d{4}-\d{2}-\d{2}$/.test(request.createdOn ?? "")) problems.push("createdOn must be YYYY-MM-DD"); - if (!ACTIONS.has(request.action)) problems.push("action must be add, done, update, or cancel"); + if (!ACTIONS.has(request.action)) problems.push("action must be add, done, update, queue, or cancel"); if (!request.payload || typeof request.payload !== "object") problems.push("payload must be an object"); if (request.action === "add") { for (const field of ["pri", "type", "summary"]) @@ -92,6 +95,20 @@ export function validateRequest(request) { problems.push("update pri must be P1, P2, or P3"); } } + if (request.action === "queue") { + if (!isIssueDisplayId(request.payload?.id)) problems.push("queue requires a canonical issue display id"); + // A queue re-grade is usually exactly one cell (acuity), so requiring any + // one field rather than a prose field keeps the common correction — the + // #M6JNR8 instance — expressible without inventing filler text. + if (!QUEUE_FIELDS.some((field) => request.payload?.[field] !== undefined)) { + problems.push(`queue requires one of ${QUEUE_FIELDS.join(", ")}`); + } + if ( + request.payload?.baseRowFingerprint !== undefined && + !isValidIssueRowFingerprint(request.payload.baseRowFingerprint) + ) + problems.push("queue requires a valid baseRowFingerprint"); + } if (request.action === "cancel") { if (!REQUEST_ID.test(request.payload?.requestId ?? "")) { problems.push("cancel requires a pending request UUID"); @@ -108,6 +125,18 @@ export function applyRequest(markdown, request) { throw new Error("cancel requests must be applied through batch reconciliation"); } const options = { date: request.createdOn }; + if (request.action === "queue" && request.payload?.baseRowFingerprint) { + const id = request.payload.id; + const fingerprint = queueRowFingerprint(markdown, id); + if (!fingerprint) { + throw new Error(`${id} no longer has exactly one queue row; reread and reissue this request from the latest ledger`); + } + if (fingerprint !== String(request.payload.baseRowFingerprint).toLowerCase()) { + throw new Error( + `${id} queue row is stale: it changed after this request was queued; reread and reissue from the latest ledger`, + ); + } + } if ((request.action === "done" || request.action === "update") && request.payload?.baseRowFingerprint) { const id = request.payload.id; const fingerprint = issueRowFingerprint(markdown, id); @@ -125,6 +154,7 @@ export function applyRequest(markdown, request) { return addIssue(markdown, request.payload, { ...options, issueUlid: durableId }); } if (request.action === "done") return resolveIssue(markdown, request.payload.id, request.payload.outcome, options); + if (request.action === "queue") return updateQueueRow(markdown, request.payload.id, request.payload); return updateIssue(markdown, request.payload.id, request.payload); } @@ -132,7 +162,12 @@ function mutationConflicts(requests) { const byIssue = new Map(); for (const request of requests) { if (request.action === "add" || request.action === "cancel") continue; - const id = request.payload.id; + // Keyed by target ROW, not by issue. A `queue` request and an `update` + // request for the same id edit two different tables and cannot clobber each + // other, and re-grading an item usually means doing both in one batch — so + // treating them as a conflict would force an artificial cancel/relanded + // round trip for the ordinary case (ledger #M6JNR8). + const id = `${request.action === "queue" ? "queue " : ""}${request.payload.id}`; const requestIds = byIssue.get(id) ?? []; requestIds.push(request.id); byIssue.set(id, requestIds); @@ -644,19 +679,28 @@ function createRequest(action, argv) { ? { id: argv[1], outcome: argValue(argv, "outcome") } : action === "cancel" ? { requestId: argv[1], reason: argValue(argv, "reason") } - : { + : action === "queue" + ? { + id: argv[1], + acuity: argValue(argv, "acuity"), + capability: argValue(argv, "capability"), + when: argValue(argv, "when"), + estimate: argValue(argv, "estimate"), + outcome: argValue(argv, "outcome"), + } + : { // `pri` rides the same update request as the prose fields so a // demotion and the reason for it land as one auditable mutation. // Without it the CLI could not express a re-prioritisation at all, // which is the half of ledger #313 the inbox would otherwise // reintroduce: updateIssue accepts --pri and validateRequest // permits it, but nothing could produce the payload. - id: argv[1], - pri: argValue(argv, "pri"), - summary: argValue(argv, "summary"), - detail: argValue(argv, "detail"), - source: argValue(argv, "source"), - }; + id: argv[1], + pri: argValue(argv, "pri"), + summary: argValue(argv, "summary"), + detail: argValue(argv, "detail"), + source: argValue(argv, "source"), + }; if (["done", "update"].includes(action) && typeof payload.id === "string") { const currentFingerprint = issueRowFingerprint(readOutstandingIssues(), payload.id); if (currentFingerprint === null) { @@ -664,6 +708,15 @@ function createRequest(action, argv) { } payload.baseRowFingerprint = currentFingerprint; } + if (action === "queue" && typeof payload.id === "string") { + const currentFingerprint = queueRowFingerprint(readOutstandingIssues(), payload.id); + if (currentFingerprint === null) { + throw new Error( + `ledger request rejected: ${payload.id} does not have exactly one recommended-execution-queue row`, + ); + } + payload.baseRowFingerprint = currentFingerprint; + } const request = { version: 2, id: randomUUID(), createdOn: date(), action, payload }; const problems = validateRequest(request); if (problems.length > 0) throw new Error(problems.join("; ")); @@ -911,6 +964,66 @@ function selfTest() { throw new Error("self-test failed: a pri-only update did not reach the Pri cell"); } + // #M6JNR8: a queue re-grade must survive the whole path — validated, carried + // by a request, fingerprinted against the queue row rather than the + // Open-items row, and written to the Acuity cell without disturbing Order or + // the ID(s) linkage. The base fixture's queue is deliberately two columns + // wide, so this needs a fixture shaped like the real seven-column queue. + const queueBase = base.replace( + ["| Order | ID(s) |", "| --- | --- |", "| 1 | `#001` |"].join("\n"), + [ + "| Order | ID(s) | Acuity | Capability | When | Estimate | Outcome |", + "| --- | --- | --- | --- | --- | --- | --- |", + "| 1 | `#001` | A1 | Specialist | Immediate | 2h | investigate |", + ].join("\n"), + ); + const regrade = { + version: 1, + id: "77777777-7777-4777-8777-777777777777", + createdOn: "2026-08-13", + action: "queue", + payload: { + id: "#001", + acuity: "A3", + when: "After the next release", + baseRowFingerprint: queueRowFingerprint(queueBase, "#001"), + }, + }; + if (validateRequest(regrade).length > 0) { + throw new Error("self-test failed: a queue re-grade request must validate"); + } + if (validateRequest({ ...regrade, payload: { id: "#001" } }).length === 0) { + throw new Error("self-test failed: a queue request with no editable field must be rejected"); + } + const regraded = applyRequest(queueBase, regrade); + if (!/\|\s*1\s*\|\s*`#001`\s*\|\s*A3\s*\|/.test(regraded)) { + throw new Error("self-test failed: a queue re-grade did not reach the Acuity cell"); + } + if (!/\|\s*#001\s*\|\s*P2\s*\|/.test(regraded)) { + throw new Error("self-test failed: a queue re-grade must not touch the Open-items row"); + } + // The whole point of the fingerprint is that it tracks the QUEUE row: an edit + // to the Open-items row must not invalidate a queued re-grade, and an edit to + // the queue row must. + let staleQueueRejected = false; + try { + applyRequest(queueBase.replace("| 1 | `#001` | A1 |", "| 1 | `#001` | A2 |"), regrade); + } catch (error) { + staleQueueRejected = /stale/.test(String(error)); + } + if (!staleQueueRejected) throw new Error("self-test failed: stale queue request was not rejected"); + if (!applyRequest(queueBase.replace("| #001 | P2 | issue | one |", "| #001 | P2 | issue | one prime |"), regrade)) { + throw new Error("self-test failed: an unrelated Open-items edit invalidated a queue re-grade"); + } + // A queue edit and an Open-items update for the same id are two rows, so + // reconciling both in one batch must not demand a cancellation decision. + if (mutationConflicts([regrade, update]).length > 0) { + throw new Error("self-test failed: a queue edit and an open-row update were treated as conflicting"); + } + if (mutationConflicts([regrade, { ...regrade, id: "88888888-8888-4888-8888-888888888888" }]).length !== 1) { + throw new Error("self-test failed: two queue edits for one id must conflict"); + } + const added = applyRequest(base, add); const replayed = applyRequest(base, add); const legacyUlid = issueUlidFromRequest(add.createdOn, add.id); @@ -1064,7 +1177,7 @@ function main() { ); return; } - throw new Error("usage: ledger-inbox.mjs [args]"); + throw new Error("usage: ledger-inbox.mjs [args]"); } catch (error) { console.error(`ledger inbox: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; diff --git a/scripts/outstanding-issues.mjs b/scripts/outstanding-issues.mjs index 159a9d6f1..f81774a00 100644 --- a/scripts/outstanding-issues.mjs +++ b/scripts/outstanding-issues.mjs @@ -30,6 +30,7 @@ // node scripts/outstanding-issues.mjs done '#151' --outcome "Resolved ..." // node scripts/outstanding-issues.mjs update '#151' --detail "..." // node scripts/outstanding-issues.mjs update '#151' --pri P3 +// node scripts/outstanding-issues.mjs queue '#151' --acuity A2 --when "..." // node scripts/outstanding-issues.mjs --self-test // // `update --pri` exists because re-prioritising is the mutation triage performs @@ -38,6 +39,12 @@ // that. Deliberately per-row: there is no bulk re-prioritise mode, because a // sweep that moves many rows at once is the kind of change that should be // visible row by row in review. +// +// `queue` closes the same gap one table over (ledger #M6JNR8). The recommended +// execution queue could only be pruned on close, never corrected, so a re-grade +// recorded against an Open-items row left the queue advertising the old acuity +// and timing forever — and the queue is what an operator reads to decide what to +// start, so a stale row there is the one that actually misdirects work. import { readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; @@ -50,6 +57,9 @@ const OPEN_CELLS = 7; // ID | Pri | Type | Summary | Detail / next action | Sour const ARCHIVE_CELLS = 5; // ID | Type | Summary | Outcome | Resolved const PRIORITIES = new Set(["P1", "P2", "P3"]); const TYPES = new Set(["task", "issue", "rec"]); +// Queue row is Order | ID(s) | Acuity | Capability | When | Estimate | Outcome. +// Order and ID(s) are deliberately absent — see updateQueueRow. +const QUEUE_EDITABLE = { acuity: 2, capability: 3, when: 4, estimate: 5, outcome: 6 }; /** * Make one cell safe to place in a markdown table. @@ -278,6 +288,61 @@ export function updateIssue(markdown, id, fields) { }); } +/** + * Edit the recommended-execution-queue row that cites `id` (ledger #M6JNR8). + * + * The queue owns recommended order, acuity, capability, timing and approvals, + * and it was the one table in this ledger no writer could correct. `done` + * pruned rows and nothing else touched them, so a re-grade recorded against an + * Open-items row left the queue asserting the old acuity indefinitely — the + * `#231` instance kept sending sessions at an "Immediate approved live + * investigation" for a cause the ledger had already measured closed. + * + * Deliberately NOT editable: Order and the ID(s) cell. Order is derived — it is + * renumbered 1..N whenever a row is pruned, so a hand-set value would be + * silently overwritten by the next close. The ID(s) cell is the linkage + * `checkIssues` validates against Open items; changing queue membership is an + * add or a close, not an edit. Re-ordering the queue therefore stays out of + * scope and remains a separate, visible decision. + */ +export function updateQueueRow(markdown, id, fields) { + const requested = Object.keys(QUEUE_EDITABLE).filter((key) => fields[key] !== undefined); + if (requested.length === 0) { + const flags = Object.keys(QUEUE_EDITABLE) + .map((key) => `--${key}`) + .join(", "); + throw new Error(`pass at least one of ${flags}`); + } + + return guarded(markdown, (current) => { + const parsed = parseIssues(current); + const cited = (parsed.queueCitations ?? []).filter((citation) => citation.id === String(id)); + const lineNumbers = [...new Set(cited.map((citation) => citation.line))]; + if (lineNumbers.length === 0) { + throw new Error(`${id} has no recommended-execution-queue row; the queue carries only scheduled work`); + } + if (lineNumbers.length > 1) { + throw new Error( + `${id} is cited by ${lineNumbers.length} queue rows (lines ${lineNumbers.join(", ")}); refusing to guess which`, + ); + } + + const lines = current.split("\n"); + const cells = splitCells(lines[lineNumbers[0] - 1]); + for (const key of requested) { + const column = QUEUE_EDITABLE[key]; + if (column >= cells.length) { + throw new Error( + `the queue row for ${id} has ${cells.length} cells, so --${key} (column ${column + 1}) has nowhere to go`, + ); + } + cells[column] = escapeCell(fields[key]); + } + lines[lineNumbers[0] - 1] = buildRow(cells); + return lines.join("\n"); + }); +} + function argValue(argv, name) { const index = argv.indexOf(`--${name}`); return index >= 0 ? argv[index + 1] : undefined; @@ -535,6 +600,64 @@ function selfTest() { rejects("lowercase update priority", () => updateIssue(fixture, "#006", { pri: "p1" })); rejects("archived row cannot be re-prioritised", () => updateIssue(resolved, "#005", { pri: "P1" })); + // queue (#M6JNR8): the recommended-execution-queue row is editable, and the + // Order and ID(s) cells are not. Order is renumbered on every close, so a + // writer that could set it would be writing a value the next close discards. + const queueLine = (markdown, id) => { + const parsed = parseIssues(markdown); + const line = [...new Set((parsed.queueCitations ?? []).filter((c) => c.id === id).map((c) => c.line))][0]; + return splitCells(markdown.split("\n")[line - 1]); + }; + const regraded = updateQueueRow(fixture, "#005", { acuity: "A3", when: "after the audit" }); + const regradedCells = queueLine(regraded, "#005"); + check("queue writes the Acuity cell", regradedCells[2] === "A3"); + check("queue writes the When cell", regradedCells[4] === "after the audit"); + check( + "queue leaves Order, ID(s) and untouched columns alone", + regradedCells[0] === "1" && + regradedCells[1] === "`#005`" && + regradedCells[3] === "High" && + regradedCells[5] === "1h" && + regradedCells[6] === "solo", + ); + check("queue does not touch the Open-items row", splitCells(parseIssues(regraded).rows.find((r) => r.id === "#005").raw)[1] === "P2"); + + // A composite ID(s) cell is one row shared by two issues; addressing it by + // either cited id must reach the same row rather than only the first. + const compositeByFirst = queueLine(updateQueueRow(fixture, "#013", { estimate: "2d" }), "#013"); + const compositeBySecond = queueLine(updateQueueRow(fixture, "#016", { estimate: "2d" }), "#016"); + check("queue reaches a composite row by its first id", compositeByFirst[5] === "2d"); + check("queue reaches a composite row by its second id", compositeBySecond[5] === "2d"); + check("queue keeps the composite ID(s) cell intact", compositeBySecond[1] === "`#013`, `#016`"); + + // Escaping is the same hazard as everywhere else in this file: a bare pipe in + // prose would silently become a column boundary. + check( + "queue escapes pipes in new text", + updateQueueRow(fixture, "#005", { outcome: "a | b" }).includes("a \\| b"), + ); + + // Assert the MESSAGE, not merely that something threw. Without the explicit + // "no queue row" guard the lookup still fails — on a NaN index, several frames + // later, with a message about splitting undefined — so a bare rejects() here + // passes whether or not the guard exists and pins nothing. + const rejectsWith = (label, pattern, run) => { + try { + run(); + failures.push(`${label} should have thrown`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!pattern.test(message)) failures.push(`${label} threw the wrong error: ${message}`); + } + }; + rejectsWith("queue with no editable field", /--acuity/, () => updateQueueRow(fixture, "#005", {})); + rejectsWith("queue for an id with no queue row", /no recommended-execution-queue row/, () => + updateQueueRow(fixture, "#008", { acuity: "A1" }), + ); + // Order is not in the editable map, so passing it alone is "no editable field" + // rather than a silent no-op write — and the queue row must be untouched. + rejectsWith("queue refuses to set Order", /--acuity/, () => updateQueueRow(fixture, "#005", { order: "9" })); + if (failures.length > 0) { console.error("outstanding-issues writer self-test FAILED:"); for (const failure of failures) console.error(` - ${failure}`); @@ -574,8 +697,16 @@ function main() { detail: argValue(argv, "detail"), source: argValue(argv, "source"), }); + } else if (command === "queue") { + next = updateQueueRow(markdown, positional, { + acuity: argValue(argv, "acuity"), + capability: argValue(argv, "capability"), + when: argValue(argv, "when"), + estimate: argValue(argv, "estimate"), + outcome: argValue(argv, "outcome"), + }); } else { - console.error("usage: outstanding-issues.mjs [id] [--flags] (see file header)"); + console.error("usage: outstanding-issues.mjs [id] [--flags] (see file header)"); process.exitCode = 1; return; } From 5003cb517c2bbd707c29b5520fbc04f6187c7462 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:46:26 +0800 Subject: [PATCH 2/7] fix(ledger): cross-reference the measured #CCZ4HB analysis and split its zero-cost levers out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A separate session had already measured this issue in docs/decisions/ccz4hb-review-coverage.md (branch claude/preserve-review-coverage-docs, local and unpushed). Its finding changes what the closure means: the binding constraint is volume, not price — CodeRabbit's included reviews refill at 1/hour (24/day) against 25.4 PRs merged per day, and 24% of merged PRs changed only documentation while 16% changed only the repo's own record-keeping files. The owner's decision to leave the spending cap alone stands and is unchanged. But it decided the billing question only, so the two levers that cost nothing — skip documentation-only PRs, stop opening bookkeeping-only PRs — now live on their own row rather than being archived along with the cap decision. AGENTS.md points at the analysis before anyone acts on the accepted position, and no longer claims the Codex connector is simply unaffected: it reviewed #2278 on 2026-08-22, and it reported its own usage limit on #2113 on 2026-08-18. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 19 +++++++++++++++---- data/outstanding-issues-snapshot.json | 8 +++++++- .../e0508db3-fdbc-4d76-9d6a-5e592b77d198.json | 14 ++++++++++++++ scripts/ledger-inbox.mjs | 16 +++++++++------- scripts/outstanding-issues.mjs | 10 +++++----- 5 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 docs/outstanding-issues-inbox/e0508db3-fdbc-4d76-9d6a-5e592b77d198.json diff --git a/AGENTS.md b/AGENTS.md index b16cfa431..c3d3d3c66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -931,10 +931,21 @@ cap, so it declines most reviews with "Review limit reached" — measured on PRs #2256, #2263 and #2278 between 2026-08-18 and 2026-08-22. The owner has decided to **leave the cap as it is and accept that CodeRabbit review is intermittent**, rather than raise it (`#CCZ4HB`). -What that means in practice, and what it does not mean: - -- The ChatGPT Codex connector still reviews and is unaffected by CodeRabbit's cap — it commented on - #2278 in the same run CodeRabbit skipped. Automated review is therefore reduced, not absent. +**Read `docs/decisions/ccz4hb-review-coverage.md` before acting on this.** A separate analysis +written the same day measured the cause and it is not primarily billing: CodeRabbit's included +reviews refill at one per hour (24/day) while this repo merged 25.4 PRs/day on average over the +preceding month. It also found that 285 of the last 1,190 merged PRs (24%) changed only +documentation and 190 (16%) changed only the repo's own record-keeping files — roughly six review +credits a day spent on files a code-review bot has nothing to say about. Two levers therefore remain +open at **no cost and no loss of safety**, and the owner's decision above does not foreclose them: +configure CodeRabbit to skip documentation-only PRs, and stop opening PRs whose only content is a +bookkeeping record. Do those before anyone proposes raising the cap again. + +What the decision means in practice, and what it does not mean: + +- The ChatGPT Codex connector reviewed PR #2278 on 2026-08-22 in the same run CodeRabbit skipped, so + as of that date automated review is reduced rather than absent. Do not treat that as permanent — + the connector reported its own usage limit on PR #2113 on 2026-08-18, so it can lapse too. - Draft PRs are skipped by CodeRabbit outright, so a PR that stays in draft gets nothing from it even when allowance is available. Undrafting mid-CI also cancels the in-flight run. - **Do not weaken, skip, or relax any required check to compensate.** The required gates are now diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index c62f1df04..e4b3e3c24 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -10,7 +10,7 @@ "p2": 43, "p3": 33, "queued": 11, - "pending": 37, + "pending": 38, "resolved": 359 }, "queue": [ @@ -1039,6 +1039,12 @@ "summary": "#99KVDF: Resolved 2026-08-21 by accepting the measured stop decision: do not retry folding AnswerCard support text into VerificationNotice because the 4.5px saving does not remove a row.", "created_at": "2026-08-21" }, + { + "request_id": "e0508db3-fdbc-4d76-9d6a-5e592b77d198", + "action": "add", + "summary": "Two zero-cost levers to restore automated review coverage remain unbuilt", + "created_at": "2026-08-22" + }, { "request_id": "eeac3580-2340-415d-824f-e31c23d8e2f7", "action": "done", diff --git a/docs/outstanding-issues-inbox/e0508db3-fdbc-4d76-9d6a-5e592b77d198.json b/docs/outstanding-issues-inbox/e0508db3-fdbc-4d76-9d6a-5e592b77d198.json new file mode 100644 index 000000000..1f40e25ab --- /dev/null +++ b/docs/outstanding-issues-inbox/e0508db3-fdbc-4d76-9d6a-5e592b77d198.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "e0508db3-fdbc-4d76-9d6a-5e592b77d198", + "createdOn": "2026-08-22", + "action": "add", + "payload": { + "pri": "P2", + "type": "task", + "summary": "Two zero-cost levers to restore automated review coverage remain unbuilt", + "detail": "SPLIT OUT 2026-08-22 from #CCZ4HB, which the owner closed by accepting intermittent CodeRabbit review. The closure decided the BILLING question only; it did not decide these two, which cost nothing and remove no safety, and docs/decisions/ccz4hb-review-coverage.md measures why they matter more than the cap does. That analysis found the binding constraint is volume, not price: CodeRabbit's included reviews refill at 1/hour (24/day) while this repo merged 25.4 PRs/day on average over the preceding month, with single days as high as 67. It also measured that 285 of the last 1190 merged PRs (24%) changed only documentation and 190 (16%) changed only the repo's own record-keeping files (review ledger, outstanding-issues) -- roughly six review credits a day spent on files a code-review bot has nothing useful to say about. LEVER 1: configure CodeRabbit to skip documentation-only PRs; recovers about a quarter of the budget immediately and touches no required check. LEVER 2: stop opening PRs whose only content is a bookkeeping record -- AGENTS.md 'PR bundling' already says to fold those in and is plainly not being followed, and this repo has learned before (.claude/hooks/pr-handoff-stop.sh header) that a prose rule does not hold where a blocked command does. DO NOT build the third idea that analysis rejected: a gate refusing a new branch when an open PR covers the same ground, because 87% of PRs touch a file another PR touched within a day, so it would block roughly seven in ten legitimate branches. Next: implement lever 1 (owner has to make the CodeRabbit config change or approve a committed .coderabbit.yaml), then lever 2 as a push/PR-creation guard rather than prose.", + "source": "docs/decisions/ccz4hb-review-coverage.md (branch claude/preserve-review-coverage-docs, local and unpushed as of 2026-08-22); #CCZ4HB closure decision 2026-08-22", + "issueUlid": "01M0MMGFRKKZJD4QXBPQR2WX6A" + } +} diff --git a/scripts/ledger-inbox.mjs b/scripts/ledger-inbox.mjs index 1cf41ba45..d3b5e766d 100644 --- a/scripts/ledger-inbox.mjs +++ b/scripts/ledger-inbox.mjs @@ -129,7 +129,9 @@ export function applyRequest(markdown, request) { const id = request.payload.id; const fingerprint = queueRowFingerprint(markdown, id); if (!fingerprint) { - throw new Error(`${id} no longer has exactly one queue row; reread and reissue this request from the latest ledger`); + throw new Error( + `${id} no longer has exactly one queue row; reread and reissue this request from the latest ledger`, + ); } if (fingerprint !== String(request.payload.baseRowFingerprint).toLowerCase()) { throw new Error( @@ -689,12 +691,12 @@ function createRequest(action, argv) { outcome: argValue(argv, "outcome"), } : { - // `pri` rides the same update request as the prose fields so a - // demotion and the reason for it land as one auditable mutation. - // Without it the CLI could not express a re-prioritisation at all, - // which is the half of ledger #313 the inbox would otherwise - // reintroduce: updateIssue accepts --pri and validateRequest - // permits it, but nothing could produce the payload. + // `pri` rides the same update request as the prose fields so a + // demotion and the reason for it land as one auditable mutation. + // Without it the CLI could not express a re-prioritisation at all, + // which is the half of ledger #313 the inbox would otherwise + // reintroduce: updateIssue accepts --pri and validateRequest + // permits it, but nothing could produce the payload. id: argv[1], pri: argValue(argv, "pri"), summary: argValue(argv, "summary"), diff --git a/scripts/outstanding-issues.mjs b/scripts/outstanding-issues.mjs index f81774a00..e01b2fa85 100644 --- a/scripts/outstanding-issues.mjs +++ b/scripts/outstanding-issues.mjs @@ -620,7 +620,10 @@ function selfTest() { regradedCells[5] === "1h" && regradedCells[6] === "solo", ); - check("queue does not touch the Open-items row", splitCells(parseIssues(regraded).rows.find((r) => r.id === "#005").raw)[1] === "P2"); + check( + "queue does not touch the Open-items row", + splitCells(parseIssues(regraded).rows.find((r) => r.id === "#005").raw)[1] === "P2", + ); // A composite ID(s) cell is one row shared by two issues; addressing it by // either cited id must reach the same row rather than only the first. @@ -632,10 +635,7 @@ function selfTest() { // Escaping is the same hazard as everywhere else in this file: a bare pipe in // prose would silently become a column boundary. - check( - "queue escapes pipes in new text", - updateQueueRow(fixture, "#005", { outcome: "a | b" }).includes("a \\| b"), - ); + check("queue escapes pipes in new text", updateQueueRow(fixture, "#005", { outcome: "a | b" }).includes("a \\| b")); // Assert the MESSAGE, not merely that something threw. Without the explicit // "no queue row" guard the lookup still fails — on a NaN index, several frames From 2f334f5a567e794f3cfcdec2513c23ca9e0ec021 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:59:16 +0800 Subject: [PATCH 3/7] fix(ledger): resolve the #M6JNR8 mutation collision with another session's re-grade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs:check-links` projects the ledger through applyRequestBatch, and that refused the batch: two pending mutations targeted #M6JNR8 — this PR's closure and request 993f7ca7 from a second session, which had independently re-proven the same defect (a probe commit changing queue row 1 from A1 to A2 was refused by check:ledger-write-discipline) and re-graded the row P2 -> P1 so it would be fixed sooner. The fix has now landed, so the row closes rather than being re-prioritised. Both requests are cancelled with reasons and the closure is reissued carrying that session's evidence verbatim, because requests are immutable and folding evidence in means replacing rather than editing. The re-grade is superseded, not rejected — it was correct, and it is the reason this was built. Also worth recording for the next reader: `npm run verify:pr-local` reported `failed: docs:check-links (exit 1)` in its own summary and then exited 0. The wrapper's exit code cannot be trusted; read the summary block. Co-Authored-By: Claude Opus 5 --- data/outstanding-issues-snapshot.json | 20 ++++++++++++++++++- .../8db683bd-8be5-4f86-912a-bcf40eb0404c.json | 10 ++++++++++ .../acc8b0f7-b363-4888-9eb8-1712f48e2779.json | 10 ++++++++++ .../f046ea4a-214f-4917-8740-4f56565fee0b.json | 11 ++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 docs/outstanding-issues-inbox/8db683bd-8be5-4f86-912a-bcf40eb0404c.json create mode 100644 docs/outstanding-issues-inbox/acc8b0f7-b363-4888-9eb8-1712f48e2779.json create mode 100644 docs/outstanding-issues-inbox/f046ea4a-214f-4917-8740-4f56565fee0b.json diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index e4b3e3c24..25edd92b8 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -10,7 +10,7 @@ "p2": 43, "p3": 33, "queued": 11, - "pending": 38, + "pending": 41, "resolved": 359 }, "queue": [ @@ -979,6 +979,12 @@ "summary": "No gate can detect a React Server Component boundary violation: a component with an event handler and no \"use client\" throws only at request time", "created_at": "2026-08-21" }, + { + "request_id": "8db683bd-8be5-4f86-912a-bcf40eb0404c", + "action": "cancel", + "summary": "Cancel request 54bbb5e0-a573-47e5-9daa-f7eedc4e6b73: Reissued as a single closure that also carries the evidence from cancelled request 993f7ca7, which had independently re-proven the defect in another session. Requests are immutable, so folding that evidence into the outcome means replacing this one rather than editing it.", + "created_at": "2026-08-22" + }, { "request_id": "993f7ca7-5277-4399-82fc-19ff981823e4", "action": "update", @@ -997,6 +1003,12 @@ "summary": "Cancel request 3552c196-bf4f-49c8-a39b-28f8876d8caf: Duplicate done request created by overlapping command completion; retain 4bc642e9-5fba-4b43-90e8-407fe2c11408.", "created_at": "2026-08-21" }, + { + "request_id": "acc8b0f7-b363-4888-9eb8-1712f48e2779", + "action": "cancel", + "summary": "Cancel request 993f7ca7-5277-4399-82fc-19ff981823e4: Superseded: this request re-grades #M6JNR8 to P1 so the queue-edit gap gets fixed sooner, and the fix has now landed in this PR (updateQueueRow plus the 'queue' inbox action), so the row is being closed rather than re-prioritised. Nothing from it is lost: its empirical proof — that a probe commit changing queue row 1 from A1 to A2 was refused by check:ledger-write-discipline with 'does not exactly match the audited application of 0 moved inbox request(s) from the base', and that the stale row had by then mis-directed two sessions — is carried verbatim into the closure outcome, which is the durable record. Cancelled only because two mutations on one row need an explicit decision, not because the finding was wrong; it was correct and it is the reason this was built.", + "created_at": "2026-08-22" + }, { "request_id": "ae7596a8-9064-4554-9530-4d145a02672c", "action": "queue", @@ -1051,6 +1063,12 @@ "summary": "#0HFDWD: Resolved 2026-08-21: CI and PR-policy UI classifiers include the narrow mode, route, copy, navigation, and therapies library owners; self-tests pin app-modes and route ownership as UI.", "created_at": "2026-08-21" }, + { + "request_id": "f046ea4a-214f-4917-8740-4f56565fee0b", + "action": "done", + "summary": "#M6JNR8: Resolved 2026-08-22: scripts/outstanding-issues.mjs gains updateQueueRow and scripts/ledger-inbox.mjs gains a 'queue' action (npm run issues:queue), so a recommended-execution-queue row can be corrected through the same immutable-request + serial-reconcile transaction as every other ledger mutation — option (a) of the two this row offered, not the audited hand-edit path. Acuity, Capability, When, Estimate and Outcome are editable; Order and the ID(s) cell deliberately are not, since Order is renumbered on every close and changing queue membership is an add or a done. Queue edits carry their own queueRowFingerprint, so they are optimistic against the QUEUE row rather than the Open-items row, and mutationConflicts namespaces queue targets separately so a re-grade and a detail update for the same id can land in one batch. Both writers gained mutation-tested self-tests. EVIDENCE CARRIED FROM CANCELLED REQUEST 993f7ca7 (a second session, 2026-08-22, independently tasked with fixing the #231 queue row and unable to): a probe commit changing queue row 1 from A1 to A2 was refused by npm run check:ledger-write-discipline with 'docs/outstanding-issues.md does not exactly match the audited application of 0 moved inbox request(s) from the base', and the probe was reverted; that session re-graded this row P2 -> P1 on the grounds that the stale queue row is the first thing the session-start hook surfaces and had by then mis-directed two sessions. That re-grade is superseded by this closure, not rejected. The #231 instance is corrected by a 'queue' request in the same PR.", + "created_at": "2026-08-22" + }, { "request_id": "f7a561a7-1fe2-4aaa-bbfc-90d24baecc7a", "action": "add", diff --git a/docs/outstanding-issues-inbox/8db683bd-8be5-4f86-912a-bcf40eb0404c.json b/docs/outstanding-issues-inbox/8db683bd-8be5-4f86-912a-bcf40eb0404c.json new file mode 100644 index 000000000..ef80a132e --- /dev/null +++ b/docs/outstanding-issues-inbox/8db683bd-8be5-4f86-912a-bcf40eb0404c.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "8db683bd-8be5-4f86-912a-bcf40eb0404c", + "createdOn": "2026-08-22", + "action": "cancel", + "payload": { + "requestId": "54bbb5e0-a573-47e5-9daa-f7eedc4e6b73", + "reason": "Reissued as a single closure that also carries the evidence from cancelled request 993f7ca7, which had independently re-proven the defect in another session. Requests are immutable, so folding that evidence into the outcome means replacing this one rather than editing it." + } +} diff --git a/docs/outstanding-issues-inbox/acc8b0f7-b363-4888-9eb8-1712f48e2779.json b/docs/outstanding-issues-inbox/acc8b0f7-b363-4888-9eb8-1712f48e2779.json new file mode 100644 index 000000000..efa1a8e8d --- /dev/null +++ b/docs/outstanding-issues-inbox/acc8b0f7-b363-4888-9eb8-1712f48e2779.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "acc8b0f7-b363-4888-9eb8-1712f48e2779", + "createdOn": "2026-08-22", + "action": "cancel", + "payload": { + "requestId": "993f7ca7-5277-4399-82fc-19ff981823e4", + "reason": "Superseded: this request re-grades #M6JNR8 to P1 so the queue-edit gap gets fixed sooner, and the fix has now landed in this PR (updateQueueRow plus the 'queue' inbox action), so the row is being closed rather than re-prioritised. Nothing from it is lost: its empirical proof — that a probe commit changing queue row 1 from A1 to A2 was refused by check:ledger-write-discipline with 'does not exactly match the audited application of 0 moved inbox request(s) from the base', and that the stale row had by then mis-directed two sessions — is carried verbatim into the closure outcome, which is the durable record. Cancelled only because two mutations on one row need an explicit decision, not because the finding was wrong; it was correct and it is the reason this was built." + } +} diff --git a/docs/outstanding-issues-inbox/f046ea4a-214f-4917-8740-4f56565fee0b.json b/docs/outstanding-issues-inbox/f046ea4a-214f-4917-8740-4f56565fee0b.json new file mode 100644 index 000000000..6a62d314f --- /dev/null +++ b/docs/outstanding-issues-inbox/f046ea4a-214f-4917-8740-4f56565fee0b.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "f046ea4a-214f-4917-8740-4f56565fee0b", + "createdOn": "2026-08-22", + "action": "done", + "payload": { + "id": "#M6JNR8", + "outcome": "Resolved 2026-08-22: scripts/outstanding-issues.mjs gains updateQueueRow and scripts/ledger-inbox.mjs gains a 'queue' action (npm run issues:queue), so a recommended-execution-queue row can be corrected through the same immutable-request + serial-reconcile transaction as every other ledger mutation — option (a) of the two this row offered, not the audited hand-edit path. Acuity, Capability, When, Estimate and Outcome are editable; Order and the ID(s) cell deliberately are not, since Order is renumbered on every close and changing queue membership is an add or a done. Queue edits carry their own queueRowFingerprint, so they are optimistic against the QUEUE row rather than the Open-items row, and mutationConflicts namespaces queue targets separately so a re-grade and a detail update for the same id can land in one batch. Both writers gained mutation-tested self-tests. EVIDENCE CARRIED FROM CANCELLED REQUEST 993f7ca7 (a second session, 2026-08-22, independently tasked with fixing the #231 queue row and unable to): a probe commit changing queue row 1 from A1 to A2 was refused by npm run check:ledger-write-discipline with 'docs/outstanding-issues.md does not exactly match the audited application of 0 moved inbox request(s) from the base', and the probe was reverted; that session re-graded this row P2 -> P1 on the grounds that the stale queue row is the first thing the session-start hook surfaces and had by then mis-directed two sessions. That re-grade is superseded by this closure, not rejected. The #231 instance is corrected by a 'queue' request in the same PR.", + "baseRowFingerprint": "79e97b657c60f6571e31dab7b2e0e088dd7c34a1dfac53a769532110e1e548ff" + } +} From 98fed3f62892fff55e36ced2c61cfa3ab3ba6a15 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:55:56 +0800 Subject: [PATCH 4/7] fix(ledger): guard queue and closure conflicts --- scripts/ledger-inbox.mjs | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/scripts/ledger-inbox.mjs b/scripts/ledger-inbox.mjs index d3b5e766d..bc9eeac2c 100644 --- a/scripts/ledger-inbox.mjs +++ b/scripts/ledger-inbox.mjs @@ -20,7 +20,13 @@ import { isValidIssueRowFingerprint, queueRowFingerprint, } from "./check-outstanding-issues.mjs"; -import { isIssueDisplayId, isIssueUlid, issueUlid, issueUlidFromRequest } from "./issue-id.mjs"; +import { + isIssueDisplayId, + isIssueUlid, + issueUlid, + issueUlidFromRequest, + normalizeIssueDisplayId, +} from "./issue-id.mjs"; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const INBOX_DIR = "docs/outstanding-issues-inbox"; @@ -164,15 +170,17 @@ function mutationConflicts(requests) { const byIssue = new Map(); for (const request of requests) { if (request.action === "add" || request.action === "cancel") continue; - // Keyed by target ROW, not by issue. A `queue` request and an `update` - // request for the same id edit two different tables and cannot clobber each - // other, and re-grading an item usually means doing both in one batch — so - // treating them as a conflict would force an artificial cancel/relanded - // round trip for the ordinary case (ledger #M6JNR8). - const id = `${request.action === "queue" ? "queue " : ""}${request.payload.id}`; - const requestIds = byIssue.get(id) ?? []; - requestIds.push(request.id); - byIssue.set(id, requestIds); + const id = normalizeIssueDisplayId(request.payload.id); + // Key by the rows a request actually mutates. `update` changes only the + // Open-items row and `queue` changes only its queue row, so they can safely + // share a batch. `done` moves the Open-items row *and* prunes its queue + // citation, so it conflicts with either action on the same issue. + const targets = request.action === "done" ? [id, `queue ${id}`] : [request.action === "queue" ? `queue ${id}` : id]; + for (const target of targets) { + const requestIds = byIssue.get(target) ?? []; + requestIds.push(request.id); + byIssue.set(target, requestIds); + } } return [...byIssue.entries()].filter(([, requestIds]) => requestIds.length > 1); } @@ -1025,6 +1033,20 @@ function selfTest() { if (mutationConflicts([regrade, { ...regrade, id: "88888888-8888-4888-8888-888888888888" }]).length !== 1) { throw new Error("self-test failed: two queue edits for one id must conflict"); } + for (const requests of [ + [regrade, done], + [done, regrade], + ]) { + let queueClosureRejected = false; + try { + applyRequestBatch(queueBase, requests); + } catch (error) { + queueClosureRejected = /explicit cancellation decision/.test(String(error)); + } + if (!queueClosureRejected) { + throw new Error("self-test failed: a queue edit and closure must require an explicit cancellation decision"); + } + } const added = applyRequest(base, add); const replayed = applyRequest(base, add); From 8fb5c3c3e23440952f736f007a196b51e9173c6f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:55:58 +0800 Subject: [PATCH 5/7] fix(issues): normalize queued issue ids --- scripts/outstanding-issues.mjs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/outstanding-issues.mjs b/scripts/outstanding-issues.mjs index e01b2fa85..ef16ec95a 100644 --- a/scripts/outstanding-issues.mjs +++ b/scripts/outstanding-issues.mjs @@ -51,7 +51,14 @@ import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { ISSUES_PATH, checkIssues, parseIssues } from "./check-outstanding-issues.mjs"; -import { allocateDisplayId, displayIdForUlid, issueIdCell, issueIdCitations, issueUlid } from "./issue-id.mjs"; +import { + allocateDisplayId, + displayIdForUlid, + issueIdCell, + issueIdCitations, + issueUlid, + normalizeIssueDisplayId, +} from "./issue-id.mjs"; const OPEN_CELLS = 7; // ID | Pri | Type | Summary | Detail / next action | Source | Added const ARCHIVE_CELLS = 5; // ID | Type | Summary | Outcome | Resolved @@ -316,14 +323,15 @@ export function updateQueueRow(markdown, id, fields) { return guarded(markdown, (current) => { const parsed = parseIssues(current); - const cited = (parsed.queueCitations ?? []).filter((citation) => citation.id === String(id)); + const normalizedId = normalizeIssueDisplayId(id); + const cited = (parsed.queueCitations ?? []).filter((citation) => citation.id === normalizedId); const lineNumbers = [...new Set(cited.map((citation) => citation.line))]; if (lineNumbers.length === 0) { - throw new Error(`${id} has no recommended-execution-queue row; the queue carries only scheduled work`); + throw new Error(`${normalizedId} has no recommended-execution-queue row; the queue carries only scheduled work`); } if (lineNumbers.length > 1) { throw new Error( - `${id} is cited by ${lineNumbers.length} queue rows (lines ${lineNumbers.join(", ")}); refusing to guess which`, + `${normalizedId} is cited by ${lineNumbers.length} queue rows (lines ${lineNumbers.join(", ")}); refusing to guess which`, ); } From 7f4dd87299663a68575b0746472963039d795ce8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:56:00 +0800 Subject: [PATCH 6/7] test(ledger): cover queue and closure conflicts --- tests/ledger-inbox-cancellation.test.ts | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/ledger-inbox-cancellation.test.ts b/tests/ledger-inbox-cancellation.test.ts index 06948733c..1a8490854 100644 --- a/tests/ledger-inbox-cancellation.test.ts +++ b/tests/ledger-inbox-cancellation.test.ts @@ -17,6 +17,26 @@ function update(id: string, issueId = "#316") { }; } +function done(id: string, issueId = "#316") { + return { + version: 1, + id, + createdOn: "2026-08-14", + action: "done", + payload: { id: issueId, outcome: `resolved ${issueId}` }, + }; +} + +function queue(id: string, issueId = "#316") { + return { + version: 1, + id, + createdOn: "2026-08-14", + action: "queue", + payload: { id: issueId, acuity: "A2" }, + }; +} + function cancel(id: string, requestId: string) { return { version: 1, @@ -103,6 +123,15 @@ describe("ledger inbox cancellation planning", () => { ); }); + it("treats a queue edit and closure as conflicting mutations in either order", () => { + for (const requests of [ + [queue(UPDATE_ID), done(CANCEL_ID)], + [done(UPDATE_ID), queue(CANCEL_ID)], + ]) { + expect(() => plan(requests)).toThrow(/multiple pending mutations require an explicit cancellation decision/); + } + }); + it("an ineffective cancellation does not resolve a competing-mutation conflict", () => { // The cancellation lost its race, so both updates are still active and the // conflict must still be raised rather than silently half-applied. From f6901da484855640f7f4801526ba26536eeb4f40 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:56:01 +0800 Subject: [PATCH 7/7] test(issues): cover normalized queue lookup --- tests/outstanding-issues-writer.test.ts | 36 ++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/outstanding-issues-writer.test.ts b/tests/outstanding-issues-writer.test.ts index 006713478..fb395bcc9 100644 --- a/tests/outstanding-issues-writer.test.ts +++ b/tests/outstanding-issues-writer.test.ts @@ -2,7 +2,14 @@ import { describe, expect, it } from "vitest"; import { issueRowFingerprint, parseIssues } from "../scripts/check-outstanding-issues.mjs"; import { displayIdForUlid, issueUlid } from "../scripts/issue-id.mjs"; -import { addIssue, escapeCell, resolveIssue, splitCells, updateIssue } from "../scripts/outstanding-issues.mjs"; +import { + addIssue, + escapeCell, + resolveIssue, + splitCells, + updateIssue, + updateQueueRow, +} from "../scripts/outstanding-issues.mjs"; const OPEN_CELLS = 7; const ARCHIVE_CELLS = 5; @@ -122,6 +129,33 @@ describe("outstanding-issues writer", () => { expect(row!.raw).toContain("replaced \\| detail"); }); + it("locates a Crockford queue row when a lowercase display id was accepted", () => { + const queueLedger = [ + "# Outstanding", + "", + "## Recommended execution queue", + "", + "| Order | ID(s) | Acuity | Capability | When | Estimate | Outcome |", + "| --- | --- | --- | --- | --- | --- | --- |", + "| 1 | `#ABCDEF` | A1 | Specialist | Immediate | 2h | investigate |", + "", + "## Open items", + "", + "| ID | Pri | Type | Summary | Detail / next action | Source | Added |", + "| --- | --- | --- | --- | --- | --- | --- |", + "| #ABCDEF | P2 | issue | queued | detail | source | 2026-01-01 |", + "", + "## Resolved / archive", + "", + "| ID | Type | Summary | Outcome | Resolved |", + "| --- | --- | --- | --- | --- |", + "", + ].join("\n"); + + const next = updateQueueRow(queueLedger, "#abcdef", { acuity: "A2" }); + expect(next).toContain("| 1 | `#ABCDEF` | A2 | Specialist | Immediate | 2h | investigate |"); + }); + it("extends a colliding display id and preserves it through update and archive", () => { const firstUlid = "0000000000ABCDEF0000000000"; const secondUlid = "0000000000ABCDEF1000000000";