diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index 63092186d..771e76645 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -1,17 +1,17 @@ { "version": "outstanding-issues-snapshot-v1", "ledger_revision": { - "sha": "9a382a0504632186bab075b53633cb69a6f92446", - "committed_at": "2026-08-22T15:25:05+00:00" + "sha": "ab5c69c339baff0191f49ff43fb861756c9dbce3", + "committed_at": "2026-08-22T20:29:29+00:00" }, "counts": { - "open": 66, - "p1": 1, - "p2": 36, - "p3": 29, + "open": 73, + "p1": 0, + "p2": 40, + "p3": 33, "queued": 10, - "pending": 11, - "resolved": 375 + "pending": 1, + "resolved": 376 }, "queue": [ { @@ -338,8 +338,8 @@ "priority": "P3", "type": "task", "summary": "Fleet-wide worktree inventory and safe orphan cleanup remain, but the Dev Drive capacity emergency is resolved", - "detail": "UPDATE 2026-08-21 (later the same day): the scope is wider than .claude/worktrees, and the previous update's 69 is itself a floor. An accidental `npx vitest run` issued from C:/Users/joshs instead of the repo root enumerated 253 copies of just TWO test files across the home directory, i.e. 253 separate checkouts of this repository, spread over at least six distinct roots: .codex/worktrees, .gemini/antigravity/worktrees, .copilot/repos/copilot-worktrees, .local/share/opencode/worktree, Documents/Codex///work, and an AppData/Local/pnpm store projects directory (which also contained its own .worktrees). Temp copies under AppData/Local/Temp/guard-push-format-* were still present too. So every AI tool in the fleet keeps its own worktree root and none of them is covered by a sweep scoped to .claude/worktrees. Most had no node_modules (the run failed them with 'Cannot find package'), so raw disk is likely below 253 x 0.89 GB, but the count is the point: any recursive tool run from the home directory now walks all of them. Re-scope this row from 'nine landed worktrees' to a fleet-wide inventory across all six roots before pruning, and keep the #XCAX01 safety preconditions — the 2026-08-21 data-loss incident is why this is not a one-line delete. PRIOR RECORD: UPDATE 2026-08-21 (measured on this machine): the figure recorded here is badly understated and moving the wrong way. git worktree list reported 69 registered worktrees under .claude/worktrees, not nine. At the ~0.89 GB / 51,735-file node_modules per worktree measured in #6SMMB4, even partial installs put this well beyond the 4.5 GB recorded, on a Dev Drive noted as 51% full. SAFETY, learned the hard way on 2026-08-21: a sweep removed an in-use worktree during this very session and destroyed its uncommitted work - see the separate row filed for that. Re-measure actual on-disk size, then prune, but confirm each worktree is landed, holds no uncommitted or unpushed work, AND is not currently checked out by a live agent session before removing it. UPDATE 2026-08-22: the capacity premise is now false. D: has been expanded to 80 GB total, with 22 GB used and 59 GB available (27% used), so this is no longer a space emergency. The worktree safety/inventory evidence remains relevant: 253 checkouts across at least six roots, and the #XCAX01 preconditions remain mandatory. A same-day .claude/worktrees check found 30 directories: 12 registered to live sessions and 18 unregistered orphans. Sixteen orphans were empty and two held one entry; one empty unregistered directory could not be removed because a live process held it. NEXT: keep this as a low-priority fleet-inventory and safe-orphan-cleanup task. Any future cleanup must cover every root, default to report-only, skip a directory when liveness or cleanliness cannot be proved, and never delete a registered worktree.", - "source": "Accidental home-directory vitest run, 2026-08-21: 253 test-file copies across six worktree roots; Dev Drive / worktree inventory re-measured 2026-08-22", + "detail": "UPDATE 2026-08-22 (fleet inventory COMPLETE; cleanup deliberately DEFERRED by the owner). The inventory this row asked for is done and the 253 figure is superseded by a measured one. Scanning eight roots (.claude/worktrees, D:/Worktrees, .codex/worktrees, .gemini/antigravity/worktrees, .copilot/repos/copilot-worktrees, .local/share/opencode/worktree, Documents/Codex, AppData/Local/Temp) found 208 checkouts of this repository. Of those, 92 are registered git worktrees of D:/Repos/Database and 116 are SEPARATE FULL CLONES with their own object databases (76 under .codex, 26 under .copilot, 13 under Documents/Codex, 1 under Temp). Zero unregistered worktrees and zero stale gitdir pointers, so scripts/clean-worktree.mjs sees every checkout that points at the main repo - and sees NONE of the 116 clones, which are the real disk mass. 54 carry node_modules. TWO DISTINCT POPULATIONS, repeatedly conflated by automated review - read the labels before the numbers. POPULATION A, LEFTOVER DIRECTORIES (bare paths with no .git, never git worktrees, holding no content): 18 sat under .claude/worktrees; 11 were removed (ten holding zero files, plus d4-review-fix which held one orphaned node_modules binary, no source and no .git) and the remaining 7 refused with EPERM because a live process holds them; 18 = 11 + 7. One further leftover under .gemini/antigravity was also removed, so 12 leftover-directory removals in total across both roots, the twelfth being outside .claude/worktrees. NONE of these 12 was restored, because none held any content to restore - they were empty paths. Those 7 blocked directories are NOT registered worktrees and have no .git entry - they are bare empty directories that seven sessions still treat as their cwd, filed as their own row and not to be counted as worktree debris here. npm run clean:worktree --merged --squashed --dry-run identified 23 landed candidates totalling 9.30 GB (C: 4.74 GB across 11, D: 4.56 GB across 12), of which 12 are proven-or-corroborated and 11 are 'NOT fully corroborated' and are skipped by the tool's own safety re-verification. CAPACITY IS NOT THE ISSUE: D: is 80 GB with 53 GB free (35% used). DECISION 2026-08-22: the owner halted the removal mid-run and all 13 removed worktrees were restored. Those 13 are POPULATION B - registered git worktrees removed by the clean:worktree pass, an entirely different set from the 12 empty directories above, which is why 12 and 13 do not reconcile and must not be added or compared. The 13 are: agent-ac295f35b1b90b024, caring-contacts-phase-2a-11b-a37803, developer-hub-phase-1-resume-c4f0c3, ed-care-plans-resume-a0dbfd, permission-prompts-issue-0dfcf3 and ward-flow-phase-3-49f201 under .claude/worktrees; ed-care-plans, fix-hookspath-relative, post-drift-ledger-tidy and worktree-cleanup-guard under D:/Worktrees/Database; plus .codex/worktrees/cloud-effort-defaults/Database, .codex/worktrees/image-auto-retry/Database and .gemini/antigravity/worktrees/Database/automate_pr_merge_sweep. Each was restored and verified individually for correct branch, correct head and clean tree, with dependencies restored by byte-identical copy. Five live chats had had their working directory deleted underneath them. Cleanup is DEFERRED indefinitely - do not resume it without an explicit fresh instruction naming the exact paths. Two process lessons for whoever does: a 'git refused' or EPERM removal means a live process holds that directory and is a STOP, never something to retry on a later pass; and the candidate list must be shown and approved before any deletion, not after.", + "source": "session 2026-08-22, GitHub issue #2270 Part A", "added": "2026-08-18" }, { @@ -468,15 +468,6 @@ "source": "docs/worker-deploy-runbook.md sections 3.2, 3.5 and 3.7; read-only Railway metrics on service worker (project Database 5deaad0b) 2026-08-21", "added": "2026-08-20" }, - { - "id": "#1YPV51", - "priority": "P1", - "type": "task", - "summary": "Reopen #318: the medication interaction lexicon clinical review and sign-off has not actually happened; the lexicon remains clinically unreviewed", - "detail": "PARTIAL PROGRESS 2026-08-22, NOT a clinical sign-off. The sign-off block in docs/medication-interaction-lexicon-review.md is deliberately still empty and the document still reads Status: UNREVIEWED, because a Clinical Lead read of the 37-term sheet has still not happened. What DID happen: the owner reviewed seven specific questions raised from the report and one real defect was found and fixed. FIXED: loperamide was resolving as a member of the opioids term because the selector matches the substring Opioid and the catalogue classifies it Peripheral Opioid Agonist. It is P-gp-restricted at therapeutic doses and does not contribute to the sedation and respiratory-depression rows that term drives (35 of 36 rows CRITICAL or HIGH). It is now deny-listed alongside naltrexone and naloxone, which were already excluded for the same class of reason. Proven, not asserted: regenerating the index with and without the change alters 35 medications, removes loperamide as a counterparty from every one of them, adds nothing anywhere, and leaves resolvedRows/unresolvedRows/rowsWithCatalogueTarget unchanged at 392/133/440. FOUR OTHER CHANGES WERE PROPOSED AND WITHDRAWN after reading the implementation: adding moclobemide to maois, removing atomoxetine from snris, folding the Z-drugs into benzodiazepines, and splitting antihistamines by sedation. All four would have overridden deliberate catalogue classifications (moclobemide is subclass RIMA not MAOI; atomoxetine is subclass SNRI (Non-stimulant); zolpidem and zopiclone are class Sedative subclass Z-Drug) or broken a term's meaning. The sedation concern they were aimed at is carried by the cns-depressants term, which is kind: mechanism and deliberately unenumerable, so it warns on no specific drug by design. TWO QUESTIONS REMAIN FOR THE OWNER, both catalogue-level rather than lexicon-level: (1) should moclobemide's RIMA classification make it fire serotonergic interaction warnings alongside the irreversible MAOIs; (2) is atomoxetine correctly classed SNRI for interaction purposes. STILL OPEN: the full clinical read. Do not close this row without explicit owner confirmation that the review was actually carried out.", - "source": "session 2026-08-21 ledger reconciliation and docs-truth pass; owner confirmed in chat the 2026-08-18 closure was inaccurate", - "added": "2026-08-20" - }, { "id": "#XCAX01", "priority": "P2", @@ -653,8 +644,8 @@ "priority": "P2", "type": "issue", "summary": "Answer-quality predicates accept two incoherent grounded extractive answers; diagnose predicate strictness before changing gate reachability", - "detail": "PACKET 2 IMPLEMENTED 2026-08-22, PR pending. Step 1 falsification CONFIRMED the premise before any production change: generatedAnswerQualityFailureReason returned null for BOTH captured answers at their real eval query classes (medication_dose_risk, document_lookup), run against unmodified source. TWO CORRECTIONS TO THE ROW'S OWN FRAMING, both from reading the code. (1) The gate is NOT unreachable on the grounded first-choice extractive path. rag.ts:3113 is only the review-fallback reason labeller; the ENFORCING call is unconditional inside finalizeRagAnswerQualityCore and the grounded path reaches it via finalizeAnswer -> finalizeRagAnswerQuality. No reachability change was needed or made. The only remaining bypass is the preformatted-and-grounded early return, now named and pinned by a test rather than assumed closed. (2) The wrapper is NOT purely a laundering step. The first predicate version — reject any \"The guidance is that X\" whose continuation lacks a verb from openingSentenceActionPattern — broke tests/rag-extractive-procedural-artifact.test.ts, which pins \"the ECT Coordinator places the patient onto BASE\" as a grounded answer; \"places\" is a real finite verb and rightly absent from a clinical-directive list. SHIPPED: isLaunderedGuidanceWrapperAnswer rejects only two shapes no verb list could rescue — a \">\" breadcrumb pointing at a word rather than a figure, and a bare coordinated noun list with no determiner, auxiliary or directive verb — returning reason guidance_wrapper_fragment. Placed after the other prose gates so nothing already-rejected is relabelled; not in the shouldPreserveSourceBackedGeneratedAnswer rescue allowlist, so a flagged answer cannot be recovered to grounded. clippedClinicalFragmentPattern's four-continuation enumeration is left in place and its cases are still rejected. EVAL IMPACT: eval:rag:offline 627/627 and eval:rag:adversarial:offline 25/25, identical before and after (baseline re-run with GATE_RECEIPTS=refresh). source_backed_review_fallback_count cannot be measured offline; threshold is ragSourceBackedReviewFallbackCount 0 in scripts/eval-quality.ts and PREDICTED to rise by +2 per 30-case capture. Threshold NOT adjusted and should not be. OWNER DECISION LEFT OPEN: quality-antipsychotic-metabolic-monitoring lacks acceptSourceOnly, so once it degrades it fails the live answer-quality eval with \"expected grounded answer\" (its mustContainAny is still satisfied — the source-only text echoes the query). That case asserts a grounded answer for a query whose grounded answer is one of the two incoherent outputs this change rejects; whether it should gain acceptSourceOnly like its discharge sibling is a clinical call and was deliberately not made here. STILL OPEN: the live eval-canary pair, which needs owner approval and is the only thing that can confirm the change.", - "source": "PR #2285; commit 957b76a; docs/rag-improvement/231-diagnosis-2026-08-22.md §3.1-§3.2; src/lib/rag/rag-extractive-answer.ts isLaunderedGuidanceWrapperAnswer", + "detail": "PACKET 2 SHIPPED AND CONFIRMED BY LIVE CANARY 2026-08-22. Step 1 falsification confirmed the premise before any production change: generatedAnswerQualityFailureReason returned null for BOTH captured answers at their real eval query classes (medication_dose_risk, document_lookup), against unmodified source. TWO CORRECTIONS TO THE FRAMING IN THIS ROW, both from reading the code. (1) The gate is NOT unreachable on the grounded first-choice extractive path: rag.ts:3113 is only the review-fallback reason labeller, while the ENFORCING call is unconditional inside finalizeRagAnswerQualityCore and the grounded path reaches it via finalizeAnswer. No reachability change was needed or made; the only remaining bypass is the preformatted-and-grounded early return, now pinned by a test rather than assumed closed. (2) The wrapper is not purely a laundering step: a first predicate rejecting any \"The guidance is that X\" whose continuation lacked an openingSentenceActionPattern verb broke tests/rag-extractive-procedural-artifact.test.ts, which pins \"the ECT Coordinator places the patient onto BASE\"; \"places\" is a real finite verb rightly absent from a clinical-directive list. SHIPPED (PR #2285): isLaunderedGuidanceWrapperAnswer rejects only two shapes no verb list could rescue, a \">\" breadcrumb pointing at a word rather than a figure, and a bare coordinated noun list with no determiner, auxiliary or directive verb, returning reason guidance_wrapper_fragment; placed after the other prose gates so nothing already-rejected is relabelled, and deliberately outside the shouldPreserveSourceBackedGeneratedAnswer rescue allowlist. CANARY OUTCOME, run 32589154243 (2026-08-22, post-merge half only): source_backed_review_fallback_count 2, both carrying final_quality_gate:guidance_wrapper_fragment, exactly the two answers the predicate was built for. provider_attempted:false was 11 of 30 targeting cases, reported SEPARATELY per the measurement rule; never sum the two into a single source-only rate, which is what concealed these answers originally. Retrieval flat against baseline 32111839806: all quality and rank metrics unchanged, zero per-case reciprocal-rank regressions, median latency +217ms and p90 -272ms. The run was RED solely because ragSourceBackedReviewFallbackCount was 0, which is the intended effect and not a regression. CAVEAT ON THE PAIR: no clean immediate-pre-merge baseline exists, because #2285 had already merged before canary approval could be obtained, and the 32111839806 baseline predates the merge by four days; this is sanity evidence, not causal attribution. Both incoherent answers are gone from the named cases, and quality-discharge-documentation now passes targeting with score 1. SETTLED SINCE: the threshold, raised from 0 to a documented two-case allowance in the follow-up PR, on the reasoning that a source-backed review fallback is a degraded answer rather than a defective one and a permanently red weekly canary teaches the team to ignore it. STILL OPEN, tracked separately: quality-antipsychotic-metabolic-monitoring now fails targeting with score 0, reason \"source-backed review stub\". LIMITATION RECORDED: repository_dispatch returns HTTP 403 for cloud agent session types, so eval-canary dispatch is owner-only and eval-canary.yml carries no workflow_dispatch fallback.", + "source": "PR #2285; canary run 32589154243; baseline run 32111839806; docs/rag-improvement/231-diagnosis-2026-08-22.md; src/lib/rag/rag-extractive-answer.ts isLaunderedGuidanceWrapperAnswer", "added": "2026-08-22" }, { @@ -719,73 +710,85 @@ "detail": "FOUND 2026-08-22 during PR #2285's verification, unrelated to that change. npm run test can exit 1 while reporting 'Test Files 714 passed (714) / Tests 8359 passed | 1 skipped (8360)' plus 'Errors 1 error'. The error is 'ReferenceError: document is not defined' thrown from Timeout._onTimeout at src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx:104, inside the 250ms window.setTimeout that selectDestination schedules when returning from the More sheet; the callback calls document.querySelector after the jsdom environment for tests/caring-contact-product-redesign.dom.test.tsx has been torn down. Timing-dependent: two runs of verify:pr-local on byte-identical content in the same container disagreed — the first completed all 19 gates at exit 0, the second failed at 'test (exit 1)' on this error and never reached build or the eval gates. IMPACT: an unhandled post-teardown error is indistinguishable at the summary line from a real failure, and it can fail a PR whose diff cannot reach the component. The component is design scratch under mockups/, so no production surface is affected. LIKELY FIX: the component already holds focusTimerRef; the gap is that nothing clears it on unmount. A cleanup effect that calls window.clearTimeout(focusTimerRef.current) on teardown, and/or a guard on document existence inside the callback, should close it. STOP: do not respond by skipping, disabling or quarantining tests/caring-contact-product-redesign.dom.test.tsx — the test is not what is broken, the uncancelled timer is.", "source": "verify:pr-local run 2026-08-22 during PR #2285; src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx:104; tests/caring-contact-product-redesign.dom.test.tsx", "added": "2026-08-22" - } - ], - "pending": [ - { - "request_id": "30b8fa7f-5f42-48d9-b6d4-5e255f3c67f1", - "action": "add", - "summary": "data/outstanding-issues-snapshot.json is a generated file every inbox PR must regenerate, so any two concurrent ledger PRs conflict on it and the loser must re-resolve after every main merge", - "created_at": "2026-08-22" }, { - "request_id": "3a1de9e9-ada8-4c72-9b50-cf5025148c25", - "action": "update", - "summary": "#6GW95D: detail → UPDATE 2026-08-22 (fleet inventory COMPLETE; cleanup deliberately DEFERRED by the owner). The inventory this row asked for is done and the 253 figure is superseded by a measured one. Scanning eight roots (.claude/worktrees, D:/Worktrees, .codex/worktrees, .gemini/antigravity/worktrees, .copilot/repos/copilot-worktrees, .local/share/opencode/worktree, Documents/Codex, AppData/Local/Temp) found 208 checkouts of this repository. Of those, 92 are registered git worktrees of D:/Repos/Database and 116 are SEPARATE FULL CLONES with their own object databases (76 under .codex, 26 under .copilot, 13 under Documents/Codex, 1 under Temp). Zero unregistered worktrees and zero stale gitdir pointers, so scripts/clean-worktree.mjs sees every checkout that points at the main repo - and sees NONE of the 116 clones, which are the real disk mass. 54 carry node_modules. TWO DISTINCT POPULATIONS, repeatedly conflated by automated review - read the labels before the numbers. POPULATION A, LEFTOVER DIRECTORIES (bare paths with no .git, never git worktrees, holding no content): 18 sat under .claude/worktrees; 11 were removed (ten holding zero files, plus d4-review-fix which held one orphaned node_modules binary, no source and no .git) and the remaining 7 refused with EPERM because a live process holds them; 18 = 11 + 7. One further leftover under .gemini/antigravity was also removed, so 12 leftover-directory removals in total across both roots, the twelfth being outside .claude/worktrees. NONE of these 12 was restored, because none held any content to restore - they were empty paths. Those 7 blocked directories are NOT registered worktrees and have no .git entry - they are bare empty directories that seven sessions still treat as their cwd, filed as their own row and not to be counted as worktree debris here. npm run clean:worktree --merged --squashed --dry-run identified 23 landed candidates totalling 9.30 GB (C: 4.74 GB across 11, D: 4.56 GB across 12), of which 12 are proven-or-corroborated and 11 are 'NOT fully corroborated' and are skipped by the tool's own safety re-verification. CAPACITY IS NOT THE ISSUE: D: is 80 GB with 53 GB free (35% used). DECISION 2026-08-22: the owner halted the removal mid-run and all 13 removed worktrees were restored. Those 13 are POPULATION B - registered git worktrees removed by the clean:worktree pass, an entirely different set from the 12 empty directories above, which is why 12 and 13 do not reconcile and must not be added or compared. The 13 are: agent-ac295f35b1b90b024, caring-contacts-phase-2a-11b-a37803, developer-hub-phase-1-resume-c4f0c3, ed-care-plans-resume-a0dbfd, permission-prompts-issue-0dfcf3 and ward-flow-phase-3-49f201 under .claude/worktrees; ed-care-plans, fix-hookspath-relative, post-drift-ledger-tidy and worktree-cleanup-guard under D:/Worktrees/Database; plus .codex/worktrees/cloud-effort-defaults/Database, .codex/worktrees/image-auto-retry/Database and .gemini/antigravity/worktrees/Database/automate_pr_merge_sweep. Each was restored and verified individually for correct branch, correct head and clean tree, with dependencies restored by byte-identical copy. Five live chats had had their working directory deleted underneath them. Cleanup is DEFERRED indefinitely - do not resume it without an explicit fresh instruction naming the exact paths. Two process lessons for whoever does: a 'git refused' or EPERM removal means a live process holds that directory and is a STOP, never something to retry on a later pass; and the candidate list must be shown and approved before any deletion, not after.; priority → P3; source → session 2026-08-22, GitHub issue #2270 Part A", - "created_at": "2026-08-22" + "id": "#Y090R5", + "priority": "P2", + "type": "issue", + "summary": "data/outstanding-issues-snapshot.json is a generated file every inbox PR must regenerate, so any two concurrent ledger PRs conflict on it and the loser must re-resolve after every main merge", + "detail": "Observed 2026-08-22 on PR #2284, which conflicted twice within about an hour and was closed rather than untangled. check:outstanding-issues-snapshot requires the committed data/outstanding-issues-snapshot.json to be in step with docs/outstanding-issues.md plus every pending inbox request, so a PR that queues a request MUST commit a regenerated snapshot or CI fails with 'counts.pending: committed N vs regenerated M'. But the regenerated content depends on every OTHER pending request too, so the file differs between any two concurrent ledger PRs and conflicts as soon as either lands. Recent main history shows the collision surface is real, not theoretical: 93af96cf8, 4cbac0ceb, 2ca31d6d8 and 639108f07 all touch that one file. The immutable-request design deliberately removed this class of conflict for the requests themselves; the snapshot reintroduces it in a single generated artifact, which is the same serial-only bottleneck #EH9VA6 and the ledger write-discipline work were meant to eliminate. Resolution is mechanical but must be done exactly once per main merge - never hand-merge it: take main's version then re-run node scripts/generate-outstanding-issues-snapshot.mjs. Options worth weighing: regenerate the snapshot during npm run issues:reconcile (the already-serialized step) instead of in every request-adding PR, so ordinary branches never touch the file; or have the check tolerate a snapshot that is in step with the canonical ledger while ignoring pending-request counts; or add a union/regenerate merge strategy. Whichever is chosen, this bites every future issues PR that does not land within the gap between other ledger merges. SECOND SYMPTOM, same root cause, measured 2026-08-22 on PR #2299: because the file lives under data/ - the generated CLINICAL snapshot export directory - scripts/pr-policy.mjs classifies it clinicalRisk:true. Confirmed by calling classifyPullRequestFiles directly: ['data/outstanding-issues-snapshot.json'] alone returns clinicalRisk:true, while the inbox JSONs alone return false. So every ledger PR that regenerates the snapshot is forced to carry a complete ## Clinical Governance Preflight in its body for a file holding no clinical data at all, and fails PR policy with 'Clinical-risk paths require the ## Clinical Governance Preflight section' until it does. That is ceremony with no safety value, and it trains reviewers to tick clinical governance boxes reflexively on changes that have nothing to do with clinical output - which is the failure mode that section exists to prevent. Moving regeneration into issues:reconcile fixes both symptoms at once; relocating the artefact out of data/ would fix this second one on its own.", + "source": "session 2026-08-22, closing PR #2284", + "added": "2026-08-22" }, { - "request_id": "3d5d78c6-6fde-43c5-8cc6-3e5ee7afb4c8", - "action": "add", + "id": "#XN95DM", + "priority": "P3", + "type": "issue", "summary": "tests/gate-receipts.test.ts has two file-mode assertions that can never pass on the Windows workstation, so npm run test is permanently 2-red locally", - "created_at": "2026-08-22" - }, - { - "request_id": "4aed85d0-2bed-4c39-9560-6d5ab2bada19", - "action": "done", - "summary": "#1YPV51: Resolved 2026-08-22 by the Repository Lead's sign-off, recorded in the Sign-off block of docs/medication-interaction-lexicon-review.md. All 37 catalogue terms were expanded to their resolved drug lists and reviewed alongside the text of the rows each term fires on: 34 confirmed correct as they stood, 3 narrowed. (1) ANTIHISTAMINES: all four rows describe additive ANTICHOLINERGIC burden and one names 'sedating antihistamines' in its own words, yet cetirizine, fexofenadine and loratadine resolved into the term -- benzatropine + loratadine produced a CRITICAL 'anticholinergic toxidrome ... risk of toxic megacolon' alert and oxybutynin + cetirizine 'frank delirium and bowel impaction'. The three second-generation agents are denied; cyclizine, promethazine, alimemazine and diphenhydramine are kept. (2) CORTICOSTEROIDS: every row is a systemic effect (insulin resistance x5, tendon rupture with ciprofloxacin, additive hypokalaemia), so the four purely topical glucocorticoids are denied; inhaled agents are deliberately kept because the hypokalaemia row concerns a formoterol inhaler and names high-dose corticosteroids, and fludrocortisone is kept as a mineralocorticoid that raises BSL and drives hypokalaemia. (3) ORAL-CONTRACEPTIVES: the rows concern enzyme inducers destroying the COMBINED pill, and depot medroxyprogesterone is the method a woman is switched TO on an inducer; its own catalogue row already states inducers 'accelerate the clearance of the oral tablets, but the massive 150mg IM depot is generally resistant to clinically failing from this', so the generic term was flattening a nuance the catalogue records correctly. It is removed from the term and its own row still fires, pinned by a test. Two classification questions answered: moclobemide correctly excluded from maois (the rows concern irreversible inhibition -- tyramine crisis and washout -- which a reversible inhibitor does not carry), atomoxetine correctly classed SNRI. Measured: 23 medications changed, removals only, nothing added anywhere, all 328 medications intact, resolved/unresolved unchanged at 392/133. Each change was mutation-tested: reverted, observed to fail 3/4/1 tests, restored. Also fixed: the report generator hardcoded 'Status: UNREVIEWED', so the document would have contradicted itself in its first sentence the moment the sign-off was filled in; the status line is now derived from the recorded sign-off on the same read in both the write and --check paths. HISTORY CARRIED FROM CANCELLED REQUEST 296db779 (2026-08-21, partial progress): loperamide was resolving as an opioid because the selector matches the substring 'Opioid' and the catalogue classifies it 'Peripheral Opioid Agonist'; it is deny-listed alongside naltrexone and naloxone. Four further changes were proposed and withdrawn that day -- adding moclobemide to maois, removing atomoxetine from snris, folding the Z-drugs into benzodiazepines, and splitting antihistamines by sedation. THREE OF THOSE WITHDRAWALS STILL STAND. The fourth was REVERSED on 2026-08-22 and applied: the original withdrawal reasoned only that splitting would stop the term meaning 'antihistamines', which never examined what the rows say; reading them settled it. Recorded so the reversal is visible as evidence-driven rather than a change of mind. HOW THIS REVIEW WAS DONE, stated because this row exists over an earlier closure that claimed a review that had not happened: an assistant analysis proposed a verdict per term with its evidence, and the Repository Lead read and accepted them. It is a reviewed-and-accepted sign-off, NOT an independent re-derivation from primary sources, and must not be described as one. NOT RESOLVED BY THIS, and filed as its own row: acei resolves to perindopril alone, arbs to candesartan alone, statins to atorvastatin and rosuvastatin, because ramipril, lisinopril, irbesartan and simvastatin are absent from the catalogue entirely -- a patient on one of them produces silence rather than safety, which on screen is indistinguishable from a clear check.", - "created_at": "2026-08-22" + "detail": "Measured 2026-08-22. 'gate receipts - file modes (Codex review, PR #2216)' has two cases that call chmodSync(path, 0o755) and then assert the computed input signature changed: 'changes the signature when only the WORKING-TREE mode changes' (line 160) and 'keeps both modes, so one cannot cancel the other' (line 170). The primary workstation is a Windows ReFS Dev Drive with core.fileMode=false, where chmod is a silent no-op, so the working-tree mode never changes and the signatures collide. Reproduced on an unmodified main at 73b1e71a0 with a clean working tree for those files: Tests 2 failed | 32 passed (34). Also seen inside a full local suite run as Tests 2 failed | 7728 passed | 72 skipped (7802). CI is Linux so it stays green there, which is why this has not surfaced before. Cost: every local npm run test and every verify:pr-local exits non-zero on this machine, which both hides real regressions in the noise and stops verify:pr-local before it reaches build, bundle-budget and the RAG fixture checks. Suggested fix: skip the two cases when the filesystem cannot represent an exec bit (probe once by chmod-ing a temp file and re-statting, rather than testing process.platform), keeping the index-mode half of the coverage which does work. See also the related exec-bit trap already documented in AGENTS.md under 'Claude Code hook scripts'.", + "source": "session 2026-08-22, GitHub issue #2270 Part A", + "added": "2026-08-22" }, { - "request_id": "4df11bb5-3d3a-46bc-bf82-3be1bed87663", - "action": "add", + "id": "#4AM8Z0", + "priority": "P2", + "type": "issue", "summary": "Common cardiovascular drugs are absent from the medication catalogue, so patients taking them get silence rather than an interaction check", - "created_at": "2026-08-22" + "detail": "SPLIT OUT 2026-08-22 from the #1YPV51 clinical sign-off, which recorded this as the one finding the lexicon review could NOT fix. The interaction terms acei, arbs and statins resolve to one or two drugs each -- acei to Perindopril alone, arbs to Candesartan alone, statins to Atorvastatin and Rosuvastatin -- not because the mapping is wrong but because ramipril, lisinopril, irbesartan and simvastatin are not in data/medications-snapshot.json at all. Simvastatin is the notable one: it has the largest CYP3A4 interaction profile of the statins and is entirely unrepresented. CONSEQUENCE, and it is the dangerous shape: a clinician entering a patient on ramipril sees no alert, and on screen that is indistinguishable from 'checked and clear'. The sign-off sheet's 'What this tool can never warn about' section already names the same failure mode for the 20 catalogue medications that sit outside every resolved interaction row; this is that boundary reached from the other side -- the drug is not in the catalogue at all. NEXT: decide whether the catalogue should be widened to the common ACE inhibitors, ARBs and statins prescribed in Australian practice, which is a clinical-content decision about source coverage rather than a code change, and needs the same source-backed review any catalogue addition needs. STOP: do not 'fix' this by loosening the lexicon terms -- the terms are correct; the drugs are missing. Related: #1YPV51 (the sign-off recording this), and the corpus-coverage limit documented in docs/medication-interaction-lexicon-review.md.", + "source": "docs/medication-interaction-lexicon-review.md sign-off 2026-08-22; data/medications-snapshot.json; clinical review session 2026-08-22", + "added": "2026-08-22" }, { - "request_id": "aca88c5d-9fd4-4e6b-8e0b-e4d789e1d46c", - "action": "add", + "id": "#RSD9EJ", + "priority": "P2", + "type": "issue", "summary": "The pre-push ledger guard uses the previous branch tip as its baseline, so it false-positives after a branch merges main", - "created_at": "2026-08-22" + "detail": "OBSERVED 2026-08-22/23 on branch claude/lexicon-clinical-signoff (PR #2298). SYMPTOM: git push refused by scripts/guard-push.mjs with 'docs/outstanding-issues-inbox/applied/.json was introduced without moving the identical pending request from the base' and 'reconciliation moved only part of the base inbox', for applied records the branch never touched. CAUSE: the pre-push guard diffs against the PREVIOUSLY PUSHED BRANCH TIP; scripts/check-ledger-write-discipline.mjs diffs against the base branch. When a reconcile lands on main and the branch merges main, main's applied/ records enter the diff relative to the old branch tip and look like a partial reconciliation done on the branch. EVIDENCE, all three checked: the named applied records exist on origin/main; git diff origin/main...HEAD -- docs/outstanding-issues.md was EMPTY, so the branch's canonical ledger was byte-identical to main's; and git log --diff-filter=A named commit 9a382a050 'chore(ledger): reconcile 43 queued inbox requests (#2294)' as their origin. Running the gate with the correct base -- node scripts/check-ledger-write-discipline.mjs --base origin/main --head -- PASSED on the same trees throughout. SECOND SYMPTOM FROM THE SAME CAUSE: the guard also refuses the deletion of a branch-local inbox request that has been pushed but never merged. Editing or deleting such a request is LEGITIMATE -- immutability binds requests present in the base, and the gate confirms this by passing -- but the guard sees it as mutating a published record. This wasted a cycle: the deletion was reverted as if it had been a mistake, then a Cursor autofix agent performed the identical deletion on the same branch and the gate passed. CONSEQUENCE: SKIP_LEDGER_WRITE_GUARD=1 becomes routine on ordinary branches, which is how a guard stops being read -- and this guard exists to make an accidental ledger rewrite impossible. NEXT: give the pre-push guard the same baseline the gate uses (merge base with the upstream default branch), and add a regression test for the exact shape: branch, reconcile on main, merge main into branch, assert the guard passes; plus a second for deleting a pushed-but-unmerged branch-local request. STOP: do not relax what the guard checks -- the baseline is wrong, not the rule.", + "source": "PR #2298 push attempts 2026-08-22/23; scripts/guard-push.mjs; scripts/check-ledger-write-discipline.mjs; commits 9a382a050 (#2294) and 7703188ef", + "added": "2026-08-22" }, { - "request_id": "ad18a397-1c10-4fdc-9438-e29cfa3b7eb4", - "action": "add", + "id": "#J8SJQ9", + "priority": "P2", + "type": "issue", "summary": "Antipsychotic metabolic monitoring returns a source-backed stub instead of a written answer, and the eval case must not be relaxed to hide it", - "created_at": "2026-08-22" - }, - { - "request_id": "b9f8c826-070f-408b-b700-579117d2414f", - "action": "update", - "summary": "#NPQJKP: detail → PACKET 2 SHIPPED AND CONFIRMED BY LIVE CANARY 2026-08-22. Step 1 falsification confirmed the premise before any production change: generatedAnswerQualityFailureReason returned null for BOTH captured answers at their real eval query classes (medication_dose_risk, document_lookup), against unmodified source. TWO CORRECTIONS TO THE FRAMING IN THIS ROW, both from reading the code. (1) The gate is NOT unreachable on the grounded first-choice extractive path: rag.ts:3113 is only the review-fallback reason labeller, while the ENFORCING call is unconditional inside finalizeRagAnswerQualityCore and the grounded path reaches it via finalizeAnswer. No reachability change was needed or made; the only remaining bypass is the preformatted-and-grounded early return, now pinned by a test rather than assumed closed. (2) The wrapper is not purely a laundering step: a first predicate rejecting any \"The guidance is that X\" whose continuation lacked an openingSentenceActionPattern verb broke tests/rag-extractive-procedural-artifact.test.ts, which pins \"the ECT Coordinator places the patient onto BASE\"; \"places\" is a real finite verb rightly absent from a clinical-directive list. SHIPPED (PR #2285): isLaunderedGuidanceWrapperAnswer rejects only two shapes no verb list could rescue, a \">\" breadcrumb pointing at a word rather than a figure, and a bare coordinated noun list with no determiner, auxiliary or directive verb, returning reason guidance_wrapper_fragment; placed after the other prose gates so nothing already-rejected is relabelled, and deliberately outside the shouldPreserveSourceBackedGeneratedAnswer rescue allowlist. CANARY OUTCOME, run 32589154243 (2026-08-22, post-merge half only): source_backed_review_fallback_count 2, both carrying final_quality_gate:guidance_wrapper_fragment, exactly the two answers the predicate was built for. provider_attempted:false was 11 of 30 targeting cases, reported SEPARATELY per the measurement rule; never sum the two into a single source-only rate, which is what concealed these answers originally. Retrieval flat against baseline 32111839806: all quality and rank metrics unchanged, zero per-case reciprocal-rank regressions, median latency +217ms and p90 -272ms. The run was RED solely because ragSourceBackedReviewFallbackCount was 0, which is the intended effect and not a regression. CAVEAT ON THE PAIR: no clean immediate-pre-merge baseline exists, because #2285 had already merged before canary approval could be obtained, and the 32111839806 baseline predates the merge by four days; this is sanity evidence, not causal attribution. Both incoherent answers are gone from the named cases, and quality-discharge-documentation now passes targeting with score 1. SETTLED SINCE: the threshold, raised from 0 to a documented two-case allowance in the follow-up PR, on the reasoning that a source-backed review fallback is a degraded answer rather than a defective one and a permanently red weekly canary teaches the team to ignore it. STILL OPEN, tracked separately: quality-antipsychotic-metabolic-monitoring now fails targeting with score 0, reason \"source-backed review stub\". LIMITATION RECORDED: repository_dispatch returns HTTP 403 for cloud agent session types, so eval-canary dispatch is owner-only and eval-canary.yml carries no workflow_dispatch fallback.; source → PR #2285; canary run 32589154243; baseline run 32111839806; docs/rag-improvement/231-diagnosis-2026-08-22.md; src/lib/rag/rag-extractive-answer.ts isLaunderedGuidanceWrapperAnswer", - "created_at": "2026-08-22" + "detail": "FOUND BY THE PACKET 2 CANARY, run 32589154243 (2026-08-22). \"What metabolic monitoring is required for antipsychotics?\" now returns the source-backed review stub — \"The uploaded documents contain relevant guidance on metabolic monitoring for antipsychotics, but a full written answer could not be completed just now. Relevant document passages are cited below\" — because the extractive candidate behind it was one of the two incoherent guidance-wrapper answers that #NPQJKP shipped a predicate to reject. The degradation is correct behaviour; the underlying defect it exposes is that the answer path cannot produce a usable written answer for this query at all. THIS IS NOT AN EVAL-CASE BUG AND MUST NOT BE FIXED BY ADDING acceptSourceOnly. All four cases carrying that flag document the same rationale: the corpus has no single authoritative source, so a source pointer is a legitimate answer, and quality-discharge-documentation deliberately drops mustContainAny for exactly that reason. quality-antipsychotic-metabolic-monitoring is the opposite case — it names expectedFiles [\"MHSP.MetabolicScreening.pdf\"], an authoritative source exists, and antipsychotic metabolic monitoring is a routine question a psychiatrist should get answered in prose. Adding the flag would silence a true signal. The targeting eval already grades it correctly at score 0 with reason \"source-backed review stub\", even though its mustContainAny [\"metabolic\", \"monitor\"] is satisfied by the stub text, so the instrument is working and only the answer is missing. FIRST DIAGNOSTIC STEP, because it splits the problem in two: determine whether generation was attempted for this case at all. The same run recorded provider_attempted:false for 11 of 30 targeting cases. If OpenAI was never called for this one, the cause is upstream of answer quality entirely (routing, admission control, or the retry ladder — note Packet 1 covers deadline admission control and should be checked for overlap before starting). If it was called and returned nothing usable, the cause is in extraction or generation for the medication_dose_risk class against this document. Do not begin work without checking Packet 1 and Packet 3 (#S4R2W3) for overlap: all three touch rag.ts, and overlapping changes ruin canary attribution.", + "source": "canary run 32589154243; src/lib/rag/rag-eval-cases.ts quality-antipsychotic-metabolic-monitoring; ledger #NPQJKP", + "added": "2026-08-22" }, { - "request_id": "bf12b673-b63e-4683-b45f-96a5fd09976f", - "action": "add", + "id": "#RVK6BJ", + "priority": "P3", + "type": "issue", "summary": "Seven Claude Code sessions point at .claude/worktrees directories that are empty and not registered git worktrees, so those chats have no working copy", - "created_at": "2026-08-22" + "detail": "Measured 2026-08-22 while taking the fleet worktree inventory for #6GW95D. Seven directories under .claude/worktrees contain zero regular files AND are NOT registered git worktrees - none appears in git worktree list, and none has a .git entry: caring-contacts-phase-2a-a4f69a, database-test-queue-contention-6baedb, developer-button-settings-fb9b51, ed-care-plans-impl-7f44cd, phase-4-index-restoration-b0f4ea, vibrant-diffie-c93450 and wave-1-canary-s2-unlock-17d673. They are bare empty directories, neither worktree debris nor live checkouts. Each is the recorded cwd of an existing session in the session registry, titled respectively Suicide, Dev Drive, Developer, Care Plan, Database, Ward Flow and RAG; the Database one was still marked running. They were already empty before this session touched anything: the inventory counted files first and only then attempted removal, and all seven then refused with EPERM because a live process holds the directory handle open, so nothing was deleted from them here. No git history is at risk - every branch still exists at its recorded head. Impact is that resuming any of those chats operates on an empty directory. REPAIR, VERIFIED BY EXECUTION 2026-08-22 rather than assumed, because an earlier review comment and an automated fix both asserted the opposite: since the path is unregistered and the branch is checked out nowhere, git -C D:/Repos/Database worktree add succeeds directly into the existing empty directory - probed on a scratch copy with branch claude/ward-flow-phase-3-7eda97, exit 0, 3812 files checked out. Do NOT add -f: it is unnecessary here and it disables the branch-already-checked-out guard. Then run node scripts/setup-codex-worktree.mjs inside the directory to restore dependencies by byte-identical copy, about 90 seconds rather than an hour-long npm ci. Two conditions would change that command and should be re-checked first: if git worktree list ever DOES name the path, run git worktree prune before adding (the stale-registration path, still not -f); if the branch is checked out in another worktree, add under a different branch rather than using -f. Permit -f only with explicit owner approval after proving the existing worktree is stale and inactive. Worth understanding the cause before repairing them - something is emptying directories that sessions still treat as their cwd.", + "source": "session 2026-08-22, GitHub issue #2270 Part A", + "added": "2026-08-22" }, { - "request_id": "cfaeeb46-b114-4798-93f0-84451499b8fc", - "action": "add", + "id": "#CM7DG9", + "priority": "P3", + "type": "issue", "summary": "The Claude push-format-guard hook blocks every push from a linked worktree, because it compares core.hooksPath against the worktree root instead of the common git dir", - "created_at": "2026-08-22" + "detail": "Measured 2026-08-22 pushing from D:/Repos/Database/.claude/worktrees/. .claude/hooks/push-format-guard.sh only intends to act when the repository pre-push hook is NOT wired. It computes repo_root from CLAUDE_PROJECT_DIR (the linked worktree) and compares the resolved core.hooksPath against repo_root/.githooks. In a linked worktree core.hooksPath is the absolute path of the MAIN checkout's .githooks (D:/Repos/Database/.githooks), which never equals /.githooks, so the guard concludes the push is unguarded even though .githooks/pre-push is present, executable and does in fact run. It then performs a whole-tree prettier --check, which additionally flags git-IGNORED files: the only offender was .claude/settings.local.json, untracked and matched by a global gitignore, so it could not possibly be part of any push. Net effect: pushes from worktrees are blocked on a file that cannot be committed, and the documented CLAUDE_ALLOW_UNFORMATTED_PUSH=1 escape only works as the LEADING token of the whole shell command, so the natural 'cd && CLAUDE_ALLOW_... git push' form silently fails to unlock it. Three fixes, all small: resolve the repo root with git rev-parse --path-format=absolute --git-common-dir (or compare against the common dir's parent) so a linked worktree is recognised as guarded; restrict the fallback prettier check to tracked files (git ls-files) so ignored local state can never block a push; and mention the leading-token requirement in the block message itself. Related but distinct from the already-merged claude/push-guard-hookspath-relative work, which fixed relative and case-folded hooksPath values but not the worktree case.", + "source": "session 2026-08-22, GitHub issue #2270 Part A", + "added": "2026-08-22" }, { - "request_id": "edebb730-91d9-42f5-bd93-ca2abb9678bc", - "action": "add", + "id": "#5MMK5R", + "priority": "P3", + "type": "issue", "summary": "Document that a bare verify:phone-chrome run proves nothing about already-merged work, because its selector reads the working-tree diff and a clean tree selects no browser stage", + "detail": "Reporting/documentation guidance, NOT a verifier defect - reframed 2026-08-22 after review correctly refuted the original framing, and the refutation was checked against the code rather than accepted on assertion. tests/verify-phone-chrome.test.ts pins 'keeps documentation-only work out of browser suites' (docs-only input maps exactly to ['docs-index','docs-links']), so selecting no browser stage for a docs-only diff is the intended, tested contract. The original request proposed a named skip OR a non-zero advisory exit code; the non-zero half was wrong and would have broken legitimate docs-only verification and contradicted the affected-owner/journey selection contract. That suggestion is withdrawn. The residual hazard is real and is about REPORTING, not behaviour. Measured 2026-08-22 while producing the A2 evidence for issue #2270: asked to prove the browser gates against the merged Dictionary work, a bare `npm run verify:phone-chrome` on a main checkout with no tracked changes (the only untracked file was this inbox record, `docs/outstanding-issues-inbox/edebb730-91d9-42f5-bd93-ca2abb9678bc.json`) derived its inputs from the working-tree diff, found that untracked inbox JSON, printed 'Note: No phone-chrome-affecting file was detected; only prerequisite checks were selected', ran docs:check-index and docs:check-links, and exited 0. Every part of that is correct behaviour; the trap is that the exit code is identical to a full pass, so quoting it as 'verify:phone-chrome green' would have been a false green of exactly the class AGENTS.md's evidence rule exists to catch. The real gate only engaged after an explicit --files list of the merged scope (git diff --name-only 66594ddc1^1..main filtered to src/ and tests/ui-), which then selected lock-parity, runtime, contracts, focused-browser and full-ui and produced 133 contract tests, 12 focused phone journeys and 473 Chromium tests, all green. ACTION: add one line to docs/search-chrome-behaviour.md and to the gates skill stating that proving phone chrome for work already on main requires `-- --files ` (or a branch carrying those changes), because a bare run against a clean tree selects nothing and its exit 0 is not evidence about that work. Do NOT change the selector or the exit code.", + "source": "session 2026-08-22, GitHub issue #2270 Part A", + "added": "2026-08-22" + } + ], + "pending": [ + { + "request_id": "7dc2d09a-b05c-4d10-b198-263af3c0f45e", + "action": "update", + "summary": "#RSD9EJ: source → PR #2298 push attempts 2026-08-22/23 (squash-merged as f3d1a3cce2c943ad3083425ed9c7c46dbef23087; the feature branch and its commits 90b5de61f/7703188ef/55f3a47fa are NOT reachable from any ref -- view them on the PR page, not via git); scripts/guard-push.mjs; scripts/check-ledger-write-discipline.mjs; commit 9a382a050 (#2294) is the reconcile whose applied/ records triggered the false positive and IS on main. REPRODUCTION without those commits: branch from main, let a reconcile PR land on main, merge origin/main into the branch, then push -- guard-push refuses with 'applied/.json was introduced without moving the identical pending request from the base' while 'node scripts/check-ledger-write-discipline.mjs --base origin/main --head HEAD' passes on the same tree. The divergence between those two results is the bug.", "created_at": "2026-08-22" } ] diff --git a/docs/outstanding-issues-inbox/7dc2d09a-b05c-4d10-b198-263af3c0f45e.json b/docs/outstanding-issues-inbox/7dc2d09a-b05c-4d10-b198-263af3c0f45e.json new file mode 100644 index 000000000..48eed775a --- /dev/null +++ b/docs/outstanding-issues-inbox/7dc2d09a-b05c-4d10-b198-263af3c0f45e.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "7dc2d09a-b05c-4d10-b198-263af3c0f45e", + "createdOn": "2026-08-22", + "action": "update", + "payload": { + "id": "#RSD9EJ", + "source": "PR #2298 push attempts 2026-08-22/23 (squash-merged as f3d1a3cce2c943ad3083425ed9c7c46dbef23087; the feature branch and its commits 90b5de61f/7703188ef/55f3a47fa are NOT reachable from any ref -- view them on the PR page, not via git); scripts/guard-push.mjs; scripts/check-ledger-write-discipline.mjs; commit 9a382a050 (#2294) is the reconcile whose applied/ records triggered the false positive and IS on main. REPRODUCTION without those commits: branch from main, let a reconcile PR land on main, merge origin/main into the branch, then push -- guard-push refuses with 'applied/.json was introduced without moving the identical pending request from the base' while 'node scripts/check-ledger-write-discipline.mjs --base origin/main --head HEAD' passes on the same tree. The divergence between those two results is the bug.", + "baseRowFingerprint": "b687cb212853e6b4c4df866ea79d625556cad416a0c3e406a2044f82691a4d4b" + } +} diff --git a/docs/outstanding-issues-inbox/30b8fa7f-5f42-48d9-b6d4-5e255f3c67f1.json b/docs/outstanding-issues-inbox/applied/30b8fa7f-5f42-48d9-b6d4-5e255f3c67f1.json similarity index 100% rename from docs/outstanding-issues-inbox/30b8fa7f-5f42-48d9-b6d4-5e255f3c67f1.json rename to docs/outstanding-issues-inbox/applied/30b8fa7f-5f42-48d9-b6d4-5e255f3c67f1.json diff --git a/docs/outstanding-issues-inbox/3a1de9e9-ada8-4c72-9b50-cf5025148c25.json b/docs/outstanding-issues-inbox/applied/3a1de9e9-ada8-4c72-9b50-cf5025148c25.json similarity index 100% rename from docs/outstanding-issues-inbox/3a1de9e9-ada8-4c72-9b50-cf5025148c25.json rename to docs/outstanding-issues-inbox/applied/3a1de9e9-ada8-4c72-9b50-cf5025148c25.json diff --git a/docs/outstanding-issues-inbox/3d5d78c6-6fde-43c5-8cc6-3e5ee7afb4c8.json b/docs/outstanding-issues-inbox/applied/3d5d78c6-6fde-43c5-8cc6-3e5ee7afb4c8.json similarity index 100% rename from docs/outstanding-issues-inbox/3d5d78c6-6fde-43c5-8cc6-3e5ee7afb4c8.json rename to docs/outstanding-issues-inbox/applied/3d5d78c6-6fde-43c5-8cc6-3e5ee7afb4c8.json diff --git a/docs/outstanding-issues-inbox/4aed85d0-2bed-4c39-9560-6d5ab2bada19.json b/docs/outstanding-issues-inbox/applied/4aed85d0-2bed-4c39-9560-6d5ab2bada19.json similarity index 100% rename from docs/outstanding-issues-inbox/4aed85d0-2bed-4c39-9560-6d5ab2bada19.json rename to docs/outstanding-issues-inbox/applied/4aed85d0-2bed-4c39-9560-6d5ab2bada19.json diff --git a/docs/outstanding-issues-inbox/4df11bb5-3d3a-46bc-bf82-3be1bed87663.json b/docs/outstanding-issues-inbox/applied/4df11bb5-3d3a-46bc-bf82-3be1bed87663.json similarity index 100% rename from docs/outstanding-issues-inbox/4df11bb5-3d3a-46bc-bf82-3be1bed87663.json rename to docs/outstanding-issues-inbox/applied/4df11bb5-3d3a-46bc-bf82-3be1bed87663.json diff --git a/docs/outstanding-issues-inbox/aca88c5d-9fd4-4e6b-8e0b-e4d789e1d46c.json b/docs/outstanding-issues-inbox/applied/aca88c5d-9fd4-4e6b-8e0b-e4d789e1d46c.json similarity index 100% rename from docs/outstanding-issues-inbox/aca88c5d-9fd4-4e6b-8e0b-e4d789e1d46c.json rename to docs/outstanding-issues-inbox/applied/aca88c5d-9fd4-4e6b-8e0b-e4d789e1d46c.json diff --git a/docs/outstanding-issues-inbox/ad18a397-1c10-4fdc-9438-e29cfa3b7eb4.json b/docs/outstanding-issues-inbox/applied/ad18a397-1c10-4fdc-9438-e29cfa3b7eb4.json similarity index 100% rename from docs/outstanding-issues-inbox/ad18a397-1c10-4fdc-9438-e29cfa3b7eb4.json rename to docs/outstanding-issues-inbox/applied/ad18a397-1c10-4fdc-9438-e29cfa3b7eb4.json diff --git a/docs/outstanding-issues-inbox/b9f8c826-070f-408b-b700-579117d2414f.json b/docs/outstanding-issues-inbox/applied/b9f8c826-070f-408b-b700-579117d2414f.json similarity index 100% rename from docs/outstanding-issues-inbox/b9f8c826-070f-408b-b700-579117d2414f.json rename to docs/outstanding-issues-inbox/applied/b9f8c826-070f-408b-b700-579117d2414f.json diff --git a/docs/outstanding-issues-inbox/bf12b673-b63e-4683-b45f-96a5fd09976f.json b/docs/outstanding-issues-inbox/applied/bf12b673-b63e-4683-b45f-96a5fd09976f.json similarity index 100% rename from docs/outstanding-issues-inbox/bf12b673-b63e-4683-b45f-96a5fd09976f.json rename to docs/outstanding-issues-inbox/applied/bf12b673-b63e-4683-b45f-96a5fd09976f.json diff --git a/docs/outstanding-issues-inbox/cfaeeb46-b114-4798-93f0-84451499b8fc.json b/docs/outstanding-issues-inbox/applied/cfaeeb46-b114-4798-93f0-84451499b8fc.json similarity index 100% rename from docs/outstanding-issues-inbox/cfaeeb46-b114-4798-93f0-84451499b8fc.json rename to docs/outstanding-issues-inbox/applied/cfaeeb46-b114-4798-93f0-84451499b8fc.json diff --git a/docs/outstanding-issues-inbox/edebb730-91d9-42f5-bd93-ca2abb9678bc.json b/docs/outstanding-issues-inbox/applied/edebb730-91d9-42f5-bd93-ca2abb9678bc.json similarity index 100% rename from docs/outstanding-issues-inbox/edebb730-91d9-42f5-bd93-ca2abb9678bc.json rename to docs/outstanding-issues-inbox/applied/edebb730-91d9-42f5-bd93-ca2abb9678bc.json diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 6e1390d9e..9169140bd 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -106,7 +106,7 @@ removed after current-main verification; it is not missing recommended work. | #6SMMB4 | P3 | task | Confirm D:\.npm-cache is a registered Dev Drive trusted cache, or Defender is scanning every npm ci | The repo lives on a Windows Dev Drive (D:, ReFS, 50 GB) and npm config get cache resolves to D:\.npm-cache, which is correctly on the same volume. Whether that path is registered as a Dev Drive TRUSTED cache is unverified: 'fsutil devdrv query D:' returns 'Failed to open the volume. Error 5: Access is denied' without elevation, and the non-elevated registry fallback (HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem, FilterAttachModeOnDevDrive and DevDriveTrustSetting) reads empty. If it is not registered, Microsoft Defender real-time scanning runs over every npm ci — and this machine performs a lot of them: 21 D: worktrees each carry their own ~0.89 GB / 51,735-file node_modules, because npm extracts fresh copies rather than hardlinking from cache (ReFS does support hardlinks here, probed directly, but npm does not use them). Next: from an ELEVATED prompt run 'fsutil devdrv query D:' and, if the cache is not listed as trusted, 'fsutil devdrv trust D:\.npm-cache'. Cheap, one-off, no code change. Not blocking anything. | session 2026-08-18; fsutil Error 5 without elevation | 2026-08-18 | | #RZQQBT | P3 | task | Confirm from its own log whether the PreCompact hook's output actually reaches model context | UPDATE 2026-08-21 (measured on this machine): still unanswered, and now with a concrete reason. The hook's own log has never been written - neither the main repository's .git/claude-precompact.log nor the per-worktree claude-precompact.log under the .git/worktrees admin directory exists. So no compaction has fired the hook in an inspectable session yet, and the question stays open rather than answered-negative. Next: after the next compaction in a session using this checkout, read that path; an entry proves the hook ran, and its absence in model context would then be the separate question. | PR #2113 .claude/hooks/precompact-issues-capture.sh | 2026-08-18 | | #164Z0H | P3 | task | Confirm on a real Claude Code web session that the session-start hook now runs, after the exec-bit fix | UPDATE 2026-08-21 (repo read on main at 1cc0d2987): the repo-side half is now proven green. git ls-files -s .claude/hooks/ reports mode 100755 for all five hooks, session-start.sh included, so the 100644 asymmetry recorded here is fixed in the index rather than only on disk. What remains is exactly what this row was filed for and cannot be proven from Windows: one real Claude Code web/container session confirming the hook actually runs and provisions Node 24. | PR #2113; AGENTS.md 'Claude Code hook scripts' | 2026-08-18 | -| #6GW95D | P3 | task | Fleet-wide worktree inventory and safe orphan cleanup remain, but the Dev Drive capacity emergency is resolved | UPDATE 2026-08-21 (later the same day): the scope is wider than .claude/worktrees, and the previous update's 69 is itself a floor. An accidental `npx vitest run` issued from C:/Users/joshs instead of the repo root enumerated 253 copies of just TWO test files across the home directory, i.e. 253 separate checkouts of this repository, spread over at least six distinct roots: .codex/worktrees, .gemini/antigravity/worktrees, .copilot/repos/copilot-worktrees, .local/share/opencode/worktree, Documents/Codex///work, and an AppData/Local/pnpm store projects directory (which also contained its own .worktrees). Temp copies under AppData/Local/Temp/guard-push-format-* were still present too. So every AI tool in the fleet keeps its own worktree root and none of them is covered by a sweep scoped to .claude/worktrees. Most had no node_modules (the run failed them with 'Cannot find package'), so raw disk is likely below 253 x 0.89 GB, but the count is the point: any recursive tool run from the home directory now walks all of them. Re-scope this row from 'nine landed worktrees' to a fleet-wide inventory across all six roots before pruning, and keep the #XCAX01 safety preconditions — the 2026-08-21 data-loss incident is why this is not a one-line delete. PRIOR RECORD: UPDATE 2026-08-21 (measured on this machine): the figure recorded here is badly understated and moving the wrong way. git worktree list reported 69 registered worktrees under .claude/worktrees, not nine. At the ~0.89 GB / 51,735-file node_modules per worktree measured in #6SMMB4, even partial installs put this well beyond the 4.5 GB recorded, on a Dev Drive noted as 51% full. SAFETY, learned the hard way on 2026-08-21: a sweep removed an in-use worktree during this very session and destroyed its uncommitted work - see the separate row filed for that. Re-measure actual on-disk size, then prune, but confirm each worktree is landed, holds no uncommitted or unpushed work, AND is not currently checked out by a live agent session before removing it. UPDATE 2026-08-22: the capacity premise is now false. D: has been expanded to 80 GB total, with 22 GB used and 59 GB available (27% used), so this is no longer a space emergency. The worktree safety/inventory evidence remains relevant: 253 checkouts across at least six roots, and the #XCAX01 preconditions remain mandatory. A same-day .claude/worktrees check found 30 directories: 12 registered to live sessions and 18 unregistered orphans. Sixteen orphans were empty and two held one entry; one empty unregistered directory could not be removed because a live process held it. NEXT: keep this as a low-priority fleet-inventory and safe-orphan-cleanup task. Any future cleanup must cover every root, default to report-only, skip a directory when liveness or cleanliness cannot be proved, and never delete a registered worktree. | Accidental home-directory vitest run, 2026-08-21: 253 test-file copies across six worktree roots; Dev Drive / worktree inventory re-measured 2026-08-22 | 2026-08-18 | +| #6GW95D | P3 | task | Fleet-wide worktree inventory and safe orphan cleanup remain, but the Dev Drive capacity emergency is resolved | UPDATE 2026-08-22 (fleet inventory COMPLETE; cleanup deliberately DEFERRED by the owner). The inventory this row asked for is done and the 253 figure is superseded by a measured one. Scanning eight roots (.claude/worktrees, D:/Worktrees, .codex/worktrees, .gemini/antigravity/worktrees, .copilot/repos/copilot-worktrees, .local/share/opencode/worktree, Documents/Codex, AppData/Local/Temp) found 208 checkouts of this repository. Of those, 92 are registered git worktrees of D:/Repos/Database and 116 are SEPARATE FULL CLONES with their own object databases (76 under .codex, 26 under .copilot, 13 under Documents/Codex, 1 under Temp). Zero unregistered worktrees and zero stale gitdir pointers, so scripts/clean-worktree.mjs sees every checkout that points at the main repo - and sees NONE of the 116 clones, which are the real disk mass. 54 carry node_modules. TWO DISTINCT POPULATIONS, repeatedly conflated by automated review - read the labels before the numbers. POPULATION A, LEFTOVER DIRECTORIES (bare paths with no .git, never git worktrees, holding no content): 18 sat under .claude/worktrees; 11 were removed (ten holding zero files, plus d4-review-fix which held one orphaned node_modules binary, no source and no .git) and the remaining 7 refused with EPERM because a live process holds them; 18 = 11 + 7. One further leftover under .gemini/antigravity was also removed, so 12 leftover-directory removals in total across both roots, the twelfth being outside .claude/worktrees. NONE of these 12 was restored, because none held any content to restore - they were empty paths. Those 7 blocked directories are NOT registered worktrees and have no .git entry - they are bare empty directories that seven sessions still treat as their cwd, filed as their own row and not to be counted as worktree debris here. npm run clean:worktree --merged --squashed --dry-run identified 23 landed candidates totalling 9.30 GB (C: 4.74 GB across 11, D: 4.56 GB across 12), of which 12 are proven-or-corroborated and 11 are 'NOT fully corroborated' and are skipped by the tool's own safety re-verification. CAPACITY IS NOT THE ISSUE: D: is 80 GB with 53 GB free (35% used). DECISION 2026-08-22: the owner halted the removal mid-run and all 13 removed worktrees were restored. Those 13 are POPULATION B - registered git worktrees removed by the clean:worktree pass, an entirely different set from the 12 empty directories above, which is why 12 and 13 do not reconcile and must not be added or compared. The 13 are: agent-ac295f35b1b90b024, caring-contacts-phase-2a-11b-a37803, developer-hub-phase-1-resume-c4f0c3, ed-care-plans-resume-a0dbfd, permission-prompts-issue-0dfcf3 and ward-flow-phase-3-49f201 under .claude/worktrees; ed-care-plans, fix-hookspath-relative, post-drift-ledger-tidy and worktree-cleanup-guard under D:/Worktrees/Database; plus .codex/worktrees/cloud-effort-defaults/Database, .codex/worktrees/image-auto-retry/Database and .gemini/antigravity/worktrees/Database/automate_pr_merge_sweep. Each was restored and verified individually for correct branch, correct head and clean tree, with dependencies restored by byte-identical copy. Five live chats had had their working directory deleted underneath them. Cleanup is DEFERRED indefinitely - do not resume it without an explicit fresh instruction naming the exact paths. Two process lessons for whoever does: a 'git refused' or EPERM removal means a live process holds that directory and is a STOP, never something to retry on a later pass; and the candidate list must be shown and approved before any deletion, not after. | session 2026-08-22, GitHub issue #2270 Part A | 2026-08-18 | | #VKH7N1 | P3 | rec | eval-canary neuroleptic-side-effect-escalation exceeded its 20 s latency SLO once | Run 32111839806 (canary pair 32100681177 -> 32111839806, otherwise green): strong generation took 20.2 s on neuroleptic-side-effect-escalation, flagged as a non-blocking latency advisory. Answer was still grounded via the source-backed extractive fallback. Watch on subsequent canaries; escalate only if it repeats or worsens. | docs/rag-improvement/HANDOVER.md packet table row S2, canary pair 32100681177 -> 32111839806 | 2026-08-18 | | #TF6TPJ | P2 | issue | Repeated main-merges on open PR branches cancel required CI, so 'PR required' reads red with zero failing jobs | UPDATE 2026-08-21 (repo read on main at 1cc0d2987): the guard that closes the shared root cause has landed. scripts/guard-push.mjs now carries an explicit Guard 2 in-flight CI push guard block naming #HSSHRG, with inFlightCiVerdict() and findInFlightCiRuns(), covering merge-main syncs made outside sync:pr-branches. #HSSHRG was closed on that evidence. This row should be re-checked against that guard and closed too if the false-red symptom is gone; it was NOT verified against a live PR from here, which needs GitHub access. | PR #2143, runs 32170524256 and 32178668323, 2026-08-18 | 2026-08-18 | | #VTEW3W | P3 | rec | therapyBtn still dresses 13 raw controls across 7 therapy files with no shared equivalent | CORRECTION 2026-08-21, superseding the update applied earlier the same day: the count of 21 across 8 files recorded by that update was WRONG, and the 13 across 7 files measured by the request cancelled as c6a9756d is RIGHT. The discrepancy flagged in that cancellation is now resolved by direct re-measurement on main rather than by choosing between the two numbers. Cause of the error: the earlier update counted raw textual occurrences of therapyBtn, which includes one import statement per consuming file plus the export in controls.ts, and then compared that total against a baseline expressed in CALL SITES. That is an apples-to-oranges comparison and it overstated the remaining work. Measured per file, excluding the import line in each and excluding the definition: brief-screen 1, compare-screen 1, detail-screen 1, pathways-screen 1, recommend-screen 1, sheets-screen 7, therapy-card 1 = 13 call sites across 7 .tsx files, with the export at controls.ts:26. Against the 30 call sites recorded 2026-08-18 at adf93a75, that is a real reduction of 17, not the 9 the bad arithmetic implied. The design question and the stop rule are unchanged: nothing shared covers list rows, disclosure headers, section toggles and chips, and this must not be resolved by converting the remaining sites to Button variants. | session 2026-08-18 | 2026-08-18 | @@ -121,7 +121,6 @@ removed after current-main verification; it is not missing recommended work. | #JVYQEM | P2 | issue | Mode-home composer reserve does not account for the suggestion ticket, so every ticket-bearing home carries a ~0.035 CLS shift | PARTIALLY STALE as of 2026-08-21 — verify before acting. The phone remedy this row proposes has already landed: --spacing-mode-home-composer-phone in src/app/globals.css now reads 10.125rem, not the 6.625rem recorded here, and the surrounding comment states the raise was made to cover the hint row. What remains open is the WIDE case, which that same comment calls out explicitly: --spacing-mode-home-composer-wide is still 5.5rem (88px) against a settled 160px at 1280 and 199px at 800, and it is deliberately NOT raised to match, because the sm+ surface swaps the phone ticket for a prompt-chip row that rewraps with viewport width, so no single static value is correct at every width. That needs a different mechanism (measure-and-publish, or a container-query reserve), not a bigger constant. Also: do NOT treat this row as the cause of the mobile-/ CLS 0.223 breach seen on PR #2199 — that value is bistable, an order of magnitude larger than the ~0.035 recorded here, and did not reproduce under either local harness; it has its own row. Next: confirm the phone case is closed by measuring a ticket-bearing mode home, then scope the wide-reserve mechanism separately. | PR #2160 Lighthouse budget failure | 2026-08-18 | | #HVTYAT | P2 | issue | OpenAI zero-data-retention status contradicts itself: cross-border doc says no, ledger #053 says verified | docs/openai-cross-border-basis.md §8 (dated 2026-07-14) records ZDR 'no', DPA executed 'no', Australia data residency 'not enabled'. Completed ledger row #053 (2026-08-18) states the cross-border package was executed and 'verified OpenAI data controls with input/output data sharing disabled and API zero data retention'. One of the two is wrong. This blocks the /privacy page from telling clinicians what actually happens to question text at the provider: the page currently states only code-verifiable request controls (store:false, no raw owner identifier, requested prompt-cache lifetime) and deliberately makes no ZDR or no-training claim, which is correct under either reading but weaker than it could be. Next step: an operator confirms the live OpenAI project's data controls, then either §8's status table is filled in and the page's External provider processing section is strengthened, or #053 is corrected. | src/lib/privacy-page-content.tsx, docs/openai-cross-border-basis.md §8, docs/privacy-impact-assessment.md PIA-1/PIA-6 | 2026-08-19 | | #1VFSYF | P3 | task | Close the four operator unknowns the B4 shadow-extraction runbook could not answer from the repository: Railway variable-change behaviour, the shadow-record read path, the timeout rollback threshold, and the worker memory limit/peak | TWO OF FOUR ANSWERED 2026-08-21 and folded into docs/worker-deploy-runbook.md section 3. (1) RAILWAY VARIABLE-CHANGE BEHAVIOUR - ANSWERED, and it was a latent safety trap: Railway's docs state that containers read environment variables only at startup, so a variable change never restarts a running container by itself and the new value exists only inside the new deployment. The worker parses WORKER_DOCUMENT_EXTRACTOR_MODE once at process start, so setting the variable is NOT by itself the rollback. Sections 3.5 and 3.7 now state the rollback as two steps (set the variable, then deploy) and warn that stopping after the first leaves docling running. (2) MEMORY LIMIT AND PEAK - ANSWERED by a read-only Railway metrics query on the production worker service over a 7-day window, 10081 samples: memory limit 24 GB, peak 0.566 GB, average 0.139 GB, so roughly 23.4 GB of headroom against the ~1.5 GiB docling needs. The precondition is met with about a fifteenfold margin; section 3.2 now records the numbers as a baseline and requires a busy-window memory-headroom re-check immediately before every shadow enablement and again after any worker image, workload, WORKER_CONCURRENCY, service-plan, or resource-limit change. It also flags that the service reports a 24 vCPU limit while Gate B measured 9-19 s/doc on 2 CPUs, and that the section 3.4 cost model should NOT be assumed to scale down, because docling runs eager and single-process. STILL OPEN, both needing an owner decision rather than investigation: (3) the proposed rollback trigger of more than 10 percent of cohort runs timing out is an unratified operating rule, not a measurement, and nothing in the repository fixes the number; it needs ratifying or replacing, ideally once real wall_ms values exist. (4) there is still no script that reads or aggregates documents.metadata.shadow_extraction, so the first-24-hours watch remains the hand-run SQL query in section 3.6. Recommendation recorded against (4): build the reader when shadow mode is first enabled rather than now, because shadow mode has never run so the table holds zero rows and the tooling cannot be exercised end to end against real data. | docs/worker-deploy-runbook.md sections 3.2, 3.5 and 3.7; read-only Railway metrics on service worker (project Database 5deaad0b) 2026-08-21 | 2026-08-20 | -| #1YPV51 | P1 | task | Reopen #318: the medication interaction lexicon clinical review and sign-off has not actually happened; the lexicon remains clinically unreviewed | PARTIAL PROGRESS 2026-08-22, NOT a clinical sign-off. The sign-off block in docs/medication-interaction-lexicon-review.md is deliberately still empty and the document still reads Status: UNREVIEWED, because a Clinical Lead read of the 37-term sheet has still not happened. What DID happen: the owner reviewed seven specific questions raised from the report and one real defect was found and fixed. FIXED: loperamide was resolving as a member of the opioids term because the selector matches the substring Opioid and the catalogue classifies it Peripheral Opioid Agonist. It is P-gp-restricted at therapeutic doses and does not contribute to the sedation and respiratory-depression rows that term drives (35 of 36 rows CRITICAL or HIGH). It is now deny-listed alongside naltrexone and naloxone, which were already excluded for the same class of reason. Proven, not asserted: regenerating the index with and without the change alters 35 medications, removes loperamide as a counterparty from every one of them, adds nothing anywhere, and leaves resolvedRows/unresolvedRows/rowsWithCatalogueTarget unchanged at 392/133/440. FOUR OTHER CHANGES WERE PROPOSED AND WITHDRAWN after reading the implementation: adding moclobemide to maois, removing atomoxetine from snris, folding the Z-drugs into benzodiazepines, and splitting antihistamines by sedation. All four would have overridden deliberate catalogue classifications (moclobemide is subclass RIMA not MAOI; atomoxetine is subclass SNRI (Non-stimulant); zolpidem and zopiclone are class Sedative subclass Z-Drug) or broken a term's meaning. The sedation concern they were aimed at is carried by the cns-depressants term, which is kind: mechanism and deliberately unenumerable, so it warns on no specific drug by design. TWO QUESTIONS REMAIN FOR THE OWNER, both catalogue-level rather than lexicon-level: (1) should moclobemide's RIMA classification make it fire serotonergic interaction warnings alongside the irreversible MAOIs; (2) is atomoxetine correctly classed SNRI for interaction purposes. STILL OPEN: the full clinical read. Do not close this row without explicit owner confirmation that the review was actually carried out. | session 2026-08-21 ledger reconciliation and docs-truth pass; owner confirmed in chat the 2026-08-18 closure was inaccurate | 2026-08-20 | | #XCAX01 | P2 | issue | A worktree sweep deleted an in-use worktree mid-session on 2026-08-21 and destroyed its uncommitted work; nothing checks whether a worktree is live before removal | Reproduced by loss, not by test. On 2026-08-21 an agent session was working in .claude/worktrees/task-ledger-review-bee095 with 25 staged-but-uncommitted files. Mid-task the directory was emptied, its .git/worktrees admin directory (and therefore its index) was deleted, and the worktree was deregistered, while the session was still running. The staged blobs became unreachable and the work was lost; only the branch ref survived, still at its unchanged base 1cc0d298774e. Every other worktree on disk carried a modification timestamp inside the same twenty-minute span, so this was a sweep across the whole directory rather than a one-off. TWO CONSEQUENCES WORTH SEPARATING. (1) Data loss: removal considered neither uncommitted/untracked content nor whether a process was live in the directory. (2) Silent corruption of the surviving session: after the removal, git commands issued from the deleted path resolved upward to the main checkout D:/Repos/Database, which was on another agent's feature branch with uncommitted modifications - so an unlucky commit would have landed in a different session's branch and working tree. NEXT: before any automated worktree removal, require all three of (a) the branch is merged or its tip is pushed, (b) git status --porcelain --untracked-files=all is empty, and (c) no live process holds the directory; and skip rather than force when any check cannot be evaluated. Prefer a report-only default with an explicit apply flag, matching how sync:pr-branches already separates dry-run from apply. RELATED: #6GW95D records the disk pressure that motivates sweeping, and now carries the same safety caveat. | Session 2026-08-21; worktree task-ledger-review-bee095 removed while in use | 2026-08-20 | | #S19JRT | P2 | task | Add the DB-side structural constraint backing the source_metadata pin, or document why the data-backed pin is sufficient | Re-files #343, closed 2026-08-18 with outcome 'Made retrieval row contract source_metadata schema structural and nullish' -- that outcome is false. Verified 2026-08-21: PR #2107 loosened the source_metadata pin in src/lib/rag/rag-row-contracts.ts to .nullish(); PR #2121 restored the strict .nullable()-required-key pin (git log: ce702ba68 then 4575cf57a). The comment at rag-row-contracts.ts:44-49 explicitly reads 'PR #2107 loosened it to .nullish() and this PR restores it. See docs/outstanding-issues.md #343 for the constraint-backing follow-up.' The DB-side structural constraint (check (jsonb_typeof(metadata) = 'object')) was never added: grep of supabase/schema.sql and supabase/migrations/ finds only 'metadata jsonb not null default {}::jsonb' with no jsonb_typeof check anywhere. The cancelled duplicate #ND10QT record itself states '#343, which is still open', confirming the two closures landed inconsistently. Actionable follow-up: add the check (jsonb_typeof(metadata) = 'object') constraint on documents.metadata with a fail-fast validation guard migration per AGENTS.md's guard-migration contract, or record in this row why the Zod-level pin in rag-row-contracts.ts is sufficient without a DB constraint. | session 2026-08-21 ledger reconciliation and docs-truth pass | 2026-08-20 | | #50QRCF | P2 | issue | Lighthouse budget mobile-root CLS is intermittent: 0.223 vs 0.016 baseline on one run, ~0.000 on the next, same code | CAUSE FOUND AND FIXED — landed on main 2026-08-22 in PR #2253 (merge 66594dd, fix commit 0cf0493). Full evidence is recorded on #TYZK23; this row is the gate-reliability half of the same defect. Summary: the shifting element is the PWA install card (div.pwa-notice-stack), which can mount during a window in which #main-content is briefly absent from the DOM while Next 16 streams and hydrates the route. Its phone geometry is selected by body:has(#main-content[data-phone-footer-owner="hero"]) …, so a card mounting in that gap paints tall (h=401) and is restyled compact (h=161) when the shell returns — one discrete 0.2230 shift. That also explains observation (1) on this row, the part that looked impossible: the gate flips pass/fail on diffs that cannot influence layout because what varies between runs is TIMING (network speed, and whether beforeinstallprompt fires early enough to land inside the gap), not the diff. Deleting one JSON file changes nothing about the page and everything about which side of that race the run lands on — so head c8b7bcdd passing and head 09ff450c failing was never a contradiction. FIXED in src/components/pwa-lifecycle.tsx: the notice stack is held unmounted until the app shell is present. Reproduced locally at exactly 0.2230 before the fix and 0.000 after, using a synthetic beforeinstallprompt at ~120ms plus network throttling. CI on 0cf0493 (run 32531103787): Lighthouse budget SUCCESS. ALSO LANDED, and worth keeping even after this row closes: scripts/run-lighthouse-budget.mjs now prints layout-shift attribution (selector, snippet, score, raw worst item) when grading fails, before the report directory is deleted. This row previously required downloading a CI artifact that this environment cannot reach; that is no longer necessary, and the next occurrence of any layout-shift breach will name its own element. Stop rules honoured: tolerance not widened, baseline not refreshed. NOT CLOSED HERE — close with #TYZK23 and #KFRC3H once further Lighthouse budget runs confirm. | CI run 32531103787 on head 0cf0493; local reproduction 2026-08-22; merged in PR #2253; full trace on #TYZK23 | 2026-08-21 | @@ -141,7 +140,7 @@ removed after current-main verification; it is not missing recommended work. | #BJ80DB | P2 | issue | Windows-only test-budget failures: 3 tests fixed (2 of the original 5, plus 1 newly found in the same file), 2 unreproducible, 1 (hosted-migration-role-guard) still unexplained | Supersedes cancelled request 67bf71cf-25cb-40d9-a8a2-bbf993bf6b29, whose diagnosis was partly wrong. ORIGINAL REPORT: a full npm run test on a clean Windows workstation (D:\Repos\Database, main at cdfcbaccd, empty tree) exited 1 with 4 files / 5 tests failed, 680 files / 7423 passed, 601.68s. CORRECTED FINDING: main was never broken. A full GATE_RECEIPTS=off npm run test on Linux at the content-identical HEAD is green (696 files, 7770 passed, 4 skipped, exit 0, 196s), and git diff cdfcbaccd..HEAD shows none of the four test files nor their subjects changed. THE ROOT CAUSE FOR THE TWO NOW FIXED IS A TIME BUDGET, NOT A DEFECT IN BEHAVIOUR. (1) codex-cloud-setup 'writes managed shell policy behaviorally and preserves unrelated Codex config' measured 24.96s on that workstation running its file ALONE, against the 30s testTimeout in vitest.config.mts - 83% of budget with nothing competing; a full run puts maxWorkers:4 on the machine and it tips over. It spawns bash scripts/setup-codex-cloud.sh twice, cheap on Linux and expensive on Windows where every spawn goes through Git Bash. Its immediate sibling already carried an explicit 120_000 budget for the same reason (measured 112.9s); this one had been missed. FIXED in PR #2255 by giving it the same budget. (2) http-readiness 'honours the overall deadline when every response remains open' and its neighbour 'destroys stalled requests and resumes polling' both work to millisecond budgets (requestTimeoutMs 40, timeoutMs 180/1000) that Windows cannot hold. The neighbour fails IN ISOLATION ON AN IDLE MACHINE ('expected 4 to be 3'), which is stronger than a load flake: the 40ms request budget bounds not only the two deliberately-stalled responses but also the healthy third one, so a merely-late good response is destroyed as though stalled and the loop polls a fourth time. FIXED in PR #2256 (requestTimeoutMs 40->250 with the deadline 1s->5s; the other case's deadline 180ms->600ms), with a negative proof that breaking the stall-destroy path still fails both. (3) The two document-viewer page-virtualization cases PASS on re-run on that workstation (whole file 3566ms). PR #2252 made their shared failure point legible: flushIdle() could only report 'expected 0 to be greater than 0', which reads as a slow machine; it now names the canvas-budget cause and prints slot/rendered/observer counts. STILL OPEN AND THE REASON THIS ROW EXISTS: hosted-migration-role-guard 'accepts the current repository state' passed on re-run at 1550ms, nowhere near its 30s budget, so the timeout explanation that resolved the other two does NOT fit it and nothing has been changed for it. Its original cause is unexplained. Leading hypothesis, untested: validateRepository() calls repositoryEntries(), which runs git ls-files --cached --others --exclude-standard, so it grades UNTRACKED non-ignored files too - any stray file under scripts/, supabase/migrations/, .github/workflows/ or the guarded exact paths whose name or content matches the reserved role turns it red while git status still looks clean enough to call the tree empty, as would a nested git worktree directory (readFileSync on a directory entry throws EISDIR). It also SHA-256s the on-disk bytes of the pinned immutable migration, so CRLF would break it, though .gitattributes '* text=auto eol=lf' should prevent that and the bytes hash correctly on Linux. NEXT: if it recurs, capture git status --porcelain --untracked-files=all and git ls-files --others --exclude-standard at the moment of failure - the assertion alone will not identify which entry did it. FLAKE POLICY DOES NOT APPLY TO ANY OF THESE: tests/flake-ledger.json is explicitly Playwright-only (exact spec+title, @quarantine tag); all five are Vitest and none are eligible for quarantine. Also note the previously known local-Windows failures (tests/session-start-hook.test.ts, worker-observability) PASSED in the reported run, so any note calling those the known environmental set is stale. STANDING LESSON: on this workstation, a test's assertions being deterministic does NOT make its runtime deterministic. Subprocess-spawning and wall-clock-bounded tests need budgets sized for Windows, and 83%-of-budget in isolation is a latent failure, not a pass. | PRs #2252, #2255, #2256 (2026-08-21); Windows workstation runs at cdfcbaccd and after | 2026-08-21 | | #9X40BT | P2 | rec | Supabase preview-branch compute is an uncapped cost sitting outside the organisation Spend Cap | 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. | Supabase dashboard read 2026-08-21; docs/audit/live-drift-forensics-2026-08.md D4 section; AGENTS.md Supabase project safety | 2026-08-21 | | #S4R2W3 | P3 | issue | Two clinical questions are answered with a bare list of document titles instead of an answer, and no gate fires | Found 2026-08-22 while diagnosing #231 against the 60 Gate E answers (docs/rag-improvement/231-diagnosis-2026-08-22.md section 3.1). Two of the eight identical plain source_only cases are clinical questions that receive the document-inventory answer shape reserved for source-lookup questions. 'What is the duress procedure pathway?' (case quality-duress-pathway, classified query_class document_lookup, intent pathway_referral) and 'When is IM medication used in the agitation pathway?' (case quality-agitation-im-route, query_class medication_dose_risk, intent pathway_referral, routed high_confidence_extractive_retrieval) both return: I found 5 indexed documents that support this query: followed by five titles. Neither question asked which documents exist. The routing chain for both ends at the first token -- no fallback reason, no gate reason, no retry, provider_attempted false -- so nothing flags it. CONTRAST, and why this is a real defect rather than a design choice: the other four cases in the same group (lithium-monitoring-documents, long-acting-injectable-documents, patient-safety-plan-documents, nocc-document-support) literally ask which documents or sources support X, and for those the same document list IS the correct answer. So the shape is right for four and wrong for two, which points at route selection or query classification rather than at the extractive answer builder. NEXT: diagnose separately -- establish whether the fault is hasSourceSupportLookupIntent / the query classifier assigning document_lookup to a pathway question, or shouldUseExtractiveMedicationLookup admitting a pathway question into the high-confidence extractive route. Do NOT fold this into the #231 retry-ladder work or the grounded-extractive gate work; it is a different surface with a different fix. STOP: protected RAG surface (src/lib/rag/rag-routing.ts, clinical-search), flag before editing, behaviour change needs a live eval-canary pair. Lower priority than the other two because the answer is at least honest and cites real documents, rather than being wrong or incoherent. | docs/rag-improvement/231-diagnosis-2026-08-22.md section 3.1; Gate E dumps output/gate-e/dump-v18.json and dump-v19.json of 2026-08-21; session 2026-08-22 | 2026-08-21 | -| #NPQJKP | P2 | issue | Answer-quality predicates accept two incoherent grounded extractive answers; diagnose predicate strictness before changing gate reachability | PACKET 2 IMPLEMENTED 2026-08-22, PR pending. Step 1 falsification CONFIRMED the premise before any production change: generatedAnswerQualityFailureReason returned null for BOTH captured answers at their real eval query classes (medication_dose_risk, document_lookup), run against unmodified source. TWO CORRECTIONS TO THE ROW'S OWN FRAMING, both from reading the code. (1) The gate is NOT unreachable on the grounded first-choice extractive path. rag.ts:3113 is only the review-fallback reason labeller; the ENFORCING call is unconditional inside finalizeRagAnswerQualityCore and the grounded path reaches it via finalizeAnswer -> finalizeRagAnswerQuality. No reachability change was needed or made. The only remaining bypass is the preformatted-and-grounded early return, now named and pinned by a test rather than assumed closed. (2) The wrapper is NOT purely a laundering step. The first predicate version — reject any "The guidance is that X" whose continuation lacks a verb from openingSentenceActionPattern — broke tests/rag-extractive-procedural-artifact.test.ts, which pins "the ECT Coordinator places the patient onto BASE" as a grounded answer; "places" is a real finite verb and rightly absent from a clinical-directive list. SHIPPED: isLaunderedGuidanceWrapperAnswer rejects only two shapes no verb list could rescue — a ">" breadcrumb pointing at a word rather than a figure, and a bare coordinated noun list with no determiner, auxiliary or directive verb — returning reason guidance_wrapper_fragment. Placed after the other prose gates so nothing already-rejected is relabelled; not in the shouldPreserveSourceBackedGeneratedAnswer rescue allowlist, so a flagged answer cannot be recovered to grounded. clippedClinicalFragmentPattern's four-continuation enumeration is left in place and its cases are still rejected. EVAL IMPACT: eval:rag:offline 627/627 and eval:rag:adversarial:offline 25/25, identical before and after (baseline re-run with GATE_RECEIPTS=refresh). source_backed_review_fallback_count cannot be measured offline; threshold is ragSourceBackedReviewFallbackCount 0 in scripts/eval-quality.ts and PREDICTED to rise by +2 per 30-case capture. Threshold NOT adjusted and should not be. OWNER DECISION LEFT OPEN: quality-antipsychotic-metabolic-monitoring lacks acceptSourceOnly, so once it degrades it fails the live answer-quality eval with "expected grounded answer" (its mustContainAny is still satisfied — the source-only text echoes the query). That case asserts a grounded answer for a query whose grounded answer is one of the two incoherent outputs this change rejects; whether it should gain acceptSourceOnly like its discharge sibling is a clinical call and was deliberately not made here. STILL OPEN: the live eval-canary pair, which needs owner approval and is the only thing that can confirm the change. | PR #2285; commit 957b76a; docs/rag-improvement/231-diagnosis-2026-08-22.md §3.1-§3.2; src/lib/rag/rag-extractive-answer.ts isLaunderedGuidanceWrapperAnswer | 2026-08-22 | +| #NPQJKP | P2 | issue | Answer-quality predicates accept two incoherent grounded extractive answers; diagnose predicate strictness before changing gate reachability | PACKET 2 SHIPPED AND CONFIRMED BY LIVE CANARY 2026-08-22. Step 1 falsification confirmed the premise before any production change: generatedAnswerQualityFailureReason returned null for BOTH captured answers at their real eval query classes (medication_dose_risk, document_lookup), against unmodified source. TWO CORRECTIONS TO THE FRAMING IN THIS ROW, both from reading the code. (1) The gate is NOT unreachable on the grounded first-choice extractive path: rag.ts:3113 is only the review-fallback reason labeller, while the ENFORCING call is unconditional inside finalizeRagAnswerQualityCore and the grounded path reaches it via finalizeAnswer. No reachability change was needed or made; the only remaining bypass is the preformatted-and-grounded early return, now pinned by a test rather than assumed closed. (2) The wrapper is not purely a laundering step: a first predicate rejecting any "The guidance is that X" whose continuation lacked an openingSentenceActionPattern verb broke tests/rag-extractive-procedural-artifact.test.ts, which pins "the ECT Coordinator places the patient onto BASE"; "places" is a real finite verb rightly absent from a clinical-directive list. SHIPPED (PR #2285): isLaunderedGuidanceWrapperAnswer rejects only two shapes no verb list could rescue, a ">" breadcrumb pointing at a word rather than a figure, and a bare coordinated noun list with no determiner, auxiliary or directive verb, returning reason guidance_wrapper_fragment; placed after the other prose gates so nothing already-rejected is relabelled, and deliberately outside the shouldPreserveSourceBackedGeneratedAnswer rescue allowlist. CANARY OUTCOME, run 32589154243 (2026-08-22, post-merge half only): source_backed_review_fallback_count 2, both carrying final_quality_gate:guidance_wrapper_fragment, exactly the two answers the predicate was built for. provider_attempted:false was 11 of 30 targeting cases, reported SEPARATELY per the measurement rule; never sum the two into a single source-only rate, which is what concealed these answers originally. Retrieval flat against baseline 32111839806: all quality and rank metrics unchanged, zero per-case reciprocal-rank regressions, median latency +217ms and p90 -272ms. The run was RED solely because ragSourceBackedReviewFallbackCount was 0, which is the intended effect and not a regression. CAVEAT ON THE PAIR: no clean immediate-pre-merge baseline exists, because #2285 had already merged before canary approval could be obtained, and the 32111839806 baseline predates the merge by four days; this is sanity evidence, not causal attribution. Both incoherent answers are gone from the named cases, and quality-discharge-documentation now passes targeting with score 1. SETTLED SINCE: the threshold, raised from 0 to a documented two-case allowance in the follow-up PR, on the reasoning that a source-backed review fallback is a degraded answer rather than a defective one and a permanently red weekly canary teaches the team to ignore it. STILL OPEN, tracked separately: quality-antipsychotic-metabolic-monitoring now fails targeting with score 0, reason "source-backed review stub". LIMITATION RECORDED: repository_dispatch returns HTTP 403 for cloud agent session types, so eval-canary dispatch is owner-only and eval-canary.yml carries no workflow_dispatch fallback. | PR #2285; canary run 32589154243; baseline run 32111839806; docs/rag-improvement/231-diagnosis-2026-08-22.md; src/lib/rag/rag-extractive-answer.ts isLaunderedGuidanceWrapperAnswer | 2026-08-22 | | #2TAQDC | P3 | rec | Nothing stops a future :has(#main-content…) rule inheriting the app-shell hydration gap that caused the mobile-/ CLS flake | FILED 2026-08-22 from the fix for #TYZK23 / #50QRCF / #KFRC3H (PR #2253, commit 0cf0493). Established by measurement, not inference: on a throttled phone load, #main-content is absent from the DOM for roughly two seconds while Next 16 streams the route in and React hydrates it (trace: present at t=4726ms, GONE at t=7855ms, back at t=9930ms). Any CSS keyed on body:has(#main-content…) therefore evaluates FALSE for a window during load, and any element mounting inside that window paints with the wrong geometry and is restyled when the shell returns. That is exactly the 0.2230 layout shift three ledger rows chased for a week across four candidate mechanisms. NO LIVE DEFECT TODAY: all eleven current consumers are the PWA install card (src/app/globals.css lines 3018-3047), and 0cf0493 defends that one consumer by holding the notice stack unmounted until the shell is present. What is missing is anything that stops the next one. There is no lint rule, no contract test, and no line in docs/search-chrome-behaviour.md warning that a shell-presence :has() is only sound for an element that cannot mount during hydration — so a twelfth consumer added anywhere would silently reintroduce a bistable CLS that took three ledger rows and several days to attribute. OPTIONS, cheapest first: (a) a paragraph in docs/search-chrome-behaviour.md beside the existing phone-chrome reserve rules, stating the gap and the condition; (b) a static contract test asserting every :has(#main-content…) rule targets a descendant of .pwa-notice-stack, which fails loudly the moment a consumer is added elsewhere and costs nothing to run; (c) a lint rule, almost certainly not worth it for eleven selectors. RECOMMEND (a) plus (b). STOP RULE: do not respond by deleting the :has() selectors. They are the correct way to express phone-composer ownership, and the gap is a property of streaming hydration, not a selector bug. | commit 0cf0493 and the local reproduction trace, 2026-08-22; src/app/globals.css lines 3018-3047; related rows #TYZK23, #50QRCF, #KFRC3H | 2026-08-22 | | #JZM7RM | P2 | rec | Two independent auto-fixers can answer the same PR review comment, and only one of them is governed or documented | On PR #2249 a single Codex review comment on .claude/hooks/push-format-guard.sh drew more than one responder: this repo's .github/workflows/codex-autofix-review-comments.yml, and an app-level 'Autofix pull requests' watcher that also instructed a live session to do the identical work. Two complete fixes were built independently and one was discarded. Evidence in local git history: commit 687b166d (author 'Claude', 2026-08-21T19:41:56Z) and c9d990aa (author 'BigSimmo', +0800) both edit the same hook on the same branch claude/push-guard-hookspath-relative; b9414fa0 and 94b6c291 are two separate 'merge main into claude/push-guard-hookspath-relative' commits made minutes apart by those two different identities, and 828106a5 merges the branch into itself to reconcile the divergence. The repo-side Codex workflow carries real safeguards: trusted-bot login gating, a per-PR dedup marker, one automatic repair pass per PR lifetime, and a skip-codex-review opt-out. The app-level watcher has none of these, and the string 'Autofix pull requests' appears nowhere in .github/ or docs/ - it is in neither the repository nor the tooling map, while docs/agents-guide.md line 44 assigns 'Primary PR code-review + automatic resolve' to Codex alone. Next: decide which responder owns automatic resolve, then either document and throttle the app-level watcher in docs/agents-guide.md or disable it, so one comment cannot spawn two competing fixes. Sibling of the ledger item recording the same failure between two human-driven sessions. | PR #2259 review; verified against local git history at origin/main 226bd32c (687b166d, c9d990aa, b9414fa0, 94b6c291, 828106a5); replaces a mis-filed payload that duplicated inbox request 67bf71cf-25cb-40d9-a8a2-bbf993bf6b29 | 2026-08-21 | | #VV83VA | P3 | rec | Local process-spawn starvation silently makes every git push take minutes; the one-command diagnostic is not written down anywhere | MEASURED TWICE, THREE DAYS APART. On 2026-08-20 a session recorded `gh --version` at 1m37s and concluded gh itself was broken, which made every git push take 5-10 minutes because .githooks/pre-push -> guard-push.mjs invokes gh several times per push. On 2026-08-22 the same binary measured 0.201s, 0.262s and 0.278s on three consecutive runs. IT WAS NEVER GH. The decisive comparison is a binary with nothing to do with gh: `node --version` was 17s on 2026-08-20 and 0.080s on 2026-08-22, a ~200x change. `git --version` measures 0.069s. gh is unchanged between the two readings -- same 2.97.0, same C:\Program Files\GitHub CLI\gh.exe on local disk (not a network path, not a shim), same 41,775,416 bytes, and ZERO extensions installed, so the usual suspects (startup update check, extension probing, slow path) are all excluded. Windows Defender real-time protection was ON in BOTH readings, so Defender alone is not sufficient to cause it either; exclusions could not be read without administrator rights. WHAT ACTUALLY CHANGED: the machine had been rebooted about an hour before the fast reading. The condition is machine-wide process-spawn starvation that accumulates with long uptime and many concurrent agent sessions, and a reboot clears it. The precise mechanism cannot be pinned down retroactively once the state is gone, and that is stated rather than guessed at. IT IS ALREADY REBUILDING: one hour after the reboot the machine showed 49 node processes, 31 claude processes, 548 processes total, and 6.9 GB free of 32 GB. So this recurs rather than being a one-off, and the next session to hit it will again mistake it for a broken tool. NEXT (cheap, docs-only): write the diagnostic down where a session will find it -- `time node --version` is the whole test. Sub-second means the machine is healthy and a slow push is something else; multiple seconds means spawn starvation, and the fix is to close sessions or reboot, NOT to debug gh, git, or the pre-push guard. Natural home is the traps section of AGENTS.md or docs/testing.md. STOP: do not change Windows security settings or add a Defender exclusion on this evidence -- the two readings show Defender is not the discriminating variable, and a security-settings change needs the owner regardless. | measured 2026-08-20 and 2026-08-22 on the Windows workstation; session 2026-08-22 Phase 5 close-out | 2026-08-21 | @@ -149,6 +148,14 @@ removed after current-main verification; it is not missing recommended work. | #KZJD4Q | P2 | task | Two zero-cost levers to restore automated review coverage remain unbuilt | 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. | 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 | 2026-08-22 | | #243HCC | P3 | issue | tests/settings-dialog-actions.dom.test.tsx contains an assertion that can never run: data-settings-nav-target is emitted nowhere in src | Found on the developer-hub branch 2026-08-22 while renaming the Settings entry. The test collects document.querySelectorAll("[data-settings-nav-target]"), maps the attribute, and compares it to the rendered section ids — but guards the comparison with 'if (railLabels.length)'. A repo-wide grep shows data-settings-nav-target is emitted in NO source file, so railLabels is always empty and the comparison never executes. The stated intent, per its own comment, is 'The desktop rail lists exactly the sections that render' — a real invariant that is currently unguarded, and one this branch's rename could have broken silently. Fix is one of two: emit the attribute on the rail so the assertion starts doing its job, or delete the dead branch and assert the rail some other way. That choice belongs to the settings-dialog owner, which is why it was left rather than fixed in passing. Pre-existing and unrelated to the developer hub work. | Developer hub Phase 1 controller, task 9 (2026-08-22) | 2026-08-21 | | #0YK2S3 | P3 | issue | A caring-contacts mockup timer fires after jsdom teardown, so the full unit suite can exit 1 with 8359/8359 passing | FOUND 2026-08-22 during PR #2285's verification, unrelated to that change. npm run test can exit 1 while reporting 'Test Files 714 passed (714) / Tests 8359 passed \| 1 skipped (8360)' plus 'Errors 1 error'. The error is 'ReferenceError: document is not defined' thrown from Timeout._onTimeout at src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx:104, inside the 250ms window.setTimeout that selectDestination schedules when returning from the More sheet; the callback calls document.querySelector after the jsdom environment for tests/caring-contact-product-redesign.dom.test.tsx has been torn down. Timing-dependent: two runs of verify:pr-local on byte-identical content in the same container disagreed — the first completed all 19 gates at exit 0, the second failed at 'test (exit 1)' on this error and never reached build or the eval gates. IMPACT: an unhandled post-teardown error is indistinguishable at the summary line from a real failure, and it can fail a PR whose diff cannot reach the component. The component is design scratch under mockups/, so no production surface is affected. LIKELY FIX: the component already holds focusTimerRef; the gap is that nothing clears it on unmount. A cleanup effect that calls window.clearTimeout(focusTimerRef.current) on teardown, and/or a guard on document existence inside the callback, should close it. STOP: do not respond by skipping, disabling or quarantining tests/caring-contact-product-redesign.dom.test.tsx — the test is not what is broken, the uncancelled timer is. | verify:pr-local run 2026-08-22 during PR #2285; src/components/caring-contacts/mockups/caring-contact-shell-frame.tsx:104; tests/caring-contact-product-redesign.dom.test.tsx | 2026-08-22 | +| #Y090R5 | P2 | issue | data/outstanding-issues-snapshot.json is a generated file every inbox PR must regenerate, so any two concurrent ledger PRs conflict on it and the loser must re-resolve after every main merge | Observed 2026-08-22 on PR #2284, which conflicted twice within about an hour and was closed rather than untangled. check:outstanding-issues-snapshot requires the committed data/outstanding-issues-snapshot.json to be in step with docs/outstanding-issues.md plus every pending inbox request, so a PR that queues a request MUST commit a regenerated snapshot or CI fails with 'counts.pending: committed N vs regenerated M'. But the regenerated content depends on every OTHER pending request too, so the file differs between any two concurrent ledger PRs and conflicts as soon as either lands. Recent main history shows the collision surface is real, not theoretical: 93af96cf8, 4cbac0ceb, 2ca31d6d8 and 639108f07 all touch that one file. The immutable-request design deliberately removed this class of conflict for the requests themselves; the snapshot reintroduces it in a single generated artifact, which is the same serial-only bottleneck #EH9VA6 and the ledger write-discipline work were meant to eliminate. Resolution is mechanical but must be done exactly once per main merge - never hand-merge it: take main's version then re-run node scripts/generate-outstanding-issues-snapshot.mjs. Options worth weighing: regenerate the snapshot during npm run issues:reconcile (the already-serialized step) instead of in every request-adding PR, so ordinary branches never touch the file; or have the check tolerate a snapshot that is in step with the canonical ledger while ignoring pending-request counts; or add a union/regenerate merge strategy. Whichever is chosen, this bites every future issues PR that does not land within the gap between other ledger merges. SECOND SYMPTOM, same root cause, measured 2026-08-22 on PR #2299: because the file lives under data/ - the generated CLINICAL snapshot export directory - scripts/pr-policy.mjs classifies it clinicalRisk:true. Confirmed by calling classifyPullRequestFiles directly: ['data/outstanding-issues-snapshot.json'] alone returns clinicalRisk:true, while the inbox JSONs alone return false. So every ledger PR that regenerates the snapshot is forced to carry a complete ## Clinical Governance Preflight in its body for a file holding no clinical data at all, and fails PR policy with 'Clinical-risk paths require the ## Clinical Governance Preflight section' until it does. That is ceremony with no safety value, and it trains reviewers to tick clinical governance boxes reflexively on changes that have nothing to do with clinical output - which is the failure mode that section exists to prevent. Moving regeneration into issues:reconcile fixes both symptoms at once; relocating the artefact out of data/ would fix this second one on its own. | session 2026-08-22, closing PR #2284 | 2026-08-22 | +| #XN95DM | P3 | issue | tests/gate-receipts.test.ts has two file-mode assertions that can never pass on the Windows workstation, so npm run test is permanently 2-red locally | Measured 2026-08-22. 'gate receipts - file modes (Codex review, PR #2216)' has two cases that call chmodSync(path, 0o755) and then assert the computed input signature changed: 'changes the signature when only the WORKING-TREE mode changes' (line 160) and 'keeps both modes, so one cannot cancel the other' (line 170). The primary workstation is a Windows ReFS Dev Drive with core.fileMode=false, where chmod is a silent no-op, so the working-tree mode never changes and the signatures collide. Reproduced on an unmodified main at 73b1e71a0 with a clean working tree for those files: Tests 2 failed \| 32 passed (34). Also seen inside a full local suite run as Tests 2 failed \| 7728 passed \| 72 skipped (7802). CI is Linux so it stays green there, which is why this has not surfaced before. Cost: every local npm run test and every verify:pr-local exits non-zero on this machine, which both hides real regressions in the noise and stops verify:pr-local before it reaches build, bundle-budget and the RAG fixture checks. Suggested fix: skip the two cases when the filesystem cannot represent an exec bit (probe once by chmod-ing a temp file and re-statting, rather than testing process.platform), keeping the index-mode half of the coverage which does work. See also the related exec-bit trap already documented in AGENTS.md under 'Claude Code hook scripts'. | session 2026-08-22, GitHub issue #2270 Part A | 2026-08-22 | +| #4AM8Z0 | P2 | issue | Common cardiovascular drugs are absent from the medication catalogue, so patients taking them get silence rather than an interaction check | SPLIT OUT 2026-08-22 from the #1YPV51 clinical sign-off, which recorded this as the one finding the lexicon review could NOT fix. The interaction terms acei, arbs and statins resolve to one or two drugs each -- acei to Perindopril alone, arbs to Candesartan alone, statins to Atorvastatin and Rosuvastatin -- not because the mapping is wrong but because ramipril, lisinopril, irbesartan and simvastatin are not in data/medications-snapshot.json at all. Simvastatin is the notable one: it has the largest CYP3A4 interaction profile of the statins and is entirely unrepresented. CONSEQUENCE, and it is the dangerous shape: a clinician entering a patient on ramipril sees no alert, and on screen that is indistinguishable from 'checked and clear'. The sign-off sheet's 'What this tool can never warn about' section already names the same failure mode for the 20 catalogue medications that sit outside every resolved interaction row; this is that boundary reached from the other side -- the drug is not in the catalogue at all. NEXT: decide whether the catalogue should be widened to the common ACE inhibitors, ARBs and statins prescribed in Australian practice, which is a clinical-content decision about source coverage rather than a code change, and needs the same source-backed review any catalogue addition needs. STOP: do not 'fix' this by loosening the lexicon terms -- the terms are correct; the drugs are missing. Related: #1YPV51 (the sign-off recording this), and the corpus-coverage limit documented in docs/medication-interaction-lexicon-review.md. | docs/medication-interaction-lexicon-review.md sign-off 2026-08-22; data/medications-snapshot.json; clinical review session 2026-08-22 | 2026-08-22 | +| #RSD9EJ | P2 | issue | The pre-push ledger guard uses the previous branch tip as its baseline, so it false-positives after a branch merges main | OBSERVED 2026-08-22/23 on branch claude/lexicon-clinical-signoff (PR #2298). SYMPTOM: git push refused by scripts/guard-push.mjs with 'docs/outstanding-issues-inbox/applied/.json was introduced without moving the identical pending request from the base' and 'reconciliation moved only part of the base inbox', for applied records the branch never touched. CAUSE: the pre-push guard diffs against the PREVIOUSLY PUSHED BRANCH TIP; scripts/check-ledger-write-discipline.mjs diffs against the base branch. When a reconcile lands on main and the branch merges main, main's applied/ records enter the diff relative to the old branch tip and look like a partial reconciliation done on the branch. EVIDENCE, all three checked: the named applied records exist on origin/main; git diff origin/main...HEAD -- docs/outstanding-issues.md was EMPTY, so the branch's canonical ledger was byte-identical to main's; and git log --diff-filter=A named commit 9a382a050 'chore(ledger): reconcile 43 queued inbox requests (#2294)' as their origin. Running the gate with the correct base -- node scripts/check-ledger-write-discipline.mjs --base origin/main --head -- PASSED on the same trees throughout. SECOND SYMPTOM FROM THE SAME CAUSE: the guard also refuses the deletion of a branch-local inbox request that has been pushed but never merged. Editing or deleting such a request is LEGITIMATE -- immutability binds requests present in the base, and the gate confirms this by passing -- but the guard sees it as mutating a published record. This wasted a cycle: the deletion was reverted as if it had been a mistake, then a Cursor autofix agent performed the identical deletion on the same branch and the gate passed. CONSEQUENCE: SKIP_LEDGER_WRITE_GUARD=1 becomes routine on ordinary branches, which is how a guard stops being read -- and this guard exists to make an accidental ledger rewrite impossible. NEXT: give the pre-push guard the same baseline the gate uses (merge base with the upstream default branch), and add a regression test for the exact shape: branch, reconcile on main, merge main into branch, assert the guard passes; plus a second for deleting a pushed-but-unmerged branch-local request. STOP: do not relax what the guard checks -- the baseline is wrong, not the rule. | PR #2298 push attempts 2026-08-22/23; scripts/guard-push.mjs; scripts/check-ledger-write-discipline.mjs; commits 9a382a050 (#2294) and 7703188ef | 2026-08-22 | +| #J8SJQ9 | P2 | issue | Antipsychotic metabolic monitoring returns a source-backed stub instead of a written answer, and the eval case must not be relaxed to hide it | FOUND BY THE PACKET 2 CANARY, run 32589154243 (2026-08-22). "What metabolic monitoring is required for antipsychotics?" now returns the source-backed review stub — "The uploaded documents contain relevant guidance on metabolic monitoring for antipsychotics, but a full written answer could not be completed just now. Relevant document passages are cited below" — because the extractive candidate behind it was one of the two incoherent guidance-wrapper answers that #NPQJKP shipped a predicate to reject. The degradation is correct behaviour; the underlying defect it exposes is that the answer path cannot produce a usable written answer for this query at all. THIS IS NOT AN EVAL-CASE BUG AND MUST NOT BE FIXED BY ADDING acceptSourceOnly. All four cases carrying that flag document the same rationale: the corpus has no single authoritative source, so a source pointer is a legitimate answer, and quality-discharge-documentation deliberately drops mustContainAny for exactly that reason. quality-antipsychotic-metabolic-monitoring is the opposite case — it names expectedFiles ["MHSP.MetabolicScreening.pdf"], an authoritative source exists, and antipsychotic metabolic monitoring is a routine question a psychiatrist should get answered in prose. Adding the flag would silence a true signal. The targeting eval already grades it correctly at score 0 with reason "source-backed review stub", even though its mustContainAny ["metabolic", "monitor"] is satisfied by the stub text, so the instrument is working and only the answer is missing. FIRST DIAGNOSTIC STEP, because it splits the problem in two: determine whether generation was attempted for this case at all. The same run recorded provider_attempted:false for 11 of 30 targeting cases. If OpenAI was never called for this one, the cause is upstream of answer quality entirely (routing, admission control, or the retry ladder — note Packet 1 covers deadline admission control and should be checked for overlap before starting). If it was called and returned nothing usable, the cause is in extraction or generation for the medication_dose_risk class against this document. Do not begin work without checking Packet 1 and Packet 3 (#S4R2W3) for overlap: all three touch rag.ts, and overlapping changes ruin canary attribution. | canary run 32589154243; src/lib/rag/rag-eval-cases.ts quality-antipsychotic-metabolic-monitoring; ledger #NPQJKP | 2026-08-22 | +| #RVK6BJ | P3 | issue | Seven Claude Code sessions point at .claude/worktrees directories that are empty and not registered git worktrees, so those chats have no working copy | Measured 2026-08-22 while taking the fleet worktree inventory for #6GW95D. Seven directories under .claude/worktrees contain zero regular files AND are NOT registered git worktrees - none appears in git worktree list, and none has a .git entry: caring-contacts-phase-2a-a4f69a, database-test-queue-contention-6baedb, developer-button-settings-fb9b51, ed-care-plans-impl-7f44cd, phase-4-index-restoration-b0f4ea, vibrant-diffie-c93450 and wave-1-canary-s2-unlock-17d673. They are bare empty directories, neither worktree debris nor live checkouts. Each is the recorded cwd of an existing session in the session registry, titled respectively Suicide, Dev Drive, Developer, Care Plan, Database, Ward Flow and RAG; the Database one was still marked running. They were already empty before this session touched anything: the inventory counted files first and only then attempted removal, and all seven then refused with EPERM because a live process holds the directory handle open, so nothing was deleted from them here. No git history is at risk - every branch still exists at its recorded head. Impact is that resuming any of those chats operates on an empty directory. REPAIR, VERIFIED BY EXECUTION 2026-08-22 rather than assumed, because an earlier review comment and an automated fix both asserted the opposite: since the path is unregistered and the branch is checked out nowhere, git -C D:/Repos/Database worktree add succeeds directly into the existing empty directory - probed on a scratch copy with branch claude/ward-flow-phase-3-7eda97, exit 0, 3812 files checked out. Do NOT add -f: it is unnecessary here and it disables the branch-already-checked-out guard. Then run node scripts/setup-codex-worktree.mjs inside the directory to restore dependencies by byte-identical copy, about 90 seconds rather than an hour-long npm ci. Two conditions would change that command and should be re-checked first: if git worktree list ever DOES name the path, run git worktree prune before adding (the stale-registration path, still not -f); if the branch is checked out in another worktree, add under a different branch rather than using -f. Permit -f only with explicit owner approval after proving the existing worktree is stale and inactive. Worth understanding the cause before repairing them - something is emptying directories that sessions still treat as their cwd. | session 2026-08-22, GitHub issue #2270 Part A | 2026-08-22 | +| #CM7DG9 | P3 | issue | The Claude push-format-guard hook blocks every push from a linked worktree, because it compares core.hooksPath against the worktree root instead of the common git dir | Measured 2026-08-22 pushing from D:/Repos/Database/.claude/worktrees/. .claude/hooks/push-format-guard.sh only intends to act when the repository pre-push hook is NOT wired. It computes repo_root from CLAUDE_PROJECT_DIR (the linked worktree) and compares the resolved core.hooksPath against repo_root/.githooks. In a linked worktree core.hooksPath is the absolute path of the MAIN checkout's .githooks (D:/Repos/Database/.githooks), which never equals /.githooks, so the guard concludes the push is unguarded even though .githooks/pre-push is present, executable and does in fact run. It then performs a whole-tree prettier --check, which additionally flags git-IGNORED files: the only offender was .claude/settings.local.json, untracked and matched by a global gitignore, so it could not possibly be part of any push. Net effect: pushes from worktrees are blocked on a file that cannot be committed, and the documented CLAUDE_ALLOW_UNFORMATTED_PUSH=1 escape only works as the LEADING token of the whole shell command, so the natural 'cd && CLAUDE_ALLOW_... git push' form silently fails to unlock it. Three fixes, all small: resolve the repo root with git rev-parse --path-format=absolute --git-common-dir (or compare against the common dir's parent) so a linked worktree is recognised as guarded; restrict the fallback prettier check to tracked files (git ls-files) so ignored local state can never block a push; and mention the leading-token requirement in the block message itself. Related but distinct from the already-merged claude/push-guard-hookspath-relative work, which fixed relative and case-folded hooksPath values but not the worktree case. | session 2026-08-22, GitHub issue #2270 Part A | 2026-08-22 | +| #5MMK5R | P3 | issue | Document that a bare verify:phone-chrome run proves nothing about already-merged work, because its selector reads the working-tree diff and a clean tree selects no browser stage | Reporting/documentation guidance, NOT a verifier defect - reframed 2026-08-22 after review correctly refuted the original framing, and the refutation was checked against the code rather than accepted on assertion. tests/verify-phone-chrome.test.ts pins 'keeps documentation-only work out of browser suites' (docs-only input maps exactly to ['docs-index','docs-links']), so selecting no browser stage for a docs-only diff is the intended, tested contract. The original request proposed a named skip OR a non-zero advisory exit code; the non-zero half was wrong and would have broken legitimate docs-only verification and contradicted the affected-owner/journey selection contract. That suggestion is withdrawn. The residual hazard is real and is about REPORTING, not behaviour. Measured 2026-08-22 while producing the A2 evidence for issue #2270: asked to prove the browser gates against the merged Dictionary work, a bare `npm run verify:phone-chrome` on a main checkout with no tracked changes (the only untracked file was this inbox record, `docs/outstanding-issues-inbox/edebb730-91d9-42f5-bd93-ca2abb9678bc.json`) derived its inputs from the working-tree diff, found that untracked inbox JSON, printed 'Note: No phone-chrome-affecting file was detected; only prerequisite checks were selected', ran docs:check-index and docs:check-links, and exited 0. Every part of that is correct behaviour; the trap is that the exit code is identical to a full pass, so quoting it as 'verify:phone-chrome green' would have been a false green of exactly the class AGENTS.md's evidence rule exists to catch. The real gate only engaged after an explicit --files list of the merged scope (git diff --name-only 66594ddc1^1..main filtered to src/ and tests/ui-), which then selected lock-parity, runtime, contracts, focused-browser and full-ui and produced 133 contract tests, 12 focused phone journeys and 473 Chromium tests, all green. ACTION: add one line to docs/search-chrome-behaviour.md and to the gates skill stating that proving phone chrome for work already on main requires `-- --files ` (or a branch carrying those changes), because a bare run against a clean tree selects nothing and its exit 0 is not evidence about that work. Do NOT change the selector or the exit code. | session 2026-08-22, GitHub issue #2270 Part A | 2026-08-22 | ## Resolved / archive @@ -538,3 +545,4 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #99KVDF | rec | Do not re-attempt folding AnswerCard's support label inline into VerificationNotice — measured, it saves 4.5px, not a row | 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. | 2026-08-21 | | #0HFDWD | issue | CI change-scope reports UI_CHANGED false for changes that alter which modes render, so Production UI is skipped on user-facing work | 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. | 2026-08-21 | | #M6JNR8 | issue | Recommended-execution-queue rows cannot be re-graded by any ledger writer operation | 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. | 2026-08-22 | +| #1YPV51 | task | Reopen #318: the medication interaction lexicon clinical review and sign-off has not actually happened; the lexicon remains clinically unreviewed | Resolved 2026-08-22 by the Repository Lead's sign-off, recorded in the Sign-off block of docs/medication-interaction-lexicon-review.md. All 37 catalogue terms were expanded to their resolved drug lists and reviewed alongside the text of the rows each term fires on: 34 confirmed correct as they stood, 3 narrowed. (1) ANTIHISTAMINES: all four rows describe additive ANTICHOLINERGIC burden and one names 'sedating antihistamines' in its own words, yet cetirizine, fexofenadine and loratadine resolved into the term -- benzatropine + loratadine produced a CRITICAL 'anticholinergic toxidrome ... risk of toxic megacolon' alert and oxybutynin + cetirizine 'frank delirium and bowel impaction'. The three second-generation agents are denied; cyclizine, promethazine, alimemazine and diphenhydramine are kept. (2) CORTICOSTEROIDS: every row is a systemic effect (insulin resistance x5, tendon rupture with ciprofloxacin, additive hypokalaemia), so the four purely topical glucocorticoids are denied; inhaled agents are deliberately kept because the hypokalaemia row concerns a formoterol inhaler and names high-dose corticosteroids, and fludrocortisone is kept as a mineralocorticoid that raises BSL and drives hypokalaemia. (3) ORAL-CONTRACEPTIVES: the rows concern enzyme inducers destroying the COMBINED pill, and depot medroxyprogesterone is the method a woman is switched TO on an inducer; its own catalogue row already states inducers 'accelerate the clearance of the oral tablets, but the massive 150mg IM depot is generally resistant to clinically failing from this', so the generic term was flattening a nuance the catalogue records correctly. It is removed from the term and its own row still fires, pinned by a test. Two classification questions answered: moclobemide correctly excluded from maois (the rows concern irreversible inhibition -- tyramine crisis and washout -- which a reversible inhibitor does not carry), atomoxetine correctly classed SNRI. Measured: 23 medications changed, removals only, nothing added anywhere, all 328 medications intact, resolved/unresolved unchanged at 392/133. Each change was mutation-tested: reverted, observed to fail 3/4/1 tests, restored. Also fixed: the report generator hardcoded 'Status: UNREVIEWED', so the document would have contradicted itself in its first sentence the moment the sign-off was filled in; the status line is now derived from the recorded sign-off on the same read in both the write and --check paths. HISTORY CARRIED FROM CANCELLED REQUEST 296db779 (2026-08-21, partial progress): loperamide was resolving as an opioid because the selector matches the substring 'Opioid' and the catalogue classifies it 'Peripheral Opioid Agonist'; it is deny-listed alongside naltrexone and naloxone. Four further changes were proposed and withdrawn that day -- adding moclobemide to maois, removing atomoxetine from snris, folding the Z-drugs into benzodiazepines, and splitting antihistamines by sedation. THREE OF THOSE WITHDRAWALS STILL STAND. The fourth was REVERSED on 2026-08-22 and applied: the original withdrawal reasoned only that splitting would stop the term meaning 'antihistamines', which never examined what the rows say; reading them settled it. Recorded so the reversal is visible as evidence-driven rather than a change of mind. HOW THIS REVIEW WAS DONE, stated because this row exists over an earlier closure that claimed a review that had not happened: an assistant analysis proposed a verdict per term with its evidence, and the Repository Lead read and accepted them. It is a reviewed-and-accepted sign-off, NOT an independent re-derivation from primary sources, and must not be described as one. NOT RESOLVED BY THIS, and filed as its own row: acei resolves to perindopril alone, arbs to candesartan alone, statins to atorvastatin and rosuvastatin, because ramipril, lisinopril, irbesartan and simvastatin are absent from the catalogue entirely -- a patient on one of them produces silence rather than safety, which on screen is indistinguishable from a clear check. | 2026-08-22 | diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs index 912844e3e..bf65a0cb6 100644 --- a/scripts/check-docs-links.mjs +++ b/scripts/check-docs-links.mjs @@ -20,11 +20,17 @@ * Blocking for maintained docs: runs in verify:cheap and CI. Historical * directories and dated point-in-time records stay excluded unless --all is * requested, so preserved history cannot block unrelated PRs. + * + * Outstanding-issues inbox citations are special: an immutable request is + * queued at `docs/outstanding-issues-inbox/.json` and, after reconcile, + * lives at `docs/outstanding-issues-inbox/applied/.json`. Ledger rows + * (and the request's own source/detail) keep citing the pending path because + * the JSON is immutable. Treat the applied sibling as the same file. */ import { existsSync, readFileSync, readdirSync } from "node:fs"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { applyRequestBatch, validateRequest } from "./ledger-inbox.mjs"; @@ -66,10 +72,28 @@ const VERBATIM_DIRS = new Set(["codex-cloud-review"]); const APP_ROUTE_GROUPS = ["(search-app)"]; const OUTSTANDING_ISSUES = "docs/outstanding-issues.md"; const OUTSTANDING_ISSUES_INBOX = "docs/outstanding-issues-inbox"; +const INBOX_REQUEST_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i; + +/** + * Pending inbox UUID paths keep being cited after reconcile moves the file + * into `applied/`. Return that applied sibling, or null when the path is not + * a pending inbox request citation. + */ +export function appliedInboxFallbackPath(repoRelative) { + const cleaned = repoRelative.replace(/\/$/, ""); + const prefix = `${OUTSTANDING_ISSUES_INBOX}/`; + const appliedPrefix = `${OUTSTANDING_ISSUES_INBOX}/applied/`; + if (!cleaned.startsWith(prefix) || cleaned.startsWith(appliedPrefix)) return null; + const name = cleaned.slice(prefix.length); + if (name.includes("/") || !INBOX_REQUEST_NAME.test(name)) return null; + return `${appliedPrefix}${name}`; +} function repoPathExists(repoRelative) { const cleaned = repoRelative.replace(/\/$/, ""); if (existsSync(path.join(repoRoot, cleaned))) return true; + const applied = appliedInboxFallbackPath(cleaned); + if (applied && existsSync(path.join(repoRoot, applied))) return true; if (!cleaned.startsWith("src/app/") || cleaned.includes("src/app/(")) return false; const appRelative = cleaned.slice("src/app/".length); @@ -176,68 +200,73 @@ function isExternalLink(value) { return /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(value) || value.startsWith("#"); } -let missing = 0; -let checked = 0; - -for (const target of defaultTargets()) { - const absoluteTarget = path.join(repoRoot, target); - if (!existsSync(absoluteTarget)) continue; - const markdown = markdownForTarget(target, absoluteTarget); - const targetDir = path.posix.dirname(target); - const failures = []; - - const check = (repoRelative, label) => { - if (ALLOWLIST.has(repoRelative)) return; - checked += 1; - if (!repoPathExists(repoRelative)) failures.push(label); - }; - - // Inline code spans: repo-root-relative repo paths. - for (const rawCandidate of codeSpanCandidates(markdown)) { - const value = stripSuffixes(rawCandidate); - const base = ROOT_PREFIXES.some((prefix) => value.startsWith(prefix)) ? globBaseDir(value) : null; - if (base !== null) { - if (ALLOWLIST.has(value)) continue; +function main() { + let missing = 0; + let checked = 0; + + for (const target of defaultTargets()) { + const absoluteTarget = path.join(repoRoot, target); + if (!existsSync(absoluteTarget)) continue; + const markdown = markdownForTarget(target, absoluteTarget); + const targetDir = path.posix.dirname(target); + const failures = []; + + const check = (repoRelative, label) => { + if (ALLOWLIST.has(repoRelative)) return; checked += 1; - if (!existsSync(path.join(repoRoot, base))) failures.push(`${value} (glob base '${base}' missing)`); - continue; + if (!repoPathExists(repoRelative)) failures.push(label); + }; + + // Inline code spans: repo-root-relative repo paths. + for (const rawCandidate of codeSpanCandidates(markdown)) { + const value = stripSuffixes(rawCandidate); + const base = ROOT_PREFIXES.some((prefix) => value.startsWith(prefix)) ? globBaseDir(value) : null; + if (base !== null) { + if (ALLOWLIST.has(value)) continue; + checked += 1; + if (!existsSync(path.join(repoRoot, base))) failures.push(`${value} (glob base '${base}' missing)`); + continue; + } + if (!looksLikeRootPath(value)) continue; + check(value, value); } - if (!looksLikeRootPath(value)) continue; - check(value, value); - } - // Markdown link targets: repo docs use both repo-root-relative targets - // (`src/lib/env.ts`) and file-relative targets (`codebase-index.md`, - // `../AGENTS.md`). Accept whichever resolves, confined to the repository. - for (const rawCandidate of linkCandidates(markdown)) { - if (isExternalLink(rawCandidate)) continue; - const value = stripSuffixes(rawCandidate); - if (value === "" || value.includes("*") || /[<>{}$\\]/.test(value) || /\s/.test(value)) continue; - const relative = path.posix.normalize(path.posix.join(targetDir === "." ? "" : targetDir, value)); - if (relative.startsWith("..")) { + // Markdown link targets: repo docs use both repo-root-relative targets + // (`src/lib/env.ts`) and file-relative targets (`codebase-index.md`, + // `../AGENTS.md`). Accept whichever resolves, confined to the repository. + for (const rawCandidate of linkCandidates(markdown)) { + if (isExternalLink(rawCandidate)) continue; + const value = stripSuffixes(rawCandidate); + if (value === "" || value.includes("*") || /[<>{}$\\]/.test(value) || /\s/.test(value)) continue; + const relative = path.posix.normalize(path.posix.join(targetDir === "." ? "" : targetDir, value)); + if (relative.startsWith("..")) { + checked += 1; + failures.push(`${rawCandidate} (escapes repository root)`); + continue; + } + const rootStyle = path.posix.normalize(value); + const candidates = rootStyle === relative || rootStyle.startsWith("..") ? [relative] : [rootStyle, relative]; + if (candidates.some((candidate) => ALLOWLIST.has(candidate))) continue; checked += 1; - failures.push(`${rawCandidate} (escapes repository root)`); - continue; + const found = candidates.some((candidate) => repoPathExists(candidate)); + if (!found) + failures.push(rawCandidate === relative ? relative : `${rawCandidate} (tried ${candidates.join(", ")})`); + } + + if (failures.length > 0) { + missing += failures.length; + console.error(`\n${target}:`); + for (const failure of failures) console.error(` MISSING ${failure}`); } - const rootStyle = path.posix.normalize(value); - const candidates = rootStyle === relative || rootStyle.startsWith("..") ? [relative] : [rootStyle, relative]; - if (candidates.some((candidate) => ALLOWLIST.has(candidate))) continue; - checked += 1; - const found = candidates.some((candidate) => repoPathExists(candidate)); - if (!found) - failures.push(rawCandidate === relative ? relative : `${rawCandidate} (tried ${candidates.join(", ")})`); } - if (failures.length > 0) { - missing += failures.length; - console.error(`\n${target}:`); - for (const failure of failures) console.error(` MISSING ${failure}`); + if (missing > 0) { + console.error(`\ndocs link check FAILED: ${missing} missing path(s) across ${checked} checked references.`); + process.exit(1); } -} -if (missing > 0) { - console.error(`\ndocs link check FAILED: ${missing} missing path(s) across ${checked} checked references.`); - process.exit(1); + console.log(`docs link check passed: ${checked} repo path references resolve.`); } -console.log(`docs link check passed: ${checked} repo path references resolve.`); +const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; +if (invokedDirectly) main(); diff --git a/scripts/ci-change-scope.mjs b/scripts/ci-change-scope.mjs index d15ab2b9e..f6c6b0149 100644 --- a/scripts/ci-change-scope.mjs +++ b/scripts/ci-change-scope.mjs @@ -270,6 +270,15 @@ const perfExclusionPatterns = [ "src/instrumentation.ts", "src/sentry.server.config.ts", "src/sentry.edge.config.ts", + // Developer-hub payload only. `src/lib/developer-area/ledger-snapshot.ts` + // imports this JSON, and the only route importers are under + // `src/app/mockups/development/` (already excluded; 404 in production). A + // ledger reconcile that closes the last P1 must not pay a 7-minute + // Lighthouse budget run, and must not fail merge on TBT noise from + // `/documents/search`. Measured on PR #2302: this file alone flipped + // perf_changed and the job failed mobile TBT +32.7% against a baseline + // the same change cannot move. + "data/outstanding-issues-snapshot.json", ]; function isPerfChangedPath(filePath) { @@ -983,6 +992,12 @@ function selfTest() { ["public/therapy-compass-data/therapies-home.json", "data/medications-snapshot.json"], { perf_changed: true }, ); + // Mockup-only ledger snapshot: same `data/` root as medications, but it + // cannot reach a budgeted route. Closing the last P1 on PR #2302 otherwise + // forced Lighthouse onto a docs/ledger reconcile. + assertScope("perf-off-for-outstanding-issues-snapshot", ["data/outstanding-issues-snapshot.json"], { + perf_changed: false, + }); assertScope("perf-on-for-build-config", ["next.config.ts", "postcss.config.mjs", "tsconfig.json"], { perf_changed: true, }); diff --git a/tests/check-docs-links.test.ts b/tests/check-docs-links.test.ts new file mode 100644 index 000000000..c25234504 --- /dev/null +++ b/tests/check-docs-links.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { appliedInboxFallbackPath } from "../scripts/check-docs-links.mjs"; + +describe("appliedInboxFallbackPath", () => { + it("maps a pending inbox UUID citation to the applied sibling", () => { + expect(appliedInboxFallbackPath("docs/outstanding-issues-inbox/edebb730-91d9-42f5-bd93-ca2abb9678bc.json")).toBe( + "docs/outstanding-issues-inbox/applied/edebb730-91d9-42f5-bd93-ca2abb9678bc.json", + ); + }); + + it("does not wrap an already-applied path or a nested inbox file", () => { + expect( + appliedInboxFallbackPath("docs/outstanding-issues-inbox/applied/edebb730-91d9-42f5-bd93-ca2abb9678bc.json"), + ).toBeNull(); + expect(appliedInboxFallbackPath("docs/outstanding-issues-inbox/README.md")).toBeNull(); + expect(appliedInboxFallbackPath("docs/outstanding-issues.md")).toBeNull(); + expect(appliedInboxFallbackPath("docs/outstanding-issues-inbox/not-a-uuid.json")).toBeNull(); + }); +}); diff --git a/tests/developer-hub-page.dom.test.tsx b/tests/developer-hub-page.dom.test.tsx index bd64ee094..a91cbd694 100644 --- a/tests/developer-hub-page.dom.test.tsx +++ b/tests/developer-hub-page.dom.test.tsx @@ -34,8 +34,8 @@ vi.mock("@/components/account-data-provider", () => ({ * snapshot — so the band is exercised against the shape the route actually * loads rather than a hand-built fixture that could drift from it. `null` means * "do not override", which is also how the assertions below read the true - * count. The committed snapshot has `p1 === 2`, so without this the singular - * and zero branches are never reached at all. + * count. The committed snapshot can have `p1 === 0` (no open P1s), so the + * override is what reaches the singular and non-zero branches. */ const p1 = vi.hoisted(() => ({ value: null as number | null })); @@ -109,12 +109,17 @@ describe("developer hub page — synthetic-data warning", () => { describe("developer hub page — needs-you-now band", () => { it("reports the snapshot's own P1 count", () => { // No override, so this is the real committed snapshot: the band must agree - // with the data the route actually loads, not merely with itself. + // with the data the route actually loads, not merely with itself. When the + // snapshot has no P1s the page must omit the band rather than render a + // settled-looking "0 blocking items" line — that is the same contract as + // the explicit zero-override case below. const { counts } = loadLedgerSnapshot(); - expect(counts.p1).toBeGreaterThan(0); - render(); - expect(screen.getByTestId("developer-hub-needs-you-now")).toHaveTextContent(String(counts.p1)); + if (counts.p1 > 0) { + expect(screen.getByTestId("developer-hub-needs-you-now")).toHaveTextContent(String(counts.p1)); + } else { + expect(screen.queryByTestId("developer-hub-needs-you-now")).toBeNull(); + } }); it("carries no text beyond the computed count", () => { @@ -144,9 +149,9 @@ describe("developer hub page — needs-you-now band", () => { }); it("renders nothing rather than a reassuring all-clear when there are no blockers", () => { - // The committed snapshot has p1 = 2, so this branch is otherwise never - // exercised. A band reading "0 blocking items" would be a settled-looking - // statement about work the page cannot see. + // A band reading "0 blocking items" would be a settled-looking statement + // about work the page cannot see. The override keeps this branch explicit + // even on days the committed snapshot already has no P1s. p1.value = 0; render();