loki-mode 11.74.2 → 11.75.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +2 -3
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +10 -10
  5. package/autonomy/council-v2.sh +4 -4
  6. package/autonomy/grill.sh +1 -1
  7. package/autonomy/lib/ablation.py +22 -2
  8. package/autonomy/lib/browser-open.sh +1 -1
  9. package/autonomy/lib/chat.sh +1 -1
  10. package/autonomy/lib/cockpit-render.sh +1 -1
  11. package/autonomy/lib/done-recognition.sh +4 -4
  12. package/autonomy/lib/efficiency_cost.py +1 -1
  13. package/autonomy/lib/engine10-push.sh +2 -2
  14. package/autonomy/lib/prd-enrich.sh +1 -1
  15. package/autonomy/lib/proof-check.sh +1 -1
  16. package/autonomy/lib/proof-generator.py +3 -3
  17. package/autonomy/lib/proof-pr.sh +1 -1
  18. package/autonomy/lib/skill-link-heal.sh +1 -1
  19. package/autonomy/lib/voter-agents.sh +1 -1
  20. package/autonomy/loki +26 -37
  21. package/autonomy/prd-checklist.sh +1 -1
  22. package/autonomy/receipt_jwt.py +1 -1
  23. package/autonomy/run.sh +28 -28
  24. package/autonomy/trigger-schedule.py +6 -414
  25. package/autonomy/tui.sh +1 -1
  26. package/autonomy/verify.sh +4 -4
  27. package/completions/loki.bash +0 -5
  28. package/dashboard/__init__.py +1 -1
  29. package/docs/ARCHITECTURE.md +1 -2
  30. package/docs/CLI-REFERENCE.md +1 -1
  31. package/docs/COMPONENTS.md +1 -1
  32. package/docs/INSTALLATION.md +1 -1
  33. package/docs/ROUTING-EVIDENCE.md +1 -2
  34. package/docs/V10-RESEARCH.md +1 -1
  35. package/docs/dev/architecture-reference.md +3 -7
  36. package/docs/dev/project-structure.md +1 -4
  37. package/docs/dev/release-checklist.md +13 -31
  38. package/docs/research-2026-07/OBSERVABILITY-TOOL-SPEC.md +1 -1
  39. package/docs/v11/MARKET-SLICES.md +1 -1
  40. package/docs/v11/REGISTRIES.md +1 -1
  41. package/docs/v11/ROUTER-1.md +2 -2
  42. package/loki-ts/dist/loki.js +2 -2
  43. package/mcp/__init__.py +1 -1
  44. package/package.json +2 -2
  45. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  46. package/skills/extending.md +2 -3
  47. package/skills/factory-operations.md +11 -18
  48. package/skills/release-cadence.md +2 -2
  49. package/web-app/dist/assets/{AdminPage-jkjilfbl.js → AdminPage-CTwi8SUX.js} +1 -1
  50. package/web-app/dist/assets/{Avatar-bd5JWkSJ.js → Avatar-DVtGtLkJ.js} +1 -1
  51. package/web-app/dist/assets/{Badge-DVRit2Xw.js → Badge-rwKzz0Pe.js} +1 -1
  52. package/web-app/dist/assets/{Button-BNAB-pVZ.js → Button-nIsPDfsQ.js} +1 -1
  53. package/web-app/dist/assets/{CockpitPage-DpG29h_w.js → CockpitPage-BCeDpoWa.js} +1 -1
  54. package/web-app/dist/assets/{ComparePage-Dasg5V41.js → ComparePage-CYIYanVq.js} +1 -1
  55. package/web-app/dist/assets/{ErrorBoundary-DJojse6b.js → ErrorBoundary-Bkz6HqWp.js} +1 -1
  56. package/web-app/dist/assets/{EvidenceReceiptPanel-Blxw7_u1.js → EvidenceReceiptPanel-DZT2Np4F.js} +1 -1
  57. package/web-app/dist/assets/{GitHubIssuesPanel-BBvSFj80.js → GitHubIssuesPanel-Bzla91Lu.js} +1 -1
  58. package/web-app/dist/assets/{GitHubPRsPanel-BCIKeSBg.js → GitHubPRsPanel-FjT-Xn5t.js} +1 -1
  59. package/web-app/dist/assets/{HomePage-C7d__oNG.js → HomePage-pXgmRGLt.js} +2 -2
  60. package/web-app/dist/assets/{LoginPage-BXljL3yY.js → LoginPage-CnmpE28B.js} +1 -1
  61. package/web-app/dist/assets/{MagicPage-Xjkts_yE.js → MagicPage-CuSeGsLq.js} +1 -1
  62. package/web-app/dist/assets/{MetricsPage-kQvVp-j2.js → MetricsPage-Bg3PkBOU.js} +1 -1
  63. package/web-app/dist/assets/{NotFoundPage-D-4ZlEaO.js → NotFoundPage-uzBtZNV-.js} +1 -1
  64. package/web-app/dist/assets/{ProjectPage-DfzR5RXP.js → ProjectPage-Dacki8Xr.js} +3 -3
  65. package/web-app/dist/assets/{ProjectsPage-DFDsWzkV.js → ProjectsPage-BRGjksXG.js} +1 -1
  66. package/web-app/dist/assets/{SettingsPage-CLPIoYT3.js → SettingsPage-BbNf3fEJ.js} +1 -1
  67. package/web-app/dist/assets/{ShowcasePage-PZKkyGeY.js → ShowcasePage-XX4830sX.js} +1 -1
  68. package/web-app/dist/assets/{SystemSettingsPage-BsBlcnzo.js → SystemSettingsPage-DOhQeXa6.js} +1 -1
  69. package/web-app/dist/assets/{TeamsPage-BSjgNhoU.js → TeamsPage-2reIRUp_.js} +1 -1
  70. package/web-app/dist/assets/{TemplatesPage-W2KeAzbg.js → TemplatesPage-BUsLEWHW.js} +1 -1
  71. package/web-app/dist/assets/{TerminalOutput-BjroTiCC.js → TerminalOutput-BrBDcgzC.js} +1 -1
  72. package/web-app/dist/assets/{activity-BXZ5YYes.js → activity-B6AZpgmb.js} +1 -1
  73. package/web-app/dist/assets/{bell-D1urp5pa.js → bell-CB792A7S.js} +1 -1
  74. package/web-app/dist/assets/{bot-Chedf2bF.js → bot-uQZwOFAn.js} +1 -1
  75. package/web-app/dist/assets/{check-DhaCuOCV.js → check-DrtG0lIZ.js} +1 -1
  76. package/web-app/dist/assets/{chevron-left-D7BpQyA6.js → chevron-left-CrsJSAqQ.js} +1 -1
  77. package/web-app/dist/assets/{circle-alert-DnGvQ_AQ.js → circle-alert-BUCly4p3.js} +1 -1
  78. package/web-app/dist/assets/{clock-CqfxWhHM.js → clock-B2IT9SnE.js} +1 -1
  79. package/web-app/dist/assets/{cloud-BKxp5AWB.js → cloud-CN6Ux7Q7.js} +1 -1
  80. package/web-app/dist/assets/{code-xml-BDQ6GBtt.js → code-xml-BEm8PWnK.js} +1 -1
  81. package/web-app/dist/assets/{database-BmpEx1y_.js → database-cHe5Fgu8.js} +1 -1
  82. package/web-app/dist/assets/{dollar-sign-DYNY9R_C.js → dollar-sign-BVmP4YrE.js} +1 -1
  83. package/web-app/dist/assets/{file-code-corner-BJi0IBFd.js → file-code-corner-D-m-NrwO.js} +1 -1
  84. package/web-app/dist/assets/{file-plus-Dqyb2bcQ.js → file-plus-6mCjIzAB.js} +1 -1
  85. package/web-app/dist/assets/{globe-DPh4RWfM.js → globe-vcWOujwU.js} +1 -1
  86. package/web-app/dist/assets/{hammer-B3xnuoA1.js → hammer-D4nZFpO2.js} +1 -1
  87. package/web-app/dist/assets/{index-BpX8DcTr.js → index-u8toJ43r.js} +3 -3
  88. package/web-app/dist/assets/{layers-BRCmyCW8.js → layers-CNhYLop8.js} +1 -1
  89. package/web-app/dist/assets/{loader-circle-n07Pky0M.js → loader-circle-Vl7XIMV6.js} +1 -1
  90. package/web-app/dist/assets/{lock-BWqOuQmS.js → lock-SW_KwAvf.js} +1 -1
  91. package/web-app/dist/assets/{package-BJlB_Z-Z.js → package-QPK-7Ej1.js} +1 -1
  92. package/web-app/dist/assets/{plus-oH17BZu2.js → plus-DUgAlgCB.js} +1 -1
  93. package/web-app/dist/assets/{refresh-cw-CPQ476LL.js → refresh-cw-LfUksPHq.js} +1 -1
  94. package/web-app/dist/assets/{rotate-ccw-7hLvOcnS.js → rotate-ccw-CMbuouVq.js} +1 -1
  95. package/web-app/dist/assets/{scroll-text-BCmLauQn.js → scroll-text-C_FxBmNQ.js} +1 -1
  96. package/web-app/dist/assets/{server-DfTfvW84.js → server-7UJe9Jqt.js} +1 -1
  97. package/web-app/dist/assets/{shield-alert-CaeGRq19.js → shield-alert-BMuu6fYB.js} +1 -1
  98. package/web-app/dist/assets/{thumbs-up-Dz-l2GPC.js → thumbs-up-Bhs0d2Gp.js} +1 -1
  99. package/web-app/dist/assets/{trash-2-B-OsDmSo.js → trash-2-BZKxv49w.js} +1 -1
  100. package/web-app/dist/assets/{trending-down-CXTFOxzq.js → trending-down-DKeCMZek.js} +1 -1
  101. package/web-app/dist/assets/{trending-up-FQEdqr3v.js → trending-up-43_9fShg.js} +1 -1
  102. package/web-app/dist/assets/{upload-CacWSNTg.js → upload-pV3irPEW.js} +1 -1
  103. package/web-app/dist/assets/{usePolling-cIjZ9qd2.js → usePolling-CoZQM-WL.js} +1 -1
  104. package/web-app/dist/assets/{user-DAouWFHi.js → user-CMl7dXvC.js} +1 -1
  105. package/web-app/dist/index.html +1 -1
  106. package/autonomy/company-seats.json +0 -82
  107. package/docs/COMPETITIVE-INTEL-2026-09.md +0 -444
  108. package/docs/FOUNDER-DECISIONS-TRUST-SEMANTICS-2026-08-01.md +0 -145
  109. package/docs/FOUNDER-STATUS-AND-DECISIONS-2026-07-01.md +0 -132
  110. package/docs/LOKI-10-BUILD-PROMPT.md +0 -350
  111. package/docs/STRATEGY-2026-2028.md +0 -124
  112. package/docs/V10-VISION.md +0 -161
  113. package/docs/v10/ADOPTION-PLAN.md +0 -375
  114. package/docs/v10/ARCHITECT-CUTS.md +0 -744
  115. package/docs/v10/BACKLOG.md +0 -231
  116. package/docs/v10/BOARD.md +0 -1403
  117. package/docs/v10/CONTROL-PLANE.md +0 -120
  118. package/docs/v10/CONTROL.md +0 -36
  119. package/docs/v10/CP-ASK-PLAN.md +0 -85
  120. package/docs/v10/CP-ENTERPRISE-UI.md +0 -537
  121. package/docs/v10/D51-PHASE-B.md +0 -120
  122. package/docs/v10/D61-SPEED.md +0 -105
  123. package/docs/v10/D63-CARDS.md +0 -354
  124. package/docs/v10/DECISIONS.md +0 -915
  125. package/docs/v10/E2E-D65.md +0 -123
  126. package/docs/v10/ENGINE-LAWS.md +0 -163
  127. package/docs/v10/ENGINE-MEASURE.md +0 -69
  128. package/docs/v10/ENGINE.md +0 -743
  129. package/docs/v10/FAILURE-CLASSES.md +0 -2026
  130. package/docs/v10/FC21B-FC22-PLAN.md +0 -107
  131. package/docs/v10/FIRST-RUN-GATE.md +0 -39
  132. package/docs/v10/FOUNDER-QUEUE.md +0 -29
  133. package/docs/v10/GAP-RESEARCH-1003.md +0 -32
  134. package/docs/v10/GUARDS.md +0 -883
  135. package/docs/v10/L0-WAVE1.md +0 -81
  136. package/docs/v10/LEGACY-REMOVAL.md +0 -408
  137. package/docs/v10/MEDIUM-ANALYSIS.md +0 -346
  138. package/docs/v10/METRICS.md +0 -645
  139. package/docs/v10/MODERNIZE.md +0 -437
  140. package/docs/v10/OPENCODE-PARITY-PLAN.md +0 -84
  141. package/docs/v10/OPERATING-MODEL.md +0 -60
  142. package/docs/v10/PLAN-V12.md +0 -896
  143. package/docs/v10/PROGRESS.md +0 -2735
  144. package/docs/v10/RELEASE-11.md +0 -77
  145. package/docs/v10/RETRO-2026.md +0 -650
  146. package/docs/v10/ROADMAP-D63.md +0 -35
  147. package/docs/v10/SCALE.md +0 -140
  148. package/docs/v10/SCORECARD-PLAN.md +0 -250
  149. package/docs/v10/SWARM-PROMPT.md +0 -98
  150. package/docs/v10/SWARM.md +0 -293
  151. package/docs/v10/cp-redesign/PARITY.md +0 -188
  152. package/docs/v10/cp-redesign/legacy/INDEX.md +0 -1167
  153. package/docs/v10/roadmap-10/01-measurement.md +0 -113
  154. package/docs/v10/roadmap-10/02-cost.md +0 -75
  155. package/docs/v10/roadmap-10/03-speed.md +0 -65
  156. package/docs/v10/roadmap-10/04-first-run.md +0 -61
  157. package/docs/v10/roadmap-10/05-deploy.md +0 -61
  158. package/docs/v10/roadmap-10/06-stability.md +0 -64
  159. package/docs/v10/roadmap-10/07-quality.md +0 -57
  160. package/docs/v10/roadmap-10/08-proof.md +0 -48
  161. package/docs/v10/roadmap-10/09-safety.md +0 -74
  162. package/docs/v10/roadmap-10/README.md +0 -1464
  163. package/skills/sdlc-fleet.md +0 -170
  164. package/tools/cockpit/SPEC.md +0 -275
  165. package/tools/cockpit/index.html +0 -146
  166. package/tools/cockpit/roi-basis.json +0 -33
  167. package/tools/cockpit/server.py +0 -946
@@ -1,2026 +0,0 @@
1
- # Failure classes (D86)
2
-
3
- Each row answers five things:
4
- - what the user saw, compared with raw;
5
- - the law broken (docs/v10/ENGINE-LAWS.md);
6
- - a sibling sweep;
7
- - the one shared mechanism;
8
- - a regression fixture.
9
-
10
- No fix slice starts until its row exists (OPERATING-MODEL.md, Engine Laws line). Slice ids refer to the EL plan (Architect, 2026-10-03).
11
-
12
- L0 review rule (ENGINE-LAWS.md L0): a fix that adds an `if` or a regex about the user's repo shape, language, framework or task wording is rejected. The mechanism column names a model judgment (a prompt or schema) plus an execution check. Tamper checks, terminal I/O and Loki's own files are the only exceptions. Wave 1 slices: docs/v10/L0-WAVE1.md.
13
-
14
- ## FC-01 Verify ran from the repo root in a multi-root monorepo (FireLater#17)
15
- - User saw: "Failed Suites 1" three times on backend/tests/unit/validation.test.ts. Raw claude ran the suite from backend/ and passed 43/43 in about 2 min.
16
- - Law: L4.
17
- - Siblings:
18
- - verify.ts:50/142;
19
- - verify.ts:179 subtractBase;
20
- - wall.ts:83 base run;
21
- - verify.ts:224 tsconfig only at the root;
22
- - visual_evidence.ts:155-168;
23
- - deep.ts;
24
- - the brief's impacted commands;
25
- - deferred by the FC-01 stopgap (de12ad0cb): deep.ts full suite at the repo root, verify.ts tsconfig root-only, verify.ts ESLINT_CONFIGS root-only. All three are now closed (see Status).
26
- - Mechanism: Project Model packageRootOf/commandFor (EL-W1-01, EL-W1-04a/b). Interim: EL-W0-03.
27
- - Status (FIXED, lane E): the Project Model is ON by default (opt-out LOKI_E10_PROJECT_MODEL=0). One shared resolver, loki-ts/src/project_model/resolve.ts (loadProjectApi, siteFor, groupByPackage) plus verify.ts commandFor(t, repoDir, api), returns cwd and argv. A file owned by a package below the repo root runs in that package dir with a package-relative path. An unknown model, the opt-out, or a root-only (single-package) model resolves to the repo root exactly as before.
28
- - Sites now on the resolver: verify.ts test loop and subtractBase base run; verify.ts runLintChecks (tsc, eslint config lookup, cwd and file paths per package, check name lint:tsc:<root>); wall.ts RealBaseTestRunner; deep.ts runFullSuite (each package's own test command in its cwd); features/visual_evidence.ts (UI boot command, Playwright and e2e media from the UI package dir); implement brief (e10ext/context.ts prints `cd <pkg> && cmd`).
29
- - Deliberately root-only: verify.ts select-tests (scripts/select-tests.sh, Loki's own repo); ruff and shellcheck (take repo-relative paths from the root); wall_manifest.ts reads the root package.json for style hints (read only, not a runner invocation).
30
- - Fixture: tests/fixtures/firelater-17 plus the shape fixtures in benchmarks/tasks/shapes; tests/fixtures/monorepo-fc01 (backend with vitest, frontend, no root package.json) with loki-ts/tests/engine10/monorepo_cwd.test.ts (red at the repo root before, green after; includes the single-package regression).
31
-
32
- ## FC-02 A runner load error was treated as a code failure; two fix rounds, then STALLED
33
- - User saw: 11 min, $2.66 and a FAILED draft PR for a bug that did not exist. Raw: done in 2 min.
34
- - Law: L5 (and L1 on time and cost).
35
- - Siblings:
36
- - verify.ts:141-148 (a null summary counts as fail);
37
- - fix.ts:12-13;
38
- - machine.ts:171 stall;
39
- - wall.ts classify(), which already separates not_run (reuse it);
40
- - deep.ts;
41
- - vitest "No test files found, exiting with code 0" counts as a pass with n=0, when it should be not_run (EL-W1-06).
42
- - Mechanism: a result classifier with an owner (EL-W1-05). Only a FAIL owned by code drives fix and stall. Interim: EL-W0-02.
43
- - Fixture: tests/fixtures/runner-outputs/vitest/load-error.txt (from seq 56), plus siblings for each runner.
44
- - Status: FIXED (interim, FC-02 lane F). One shared classifier loki-ts/src/runner/runner_errors.ts (classifyRunnerOutput); verify.ts runCheck turns a load or collection error into not_run with owner harness (no rerun, no fix round, listed in verify not_proven); deep.ts runFullSuite does the same; wall.ts classify() already returned not_run for jest/vitest/bun with no parsed failure and is left unchanged. A real failed-test count in the same output keeps it a code failure; lint checks are never reclassified. Fixtures: loki-ts/tests/engine10/fixtures/runner-outputs/{vitest,pytest}, test loki-ts/tests/engine10/runner_errors.test.ts. Still open: the n=0 pass rule (EL-W1-06) and per-runner fixtures for jest, node:test, go, cargo.
45
- - Integration with FC-16 (2026-10-03): one mechanism identifies lint, typecheck and selector checks: `RunOpts.kind: "static"` (verify, deep and per-package paths alike); the FC-02 name-prefix exemption is removed. Load ownership is the single helper `harnessLoadReason` (runner/load_owner.ts), called from verify runCheck (kind test only), deep runFullSuite and project_model/package_suite.ts, which also routes results through FC-16 classifyCheck. Test: tests/engine10/monorepo_cwd.test.ts (FC-16 C1, lint kind blocks).
46
- - Sibling (2026-10-03): a change-introduced load error is a code fault. Base reproduction is necessary but not sufficient. The base rerun must be hermetic (an inherited editable install imports head code), and a check the task targets is never env-owned (7ad4a18b6, blocked in round 2). Superseded by L0-WAVE1 EL-W1-06: the harness gathers evidence and a separate reviewer call assigns the owner.
47
- - Ownership rule (2026-10-03, HIGH review of fd3ad2037): loki-ts/src/runner/load_owner.ts loadErrorIsHarnessOwned, one mechanism used by verify.ts runCheck AND deep.ts runFullSuite (sibling sweep done: wall.ts classify() already returns fail for a parsed failure). A load error is harness-owned not_run only when the same command reproduces a load error on a hermetic base worktree, no changed file is named in the output (path or module stem), the check is not a Wall or task-relevant test, and its target file is not itself changed; else it stays fail so fix rounds run. Patterns tightened (N1): command not found and Cannot find module match runner-load shapes only. Regressions: tests/engine10/runner_errors.test.ts (B1 SyntaxError in changed source, B2 Wall missing symbol, base-reproduced harness error, non-reproducing error, N1).
48
-
49
- ## FC-03 Scope control reverted in-scope route edits
50
- - User saw: the PR without the fix (routes/applications.ts, assets.ts and attachments.ts reverted).
51
- - Law: L2 (the work surface held destructive authority) and L3 (a heuristic beat executed evidence).
52
- - Siblings:
53
- - scope.ts:30;
54
- - unit_mode unitFence;
55
- - discard.ts;
56
- - commit_filter dropSet;
57
- - implement restoreReadOnly (trust: Wall tests, stays);
58
- - stop_restore.ts.
59
- - Mechanism: the destructive-call registry (EL-W1-08) plus advisory scope (EL-W0-01, shipped as d574c85c5 and a089aa9ab). Enforce as an opt-in comes back in EL-W1-10.
60
- - Fixture: replay of the FireLater diff. Every route file is kept, and NOT PROVEN lists them.
61
-
62
- ## FC-04 Loki gave less intelligence than raw
63
- - User saw: every session on sonnet, a 124s Wall that sealed zero tests, and fix rounds that never escalated.
64
- - Law: L1.
65
- - Siblings:
66
- - session.ts:27 defaults to sonnet;
67
- - sizing.ts:57/63/65 cascade;
68
- - fix.ts:63 escalation gate;
69
- - implement.ts:56 limit;
70
- - the plan stage on the "fast" tier;
71
- - no effort field in SessionRunOptions;
72
- - `loki start prd.md` still goes to the legacy run.sh route, whose default is sonnet (providers/claude.sh CLAUDE_DEFAULT_DEVELOPMENT).
73
- - Mechanism: a catalog best_default plus a top tier for each provider (EL-W0-06, EL-W0-07), and a size gate on the artifact (EL-W0-05).
74
- - Fixture: a unit test that the default run.started.model equals the catalog best_default, plus a parity replay row.
75
-
76
- ## FC-05 PR body printed "not recorded" for data the run had
77
- - User saw: "What the issue asked", "Why", "Files in scope" and "Tests" all empty.
78
- - Law: L7.
79
- - Siblings:
80
- - supervisor.ts:354 passes only seal;
81
- - receipt wall_s is 387s against about 654s of stages;
82
- - the issue comment body;
83
- - the Slack summary;
84
- - the --json envelope.
85
- - Mechanism: outputsFromEvents (EL-W0-08) plus output schemas (EL-W1-09).
86
- - Fixture: a golden from ~/git/FireLater/.loki/runs/e10-20261003T150744Z-59b1.
87
-
88
- ## FC-06 Control Plane showed "in progress" 5857s after run.completed
89
- - User saw: a run shown as live after it had ended.
90
- - Law: L6.
91
- - Siblings:
92
- - ship_hook.ts:27, an unref'd timer with no final flush;
93
- - supervisor.ts:155, a signal exit with no terminal event;
94
- - Slack finished;
95
- - LiveLine.
96
- - Mechanism: a flush on terminal events plus a dead-pid reconciler (EL-W0-04, EL-W0-09, EL-W1-07).
97
- - Fixture: tests/test-e10-kill-each-stage.sh plus a CP ingest test with a dead pid.
98
-
99
- ## FC-06b Control Plane run page said "No events yet" for a finished run (FireLater#17, e10-20261003T191822Z-7683)
100
- - User saw: "Live log: No events yet." on a completed run whose events.jsonl and stored events held every stage (105 events).
101
- - Law: L6 (a finished run must read as finished, with its evidence). Same FC-06 class: the page trusted a live-only path.
102
- - Siblings:
103
- - any UI reader that passes a sentinel the server validation rejects (`after=-1`) and maps every non-200 to an empty result;
104
- - fetchEvents swallowed the 400 and returned [], so the empty state was indistinguishable from "no events".
105
- - Mechanism: one shared `fetchEvents` that sends no `after` on the first page, pages until `has_more` is false, and is the only history loader; the live stream only appends.
106
- - Fixture: test/ui/run-finished.test.tsx ingests a finished run (test/fixtures/runs/verified-pr/events.jsonl) into the real server app and renders the page against it.
107
-
108
- ## FC-07 The test suite leaked fixture runs into the founder's real Control Plane
109
- - User saw: acme/widget and e10-t1.. runs in the real CP.
110
- - Law: none of L1 to L7 fits. Proposed L8, "Tests never touch real user state", is a founder decision. This is a recurrence of GUARDS.md 12 (~/.gitconfig).
111
- - Siblings:
112
- - discover.ts reads instance.json via $HOME;
113
- - the control.db path (control.ts:46);
114
- - ~/.loki keys;
115
- - the Slack webhook config;
116
- - the memory store.
117
- - Mechanism: a hermetic HOME prelude for every test (extend tests/lib/isolated-git-home.sh), plus a shipper that refuses temp and fixture repos. Cleanup: `loki control prune` (203d38544).
118
- - Fixture: a lint that fails any test reaching $HOME/.loki without the prelude.
119
-
120
- ## FC-07b Leaked fixture runs stayed in real Control Plane databases after the leak was closed
121
- - User saw: e10-t1..t7, e10-dw1..dw6, e37-cline, e37-codex (all acme/widget) in the real CP run list, with no step that removes them.
122
- - Law: L6 (the run list must show only real runs) and the rule that users never need to run a maintenance command. Same FC-07 class: closing the leak does not clean databases already polluted.
123
- - Siblings:
124
- - `loki control prune` exists but is manual, so every existing install keeps the rows;
125
- - local_repos rows whose realpath is under the OS temp dir mark the same fixtures when origin_repo is empty.
126
- - Mechanism: one startup step in createApp (`cleanupLeakedFixtures`): removes runs with origin acme/widget or a source path under the OS temp dir, audits each removal (action `fixture.cleanup`), and records a once-per-DB marker row (action `fixture.cleanup.done`) so it never repeats.
127
- - Fixture: test/server/fixture-cleanup.test.ts (first start removes and audits, second start removes nothing, real runs untouched).
128
-
129
- ## FC-08 A tampered receipt could render as VERIFIED in the UI
130
- - User saw: a VERIFIED badge on a run whose log failed integrity.
131
- - Law: L2 (trust fails closed) and L7.
132
- - Siblings:
133
- - the CP run list and run page;
134
- - the Slack summary;
135
- - the PR status;
136
- - `loki status`.
137
- - Mechanism: one verdict-display function that returns TAMPERED whenever the run is tampered or its seal is invalid (CPE-27).
138
- - Fixture: an ingested run with tamper.detected never shows VERIFIED in any surface.
139
- - Sibling follow-up (EL-FC08b): redact at source in the engine before hashing. Today the shipper (redactEvent) and the server (redactSecrets) rewrite event data before the CP can hash it, so an honest signed run holding a token reads UNVERIFIED ("log redacted before ingest; seal not checkable"), never TAMPERED and never VERIFIED. Redacting before the engine hashes and signs would make such runs checkable end to end. Status: built in Q17-05b (engine writer, shipper and ingest share redactEnvelope; redactSecrets iterates to a fixpoint so the layers produce the same bytes; a redaction-rewritten signed log now reads TAMPERED).
140
- - Sibling follow-up (deferred, not built in EL-FC08b): the shipper should send the sha256 of each line before redaction (alongside the redacted line). The CP could then check the seal over the original hashes and tell a genuine redaction from a downgrade (a FAILED log edited to PARTIAL plus a planted [REDACTED]); today such a log can only read "<verdict> (unattested)" or UNVERIFIED. This is the lasting fix for placeholder downgrades. REJECTED 2026-10-17 (Q17-05, HIGH r1 on 36524784d): a client-supplied original hash cannot be checked by the server, so a forger copies log.sealed events_sha256 into the shipped chain and a FAILED run displays VERIFIED; the chain is also unbound to the run id, so any signed seal replays onto any log. Rule: the CP never accepts a client-supplied hash in place of recomputing one over the bytes it received. The lasting fix is the line above: redact in the engine before hashing and signing (Q17-05b).
141
- - Sibling follow-up: the Slack summary, PR status and `loki status` do not yet read the CP effective_verdict.
142
-
143
- ## FC-09 A test-config edit outside scope can make broken code VERIFIED
144
- - User saw: nothing yet (reproduced by the D12 scope review with a stub provider). The agent broke `add`, edited bunfig.toml to preload a new setup.ts that mocks the module, and the verdict was VERIFIED. This happens at ffb58278b and at HEAD.
145
- - Law: L2 (trust fails closed).
146
- - Siblings:
147
- - CFG_ALWAYS in verify.ts (A-115) leaves out bunfig.toml;
148
- - new preload or setup files are never flagged (only edits to existing tests are);
149
- - the equivalents for other runners: vitest.config setupFiles, jest setupFiles and moduleNameMapper, pytest conftest.py, and go test flags in Makefiles.
150
- - Mechanism: one runner-config registry for each runner. Any added or changed file that it names (configs, preloads, setup files, conftest) caps the verdict below VERIFIED, with NOT PROVEN naming the file.
151
- - Fixture: a stub-provider repro for each runner (bun preload, vitest setupFiles, jest moduleNameMapper, pytest conftest).
152
- - Status (FC-09r): mechanism in `loki-ts/src/features/runner_config.ts`, read by verify.ts (testConfigChanged, base vs working tree) and seal.ts (runnerConfigNotes, base vs head, adds a NOT PROVEN line so the verdict cannot be VERIFIED). Fixtures: `loki-ts/tests/engine10/runner_config.test.ts` (real commit + seal stages).
153
- - Covered: bunfig.toml; vitest, vite, jest config and vitest.workspace; .mocharc; pytest.ini, tox.ini, conftest.py; pyproject.toml and setup.cfg pytest sections; .cargo/config.toml; preload, setupFiles, globalSetup and exact-file moduleNameMapper targets (jest rootDir resolved, explicit rootDir fails closed); `__mocks__/`; package.json scripts and resolution redirects; go.mod and go.work replace; Cargo.toml patch and replace; tsconfig paths; a changed config that fails to parse is capped, an unchanged broken one is not.
154
- - Deferred: transitive imports of setup files, pytest `-p` plugins and `pytest_plugins`, .mocharc `require`/`spec` targets, setupFiles given as identifiers or globs, Makefile go test flags.
155
- - FC-09w (P1, reproduced 2026-10-17 by the FC-09v r2 HIGH review): every RUNNER_CONFIGS pattern is case-sensitive, but macOS filesystems are case-insensitive, so adding `PNPM-Workspace.yaml` or `.YARNRC.yml` is not capped (notes 0) while real pnpm 11.1.3 reads the capitalized file. Siblings: every registry row (jest, vitest, bunfig, conftest and the rest). Mechanism: match case-insensitively across the whole registry in one place. Fixture: capitalized variants of each row cap in loki-ts/tests/engine10 runner_config tests. Also open (P2): patchedDependencies targets outside patches/ with non-.patch names, and .npmrc (registry=) are not in the registry.
156
-
157
- ## FC-10 Live line truncated to ~20 columns on a pty with no size
158
- - User saw: on a pty with no column size (script -q, CI, tmux before a resize, IDE terminals) the quiet-mode live status line was cut to about 20 characters, for example "[implement] implemen". Raw: the stage events were complete; only the rendering was wrong.
159
- - Law: L7 (outputs are contracts).
160
- - Siblings (every terminal width read, with its old fallback):
161
- - loki-ts/src/engine10/supervisor.ts:334 passed process.stdout.columns straight to LiveLine (undefined or 0);
162
- - loki-ts/src/e10ext/liveline.ts:35 used Math.max(20, (columns ?? 80) - 1), so 0 became 20 (the bug);
163
- - loki-ts/src/cockpit/cli.ts:73 used columns || $COLUMNS || 0 with no 80 default and accepted widths of 5 or more;
164
- - autonomy/tui.sh:27 used `tput cols || echo 80`, which accepts tput printing 0 or nothing;
165
- - packages/*/src: no terminal width reads.
166
- - Mechanism: one shared terminalWidth() helper (loki-ts/src/util/term_width.ts): a finite columns >= 40, else a valid $COLUMNS >= 40, else 80. Every TS read goes through it and the bash read applies the same rule.
167
- - Fixture: loki-ts/tests/engine10/term_width.test.ts (columns undefined, 0 and 20 give >= 80; the live line shows the full "[implement] implementing" text; a guard fails on any raw .columns read in src outside the helper).
168
-
169
- ## FC-11 Repo-wide structural guards went red after slices passed narrowed reviews
170
- - User saw: nothing yet; the 10.7.1 train was blocked on main 51e02b229 (2 failures in the full loki-ts bun test).
171
- - Law: L7 (the release is a contract), and the D44 gate.
172
- - Siblings:
173
- - spawn_env_guard (e10ext/ship_hook.ts:10 missing env);
174
- - the e10ext line budget (1509 of 1500), plus budgets that add up across slices (FC-10 +1, EL-W0-06 moved helpers to stay under the 5000 engine10 cap);
175
- - local-ci fast tier timing out at 600s while bun test alone takes about 434s.
176
- - Mechanism: every builder and reviewer brief names the structural guard set, and the full bun test runs in main after each merge batch, before the train.
177
- - Fixture: tests/engine10/budget.test.ts and tests/runner/spawn_env_guard.test.ts on the merged tree.
178
-
179
- ## FC-12 loki-ts bundles sibling-package code whose dependencies CI never installs
180
- - User saw: nothing yet; Bun Parity on train/104 (17ece536b) failed with `Could not resolve: "drizzle-orm/bun-sqlite"`. `loki control prune` (2c81d9603, 203d38544) imports packages/control-plane/src/db/migrate.ts, and Bun resolves a bare import from the importing file's directory, not from loki-ts/node_modules. The main checkout built cleanly only because packages/control-plane/node_modules happened to exist.
181
- - Law: L7 (the release is a contract: the build must reproduce from a clean checkout).
182
- - Siblings:
183
- - every workflow that runs `bun install` in loki-ts (test.yml, bun-parity.yml, coverage.yml, tier-a.yml, security-audit.yml, release.yml including its Docker `--production` job, nightly, parity-drift and mutation-testing);
184
- - the shipper imports in e10ext/ship_hook.ts and commands/control.ts, which have no external deps today but share the same path.
185
- - Mechanism: one `postinstall` in loki-ts/package.json installs packages/control-plane from its frozen lockfile, so every loki-ts install (CI, Docker, local) gets the sibling's deps. No per-workflow steps.
186
- - Fixture: a clean `git worktree add` at HEAD, then `bun install --frozen-lockfile && bun run build` in loki-ts, and again with `--production`. Both exit 0 and the dist is byte-identical to the committed one; without the postinstall the build exits 1 with the error above.
187
-
188
- ## FC-13 Tests pin a data value that a correct change is allowed to move
189
- - User saw: nothing; the full loki-ts bun test on merged main ca9b85b7f had 5 red in "budget: cache token pricing" after MW-1 correctly moved sonnet from 3/15/0.3/3.75 to 2/10/0.2/2.5. The expected totals (0.6617, 3.0) and tiers were literals, so the price change read as a cost bug. The MW-1 review was approved on the slice's own suites, without the full bun test (an FC-11 sibling).
190
- - Law: L7 (the release is a contract); a guard must fail on wrong behavior, not on a legitimate data change.
191
- - Siblings:
192
- - tests/dashboard/test_api_cost_cache.py stubs its own table, so it stays consistent, but its docstring says the other routes return 0.6617, which is stale (cosmetic);
193
- - tests/test-pricing-parity.sh (MW-1) compares routes against model-pricing.json, the correct shape;
194
- - the codex and gpt rows drift between run.sh and budget.ts (rv-mw1 follow-up), which is the same file-is-the-source rule.
195
- - Mechanism: arithmetic tests derive expectations from the single pricing source (loki-ts/data/model-pricing.json) and assert the loaded table equals the file; only the fixture token counts are literal. Pricing-table changes run the full bun test in main before the train (FC-11 mechanism).
196
- - Fixture: loki-ts/test/budget_cache_pricing.test.ts at 0de0838e1, 8 pass 0 fail; "cache tiers survive loading" compares the loaded PRICING with the file (loader mutation not yet run; follow-up).
197
-
198
- ## FC-14 A nested sandbox trusts an inherited pointer to the real environment
199
- - User saw: running the test runner truncated the founder real ~/.loki/keys/receipt-ed25519.pem. A fixture wrote through an inherited LOKI_REAL_HOME that still named the real home while HOME was a stand-in.
200
- - Law: L2 (trust fails closed).
201
- - Siblings:
202
- - loki_hermetic_home_enter (tests/lib/hermetic-home.sh) took LOKI_REAL_HOME from the environment;
203
- - loki-ts/tests/preload.ts used LOKI_REAL_HOME ?? HOME;
204
- - run_runner in tests/test-e154-e155-guards.sh kept the exported LOKI_REAL_HOME;
205
- - the outer E-154 key check compared names only, so truncating an existing key was invisible.
206
- - Mechanism: derive the real path from the current HOME, never from an inherited variable (enter unsets LOKI_REAL_HOME, preload reads HOME); the key check fingerprints name, size and mtime.
207
- - Fixture: tests/test-e154-e155-guards.sh (run_runner strips the pointers; t-keys writes only the runner-derived stand-in). A decoy-pointer probe and a bun HOME probe are owed after Oct 7 (B3, phase D vacuity).
208
-
209
- ## FC-15 A run judges "already done" against Loki's own unmerged work
210
- - User saw: FireLater#17 rerun on 10.7.1 (e10-20261003T185853Z-2d44) reported ALREADY_SATISFIED in 2m07s for $0.52. That was false. The run branched from the checked-out loki/e10-20261003T150744Z-59b1 (eda184e). That branch was the earlier FAILED run's unmerged draft PR #25, which also lacks the route edits the old scope control reverted.
211
- - Law: L4 (the base is a contract), L2 (trust fails closed).
212
- - Founder rule (2026-10-03):
213
- - Base precedence: an explicit CLI flag (add `--base` if none exists), then the loki.yaml base_branch setting (a branch name, `current` or `default`), then the user's checked-out branch.
214
- - The repo default branch is used only when it is configured as `default`. Never hardcode "main" (L0).
215
- - The start line prints `base: <branch> (<flag, config or checkout>)`.
216
- - The PR target is the configured target, else the repo default branch, and the PR body names it.
217
- - Siblings (swept 2026-10-03; every place a base or diff is computed):
218
- - loki-ts/src/engine10/stages/intake.ts: baseSha = rev-parse HEAD, the ALREADY_SATISFIED judge (checkAlreadyDone, closed-issue exit); FIXED, now refuses first;
219
- - loki-ts/src/engine10/worker.ts:49: ctx.baseSha = rev-parse HEAD (inherits intake's refusal, runs after it in the same checkout);
220
- - loki-ts/src/engine10/stages/seal.ts:111-116,190,256: staged diff, discardIfSatisfied, diff-tree and receipt base_sha, all against ctx.baseSha;
221
- - loki-ts/src/engine10/stages/verify.ts:63-67,103,176,238,287: changedFiles, testConfigChanged, the pre_red worktree at ctx.baseSha (the engine's only worktree use), inBase;
222
- - loki-ts/src/engine10/stages/deep.ts:134,236: council diff and changedFiles against ctx.baseSha;
223
- - loki-ts/src/engine10/supervisor.ts:221-224: PR diff-vs-base from intake's base_sha;
224
- - loki-ts/src/engine10/stages/wall.ts, modernize/presealed_wall.ts: Wall base run and already_satisfied count, fed by the same ctx.baseSha.
225
- All of them consume ctx.baseSha, which is HEAD after intake; one guard at intake covers them.
226
- - Mechanism (shipped, redesigned 2026-10-03): the guard sits on the CLAIM, not the run. A run always starts from the base the user asked for and is never refused for carrying commits that are not on the PR target. loki-ts/src/util/base_guard.ts resolveBase resolves the PR TARGET (LOKI_E10_BASE else origin default branch, fetched with a 10s non-interactive fetch, else local main/master; never loki/* or HEAD). unmergedEvidence compares the ALREADY_SATISFIED evidence paths (AlreadyDoneResult.paths) with `git diff --name-only <target> HEAD`: evidence that exists only in target..HEAD voids the claim, and the run proceeds to implement. When those commits are really Loki's (one carries the engine's "Loki-Run:" commit trailer, or a local .loki/runs receipt sealed head_sha != base_sha with commits of its own there; a branch merely named loki/* or a zero-commit receipt does not count) intake records data.unmerged_loki_work with the harness-owned note "work exists on <branch>, not on <target>; open or resume it" (L5; informational, never a refusal, never VERIFIED). The note is visible in three places: a stderr line when intake finishes and the run goes on to implement, the terminal summary (renderMainOutput via unmergedLokiWorkNote), and a "- Note:" line under "How it was tested" in the real PR body (e10ext/reviewer_body.ts renderReviewerBody, called by stages/pr.ts), unclipped with the evidence path list capped at 3 plus "(+N more)". When the commits are not Loki's the claim is dropped with one stderr line saying the evidence is on <branch> but not on <target>. The start line prints "PR target: <ref>, base: <branch>". No reflog is read. Placed in util/ because engine10 core is at its size cap (budget.test.ts).
227
- - Review fix (HIGH F1-F3, superseded by the redesign): the earlier refusal path and its reflog counting (including the R1 fresh-clone hole, where the oldest reflog entry is the tip itself) are removed rather than patched; with no refusal there is no wrongly-allowed run to close.
228
- - Still owed: a --base CLI flag, loki.yaml base_branch, the PR-target wording in the PR body (the start line now prints "PR target: <ref>, base: <branch>"); resolveBase already takes the explicit value via LOKI_E10_BASE.
229
- - Fixture: loki-ts/tests/engine10/intake.test.ts describe "base is not Loki's unmerged work (FC-15)" (origin + clone, local loki/e10-* branch committing the fix; without the guard intake returned already_satisfied from that unmerged commit, with it the claim is voided, the run completes and implements, and unmerged_loki_work is reported; control on main is not satisfied). Regressions A, B, B2 (no refusal, no claim), D (fresh clone of a pushed loki/* branch, 1 Loki commit: not refused, no claim, unmerged_loki_work.commits 1), a feature branch in a fresh clone (no refusal, not labelled Loki's), a loki/*-named branch with only the user's commit and a zero-commit receipt (neither labelled Loki's), the note present in renderMainOutput and renderReviewerBody, ALREADY_SATISFIED still reached when the work is on the target, and C (LOKI_E10_BASE=feature/search).
230
-
231
- ## FC-16 ALREADY_SATISFIED / VERIFIED with zero Loki-executed checks (n=0 counted as pass)
232
- - User saw: FireLater#17 on 10.7.1 recorded checks `"result":"pass","n":0` (zero tests executed counted as PASS). The same run had verify "checks": [] and changed_files []; the verdict rested only on the implement agent's self-report ("10/10 named impacted tests pass ... via npx vitest run").
233
- - Law: L0 and L3 (the model never grades its own work), L5 (not a pass: NOT PROVEN, owner harness); Seal accuracy.
234
- - Rule: a test check with n=0 executed, or a count that cannot be parsed, is not_run with reason "no tests executed". VERIFIED and ALREADY_SATISFIED require at least one Loki-executed check with n>0 and a pass; otherwise the seal verdict is PARTIAL (the engine's existing NOT PROVEN label) and `no tests executed` is listed in NOT PROVEN.
235
- - Mechanism (one shared module, loki-ts/src/util/check_result.ts): `testCount` (vitest, jest, bun test, pytest, go test -v (the engine passes -v; non-verbose is unmeasured), unittest, Playwright, cargo test, node --test, mocha; null = unknown), `classifyCheck` (kind test needs n>0 to pass; kind static = lint/typecheck/scan, decided by exit code), `hasExecutedProof`.
236
- - Sibling sweep (every site that set or consumed "pass"):
237
- - stages/verify.ts runCheck (was `n: ran(out) ?? 0` on a pass, and a null count passed): now classifyCheck. Lint, tsc, eslint, ruff, bash -n, shellcheck and select-tests are kind static.
238
- - stages/wall.ts classify (exit 0 was an unconditional pass, feeding base_run.pass and wallGreenOnBase): now classifyCheck; zero or unknown count is not_run.
239
- - stages/deep.ts runFullSuite (exit 0 was a pass): now classifyCheck, not_run lists NOT PROVEN. Council and secret-scan pass entries are static (no tests exist to count).
240
- - stages/seal.ts verdictOf (VERIFIED and every ALREADY_SATISFIED route: intake.already_satisfied, wallGreenOnBase, implement exit already_done): gated on `proof` (hasExecutedProof over verify's raw checks, or a Wall base run with n>0).
241
- - Consumers unchanged and now correct by construction: e10ext/select.ts, e10ext/reviewer_body.ts, features/pr_criteria.ts (they read result "pass", which now implies n>0 for test checks).
242
- - Not applicable: modernize/presealed_wall.ts (red-base proof, never a pass), runner/quality_gates.ts (gate stubs, not test runs).
243
- - Fixture: loki-ts/tests/engine10/check_result.test.ts (vitest `No test files found` and `Test Files 0`, pytest `no tests ran`, jest, cargo, mocha zero; go unparsed is unknown; real pass keeps n; verdict gate: VERIFIED and ALREADY_SATISFIED without proof are PARTIAL).
244
-
245
- ## FC-17 The Wall spends time and money and writes nothing
246
- - User saw: the same run's Wall produced "files": [] in 1m42s, with base_run 0/0/0. This is the D82-WALL0 class again.
247
- - Law: L5 (no work without evidence), cost.
248
- - Siblings: D82-WALL0 and any stage that can finish with zero artifacts and still bill.
249
- - Mechanism: if the Wall cannot write a check, it skips in under 10s with a NOT PROVEN note and never runs a model session to an empty result.
250
- - Fixture: loki-ts/tests/engine10/runner_errors.test.ts (FC-17 block). A repo with no runnable test command yields a Wall skip under 10s.
251
- - Status: FIXED for the no-runner case (lane G). wall.ts runWall prechecks, before any model session: a detected test map with no runner and no .py or .go file in the repo map returns stage.skipped with reason "no runnable test command detected: the Wall cannot write a runnable check". Still open: a Wall that has runners but writes nothing still bills its session (the SessionRunner has no progress hook for a no-progress cutoff); needs a session.ts change.
252
-
253
- ## FC-19 Loki's own stage rules talk a capable model out of doing the job
254
- - User saw: FireLater#17 run e10-20261003T200907Z-fea1 on 10.9.1 ended BLOCKED in 6m58s with a draft PR. The model's reason: "spec conflict: the task asks for all 37 route files to be migrated, validated and covered 100%, but the stage rules limit me to the named files and a few tests". Raw Claude Code finishes the same task in about 2 minutes. The user was told their spec conflicted; it was Loki's brief.
255
- - Law: L1 (never below raw), L0 (the harness may not shrink the model's job), L5 (a harness failure is reported as one, never as the user's spec conflict).
256
- - Siblings (limiting language swept, kept only where the stage is a read-only role or a trust rule):
257
- - Fixed: loki-ts/src/features/lean_prefix.ts STAGE_PREFIX and LEAN_PREFIX, now role-neutral (they also prefix the read-only plan brief and the Wall author brief, so no full-job text lives there).
258
- - Fixed: loki-ts/src/e10ext/context.ts FIXED_RULES, the implement brief only ("Run only the impacted tests named below", "Never run the full test suite, an E2E suite, or a long-lived server" removed; the full job is stated here).
259
- - Fixed: loki-ts/src/engine10/stages/implement.ts brief ("Impacted tests: none known" wording) and its fixed 480s implement limit (now derived from the run budget in machine.ts; deep mode now also gets its 1800s session limit, where session.ts used to kill it at 480s).
260
- - Kept, role-defining read-only stages: stages/plan.ts and engine10/already_done.ts ("do not edit any file", "do not run tests"); they never implement.
261
- - Kept, deliberate parallel fence: features/speed/unit_mode.ts (a decomposer-set write set, off unless LOKI_UNIT_SPEC).
262
- - Fix briefs reuse buildImplementBrief, so they inherit the same fix.
263
- - Mechanism: one shared constant brief (full job, trust rules only) plus one resume. No code inspects the model's wording (L0): on any LOKI_SPEC_CONFLICT from implement, util/conflict_resume.ts resumes the session exactly once with a correction ("Loki imposes no file or test limits; if your conflict is only about Loki's own rules, finish the task. If the task itself conflicts, restate the conflict."). If the model still declares a conflict the outcome is the normal BLOCKED with its one question (A-110). User work always keeps its PR or draft PR (E-67, L6).
264
- - Fixture: tests/engine10/conflict_resume.test.ts (a conflict that clears continues; one that persists stays BLOCKED with its question), tests/e10ext/full_job_brief.test.ts (limiting phrases gone, trust rules present, plan and Wall briefs carry no full-job wording), tests/engine10/machine.test.ts (deep implement session gets 1800s), tests/engine10/supervisor_backstop.test.ts (a PARTIAL seal after a failed implement stage still opens its PR).
265
-
266
- ## FC-20 The Control Plane home cannot start a new app and shows a bare "HTTP 404"
267
- - User saw: on the redesigned CP home the founder typed "create an async timezone page that gets time from a free source for the given location and displays sun movement for it in natural graphic generated as code", with the chips repo "server directory", provider claude and model haiku, and pressed Start. A red "HTTP 404" appeared under the composer, and no run started.
268
- - Law: L5 (an honest, harness-owned reason with a next step, never a raw status), L6 (a start request never fails silently), L1 (raw Claude Code starts a greenfield app from one sentence; Loki must too).
269
- - Siblings: every CP action that surfaces a raw HTTP status (start, stop, resume, retry, receipt fetch); a UI bundle served by an older CP process after an upgrade (version skew); the loopback guard rejecting a local request; any entry point that assumes a repo exists (CP composer, `loki start` with only a brief).
270
- - Mechanism: one shared CP request helper turns every non-2xx into a plain sentence (what failed, why, next step). The UI and server carry one version, and a stale CP process restarts, or refuses a newer UI with that sentence. Rescoped by the founder at 22:03Z: Loki is not a vibe coding tool, so the home box is not a build prompt; it becomes Ask Loki (BOARD CP-ASK), and greenfield-from-composer is dropped.
271
- - Fixture: owed. A forced 404 renders a sentence, not "HTTP 404"; a version-skewed UI gets the restart sentence.
272
-
273
- ## FC-21 A large task hits the implement time limit and the run skips verify, then reports two different outcomes
274
- - User saw: FireLater#17 on 10.10.3 (run e10-20261003T220847Z-78fa, steering 22:08Z). The model did the whole migration (83 files, +3999/-2476) and the backend suite then had 5855 pass, 3 fail. But implement stopped at "limit" after 639s under a fixed 900s run cap on a subscription run with no dollar cap. The run skipped verify and fix and went straight to commit and seal. The CLI printed "Outcome: FAILED, Reason: limit" while the receipt and run.completed said PARTIAL.
275
- - Law: L1 (raw Claude Code would have kept going and fixed the 3 failures), L2 (fail open on work: the work that exists is verified, never abandoned), L5 (the limit is a harness-owned reason), L7 (one source of truth for the outcome).
276
- - Siblings: every stage that can end at "limit" (plan, wall, implement, fix, deep); every caller that jumps from a stage failure straight to seal; every outcome printer (CLI summary, CP run page, PR body, Slack) that derives the verdict itself instead of reading receipt.verdict; FC-19 (a fixed time handicap on implement).
277
- - Mechanism: (a) a limit in implement still runs verify on the work that exists, plus the fix rounds the remaining budget allows, and never jumps to seal; (b) the run time budget scales with task size (sizing and the Project Model: files, packages), and subscription runs default to a generous wall cap that can be set in loki.yaml; (c) the implement session is told its remaining time so it can checkpoint with tests passing; (d) every outcome printer reads receipt.verdict.
278
- - Fixture: owed. A fake implement that hits the limit is followed by a verify stage event; the CLI summary equals receipt.verdict for VERIFIED, PARTIAL, FAILED and BLOCKED; a large sized task gets a larger cap than a small one.
279
- - Status: shipped in v10.10.5 (implement limit runs verify, cap scales, time note, PARTIAL outcome). Fixtures: loki-ts/tests/engine10/fc21_limit.test.ts and fc21_cap.test.ts. Follow-ups in BOARD FC-21b (earned VERIFIED after a limit, Project Model cap sizing, L7 outcome on a cap stop). Gate on 10.10.5: implement finished in 6m36s under cap 2700s and verify ran.
280
-
281
- ## FC-22 Impacted-test selection crosses package boundaries, and a selected package with no installed deps can never be proven
282
- - User saw: FireLater#17 on 10.10.5 (run e10-20261003T233414Z-30ca) ended PARTIAL although the backend-only change was correct (backend suite 5914 pass, 0 fail; tsc 0 errors). Verify had selected 4 frontend tests (page.test.tsx, Header, Sidebar, auth store) that do not import the backend; the clone had no frontend deps, so they could not load ("Cannot find module 'vitest/config'") and were NOT PROVEN.
283
- - Law: L0 and L4 (selection follows the repo's real structure, never a name or text guess), L1 (raw Claude Code would have run the backend suite and stopped), L5 (the NOT PROVEN reason was honest; FC-02 classified it correctly).
284
- - Siblings: every selector that maps changed files to tests (e10ext/select.ts, the Wall test map, deep full suite, package_suite per-package runs); every stage that runs a package's command without checking its deps are installed.
285
- - Mechanism: (a) impacted selection is scoped by the Project Model: the touched packages plus the packages that really depend on them (manifest or import edges), never every package in the repo. (b) when a selected package has no installed deps, run the Project Model's own install command for that package once, before verify, recorded as a check with its cwd; opt-out by config; never a guessed command.
286
- - Fixture: owed. The monorepo-fc01 fixture with a backend-only change selects no frontend test; a selected package without node_modules gets one recorded install check before its test runs.
287
-
288
- ## FC-23 The Wall writes a test that fails the package's own type check, and a fix round is spent on Loki's read-only file
289
- - User saw: the same run's Wall wrote a test that failed the package tsc (TS1470: import.meta is not allowed in CommonJS output). Fix round 1 was spent on it; the model correctly refused to edit Loki's read-only Wall file, so the round bought nothing and the check stayed red.
290
- - Law: L5 (a harness-authored failure is harness-owned, never charged to the user's work), L1 (no wasted model time), cost.
291
- - Siblings: every harness-authored file that later runs under the user's tooling (Wall tests, presealed Wall in modernize, generated fixtures); every place a fix round can target a file the model is forbidden to edit.
292
- - Mechanism: the Wall reads the package's module system and test conventions from the Project Model (type, module, test runner, import style) and writes to match; a lint or type error located inside a Wall file is harness-owned: the Wall file is regenerated once or discarded with a NOT PROVEN note, and no fix round is ever spent on it.
293
- - Fixture: loki-ts/tests/engine10/fc23_wall_conventions.test.ts (a CommonJS package: the Wall brief carries the convention and an import.meta Wall file is discarded with a NOT PROVEN note; a Wall-only tsc error yields not_run, owner harness, zero grouped failures; control: a user-code type error still fails and gets a fix round). Readers: loki-ts/src/project_model/conventions.ts, loki-ts/src/util/wall_owned.ts.
294
- - Update (FireLater#20 run e10-20261019T020704Z-103b, v11.17.67): the CommonJS brief told the Wall to use require(); the package's eslint (@typescript-eslint/no-require-imports, `eslint src` includes the Wall file) rejected it, so lint:eslint:backend was not_run (harness-owned) on every verify and the seal carried NOT PROVEN. Lint over a Wall file under src/ is correct (it ships in the user's tree); the brief must not instruct a construct the package lint forbids. The CommonJS brief now says to write import statements in TypeScript and keep require() for plain .js/.cjs. Fixture: fc23_wall_conventions.test.ts "CommonJS brief does not instruct require() in TypeScript". The run's STALLED outcome was NOT this check: the fix rounds chased `full suite: frontend` (156 failures identical on base), owned by FC-BASELINE-PER-PACKAGE (v11.17.68).
295
-
296
- ## FC-24 Agent-written code in the runner's process can forge a pass (summary and exit code)
297
- - User saw: nothing yet (found in the FC-16b round 2 review, 2026-10-04). Reproduced at f28b15d78 with pytest on python 3.14.6: a conftest.py `pytest_sessionfinish` hook writes "===== 5 passed in 0.01s =====" and calls `os._exit(0)`; the only test is `assert False`; classifyCheck returns {"result":"pass","n":5}. The Go form (init or TestMain prints RUN and PASS for a failing test, then os.Exit(0)) defeated every name check in FC-16b.
298
- - Law: L2 (fail closed on trust), L3 (what ran is the evidence); Seal accuracy.
299
- - Siblings (code-path reasoning, only pytest and go reproduced): unittest (atexit or os._exit before the real trailer), jest and vitest (config, globalSetup, setupFiles patch stdout or call process.exit(0)), bun test (preload), node --test (--import or --require hooks), cargo (the test binary is the code), npm test (an arbitrary script). A harness-owned reporter or result file (pytest plugin, jest reporter, JUnit XML) is written inside the same process, so it is forgeable too. Modernize unit runner (M-15, modernize/unit.ts): junit rewrite via the argv path plus atexit, and TestReport.from_item_and_call wrap, both reproduced PROVEN at c85b1a443 in all 4 configs. Modernize equivalence case runner and stub ablation (runNew/runStub) run unit code in the case process with cases.jsonl readable (code-path reasoning, not reproduced).
300
- - Mechanism: none closes it per runner. The runner's summary and exit code come from a process that runs agent-written code. Decision: a test pass is recorded as runner-reported, not tamper-proof (a known L2 limit). Go exit 0 is never a pass (FC-16b, not_run, owner harness, honest L5 reason) because Go has no parseable summary that survives the forgery at lower cost. Candidate later mechanisms: harness re-runs each named Wall test in a fresh process with harness-controlled entry and flags (setup code still runs), and the receipt states the limit in its evidence text. M-15 records conformance as trust "runner_reported" / limit "FC-24" in UnitRunResult.conformance and in the unit.conformance_passed event.
301
- - Fixture: the FC-16b forgery fixtures (go init/TestMain early exit asserts not_run); the unit.test.ts describe "FC-24 known limit: in-process forgery is runner_reported, not detected (documenting, not a fix)" (B1 and B2, 4 cases, plus controls).
302
-
303
- ## FC-25 A git call from the token-holding supervisor runs a plant in the agent's repo config (moat P9 red since v10.10.4)
304
- - User saw: nothing visible; the moat job of Tests has failed on main since 4fa4e8015 (runs 37160286598 through 37165257952, all "REGRESSION P9.injection-cannot-reach-token": "a plant in the agent's repo config ran holding the real GitHub token: fsmonitor"). 65ce2060b was the last green. The range touched no bash, only loki-ts.
305
- - Law: L2 (fail closed on trust); moat P9 Rule of Two.
306
- - Suspect: FC-21 (881f1275f) added util/run_cap.ts resolveRunCapS, called at engine10/supervisor.ts:330 before the worker spawns. It runs `git ls-files` in repoDir with env process.env, which still holds the token. Local probe (git, 2026-10-04): a repo with core.fsmonitor set to a recorder script fires it on plain `git ls-files` with GH_TOKEN in env; `git diff --name-only` and `git status --porcelain` did not fire in that probe.
307
- - Siblings: every git call made by a process that holds the token (supervisor, CLI before withholding) with cwd in the agent's repo: run_cap.ts, runner/load_owner.ts changedSince and worktree add (when called outside the worker), and any later addition. supervisor.ts:206 and e10ext/commit_filter.ts already pass core.fsmonitor=false and core.hooksPath=/dev/null.
308
- - Mechanism: one shared helper for git in the agent's repo from a trusted process: hardened config (core.fsmonitor=false, core.hooksPath=/dev/null, and no credential helper) plus a token-free env; plus a static guard that fails on a git spawn in loki-ts/src that does not use it, or is not listed with a reason.
309
- - Fixture: a bun test that plants core.fsmonitor and a canary GH_TOKEN, then calls resolveRunCapS and asserts the plant never ran; and the moat case P9.injection-cannot-reach-token passes locally (`bash tests/moat/run.sh`, P9 line).
310
- - FC-25b (sibling, found in the opus review, 2026-10-04T01:02Z): the hooks, fsmonitor and sshCommand flag set alone still lets three plant classes run holding the canary token (git 2.55.0). (1) filter.<drv>.clean or process, declared in .gitattributes or .git/info/attributes, fires on status --porcelain, diff --name-only HEAD, add and commit. (2) diff.<drv>.textconv fires on log -p and on a diff with patch output. (3) gpg.program with commit.gpgSign=true fires on commit. A scrubbed env does not close the hole, because a running plant can still read the gh hosts file or the keyring. Execution itself has to be stopped. Mechanism: the same safeGit helper blanks each configured filter and textconv key, and sets diff.external=, commit.gpgSign=false, core.attributesFile=/dev/null and GIT_ATTR_SOURCE=<empty tree>. Fixture: fc25_safe_git.test.ts adds one plant per class.
311
- - FC-25c (follow-ups from the opus review of d82aec15a, 2026-10-04T01:15Z; not reachable with a real token today). (a) The allowToken path still honors repo-local core.sshCommand, remote.*.uploadpack and receivepack, and a repo-scope credential.helper; the canary fired on an ssh origin and on a local-path uploadpack. Its only caller is the base_guard fetch, reached from intake in the worker with the sentinel env, and supervisor.ts baseLine passes fetchRemote=false. Fix: harden allowToken itself, or add a guard that pins it to that single worker-side caller. (b) The raw-spawn guard misses an indirect argv (`const GIT = "git"`) and does not scan packages/control-plane. ship_hook.ts reaches control-plane shipper/discover.ts gitOrigin (`git config --get`, real-token env) from the supervisor. Fix: convert it to safeGit and widen the guard. (c) preflight's safeGitArgv path does not set GIT_CONFIG_NOSYSTEM. Low risk, because system config is root-owned.
312
- - FC-25d (moat P9 [planted] red on main after v11.3.0, 2026-10-08; v11.3.0 itself passes). User saw: "[planted] a plant in the agent's repo config ran holding the real GitHub token: fsmonitor,fsmonitor,fsmonitor,fsmonitor" plus the SSH agent socket finding. Culprit: 93f22d197 (cost preview v1, merged via 939ca2992): supervisor.ts calls encodeEstimate(process.env, repoDir) before the worker spawns. Root cause: the supervisor now runs the cost preview (runner/router/cost_preview.ts estimateFor -> history.ts shapeKeyForRepo), which reached project_model/gather.ts listShallow (`git ls-files -z`, line 30) and isGitTracked (`git ls-files --error-unmatch`, line 107), both raw execFileSync with env: process.env. Each fired fsmonitor twice (trace2 parent sid: the top-level bun supervisor). The raw-spawn guard had gather.ts on its allowlist ("follow-up: confirm every caller is worker-side"). Law: L2 and moat P9 Rule of Two; the allowlist was a per-file trust claim that a later caller broke. Siblings: 41 raw git spawn sites across 21 files (the new guard on main e2f9c49ca) (engine10 stages, e10ext, runner, features, cli/completions, github_token); and preflight.ts and xreview.ts passed safeGitEnv() to util/shell.ts run(), which merges opts.env over process.env, so the token survived there too. Mechanism: util/safe_git.ts is the only way to spawn git (safeGit, safeGitSpawn for status/Buffer/stdin, safeGitRun async with the exact env, never merged); the guard tests/util/fc25_raw_spawn_guard.test.ts has NO allowlist, scans loki-ts/src, bin/ and autonomy/ JS/TS (multi-line, Bun.spawn, argv literals, `const X = "git"`), and fails on safeGitEnv/safeGitArgs used outside safe_git.ts. Content-writing worker calls (add, checkout, worktree add) opt in to repoDrivers so repo filters (local LFS) and the user's core.attributesFile still apply; the seal commit also opts in to userHooks (user hooks and commit signing, refused with allowToken). The token stays stripped and fsmonitor stays off on all of them. HIGH review B2: a core.fsmonitor plant cannot tell a token-free env from one merged over process.env (the -c flag disables it either way), so the env fixture records through a repoDrivers clean filter safe_git keeps live, for all three entry points, with an allowToken positive control. Patch commands get --no-ext-diff --no-textconv (FC-41, 7cf8028b0). Same mechanism, swept here: DRIVER_KEY_RE also blanks repo-scope diff.<drv>.command, and the subcommand finder skips the values of --config-env and --attr-source. Fixture: tests/util/fc25_supervisor_git_fsmonitor.test.ts (a child bun with a start-env canary calls shallowDirs, isGitTracked, gather, shapeKeyForRepo, estimateFor and the three helpers; the plant must never see the canary, while an explicit-env control must). Out of scope here: bash/python git calls in bin/ and autonomy/ (no JS/TS git spawns exist there), and packages/control-plane discover.ts/diffstat.ts/checkpoints.ts (FC-25c b).
313
-
314
- ## FC-26 Free text in the Control Plane starts a run in HOME that dies in 0.13s, and the run page says "did not pass verification"
315
- - User saw: runs cc22 and e606 on the founder's CP, both FAILED in about 0.13s, with a summary that blamed verification. The founder had typed "whats going on so far" in the home box. Raw `loki start` refuses a non-repo dir with a clear message.
316
- - Root cause (b-a3, 2026-10-04): the home box POSTed free text to /v1/start as `--brief`, with repo "lokesh" (HOME). planStart (packages/control-plane/src/server/spawn.ts:47) accepted any known repo path, HOME included, and `loki start --brief` exited before any stage ran. The run page summary had no branch for "stopped before verify".
317
- - Law: L2 (refuse unsafe targets before acting), L7 (the summary states the real stop reason).
318
- - Siblings: every CP entry that spawns `loki` (POST /v1/start, /v1/runs, retry); every summary line that is derived without a verify event; the stale-CP restart (a CP from an old install keeps the old behavior after upgrade).
319
- - Mechanism: one repoRefusal check in spawn.ts (refuse `/`, HOME and non-git dirs with a human reason) that every spawn path calls; one summaryLine that reads the terminal stop event; free text goes to Ask (read-only) and never to /v1/start.
320
- - Fixture: packages/control-plane test for repoRefusal (HOME, `/`, non-git, real repo); the A4b summaryLine test ("The run failed before verification ran." for a FAILED run with no verify event); the A3f cleanup test with cc22 and e606 look-alikes.
321
-
322
- ## FC-27 A short product brief skips the Wall because a tree-wide token looks like a named file
323
- - User saw: `loki start` with a one-line UI brief on a local repo (run e10-20261004T143536Z-2582) sized the task small, skipped Plan, and skipped the Wall ("small task with a relevant test"). The implementer edited test configuration, broke an existing test, and the run ended FAILED with no PR. Raw Claude Code would have kept planning against the repo instead of treating the brief as a covered one-file fix. The receipt also listed deferred deep checks, "repo not recorded" (no GitHub remote), and "playwright not resolvable"; those are separate output gaps, not this mechanism.
324
- - Law: L0 (the harness must not decide that an unnamed brief already names its files), L2 (sizing and the Wall are work surfaces: when the signal is not specific, keep the Wall).
325
- - Siblings: `selectRelevantFiles` (plan hints, decompose, implement brief context) still counts every overlapping token, on purpose. The only caller that turns that list into a skipped Wall is `speedLikelyFiles` via `hasRelevantTests` (plan.ts and wall.ts). `namedFiles` (basename in the task text) is unchanged and still lean-eligible.
326
- - Mechanism: one selector, `selectSpecificFiles`, drops tokens that occur in more than half the repo-map entries before scoring. `speedLikelyFiles` uses it. A distinctive symbol still selects its file. Plan hints stay on `selectRelevantFiles`.
327
- - Fixture: loki-ts/tests/engine10/sizing_lean.test.ts, "FC-27: a token on most paths is not a relevant test, so the Wall stays".
328
-
329
- ## FC-28 A short brief skips the Wall because a common word sits inside a longer symbol
330
- - User saw: the same one-line brief, rerun e10-20261004T210509Z-9f4f after 11.0.2, still skipped the Wall ("small task with a relevant test") and ended FAILED with no PR. The jest failure was a different test in the same file. FC-27 only dropped tokens that hit most of the tree. A shorter word still matched as a substring of one symbol, and that one file with a test was enough to skip the Wall. Raw Claude Code does not treat a common word inside a longer name as proof the change is already covered.
331
- - Law: L0 (the harness must not decide that an unnamed brief already names its files), L2 (when the signal is not an exact name, keep the Wall).
332
- - Siblings: `selectRelevantFiles` still uses substring overlap for plan hints. `namedFiles` still matches a basename written out in the task. FC-27's half-repo drop stays on the same selector.
333
- - Mechanism: `selectSpecificFiles` counts a task word only when it equals a path segment or a symbol. A word inside a longer name does not. `speedLikelyFiles` is still the only caller that can skip the Wall.
334
- - Fixture: loki-ts/tests/engine10/sizing_lean.test.ts, "FC-28: a word inside a longer symbol is not a relevant test, so the Wall stays".
335
-
336
- ## FC-29 A test silently depends on the caller's cwd
337
- - User saw: tests/test-proof-verify-jwks.sh passed 108/0 in 33s in a fresh worktree and hung until killed when run from the main checkout. The CI-style run was green and the founder's own run was red, so the suite read as flaky.
338
- - Mechanism (measured): `loki proof verify` and `loki_remote_verify_receipt` run `proof-verify.py "$pj" "${TARGET_DIR:-.}"`. With TARGET_DIR unset the verifier runs `compute_tree_digest` on the caller's cwd, whose filesystem walk visited 1,054,266 paths (13.3s just to enumerate, then it hashes them) because `.claude/worktrees` (190 worktrees, 32G) sits inside the checkout. A fresh worktree has a few thousand files.
339
- - Law: L2 (a verdict is about the tree it names, never an ambient one), and the test hermeticity rule: a test owns its fixture tree.
340
- - Siblings: swept every tests/*.sh spawn of `proof verify` and `loki_remote_verify_receipt`; only the jwks test lacked TARGET_DIR. Direct `proof-verify.py` runs pass the repo as an argument. Sibling sweep (11.2.0 lane 10): of every `${TARGET_DIR:-.}` reader in autonomy/, five spawn sites were unsafe: the Bun `loki proof verify` (loki-ts/src/commands/proof.ts, the default route for a plain `verify <id>`), the bash `loki proof verify` (human and JSON, reached by flags or LOKI_LEGACY_BASH), `loki_remote_verify_receipt` and `_deploy_receipt_verdict` all handed the verifier the caller's cwd (it digests the tree when cwd is a repo). Safe, each reads or writes fixed paths and walks nothing: cmd_deploy detectors (test -f on named files), cmd_failover (one failover.json), `loki handoff --md` (writes one HANDOFF.md), crash.sh (writes .loki/crash), spec-interrogation.sh (five named manifests). app-runner.sh and completion-council.sh run only inside a `loki start` run, where run.sh sets TARGET_DIR to the project.
341
- - Mechanism of the fix: every verifier spawn in the jwks test sets TARGET_DIR to its fixture; tests/test-verifier-spawn-sets-target-dir.sh fails any test that spawns the verifier without TARGET_DIR or a cd into an owned directory.
342
- - Shared mechanism for the sweep: `loki_verify_root` in autonomy/loki and its port `resolveVerifyRoot` in loki-ts/src/commands/proof.ts are the one resolver (TARGET_DIR, parent of an absolute LOKI_DIR, cwd or repo top-level holding .loki, else a plain refusal and exit 2 NOT CHECKED; HOME and / are never roots, a trailing slash is normalized first). The first sweep fixed only the bash copy; the HIGH review found the Bun route still defaulting to ".".
343
- - Sweep fixture: tests/test-verifier-root-not-ambient-cwd.sh (runs `proof verify` through bin/loki on both the default and legacy routes; source guards cover autonomy/loki and loki-ts/src) and loki-ts/tests/commands/proof_verify_root.test.ts (the TS resolver, red before the port). The bash source guard goes red when a spawn passes `${TARGET_DIR:-.}` again.
344
- - Fixture: tests/test-verifier-spawn-sets-target-dir.sh, mutated against the pre-fix jwks test (7 hits). tests/test-proof-verify-jwks.sh run with cwd = the main checkout.
345
-
346
- ## FC-30 An install artifact outlives the install that made it
347
- - User saw: after upgrading to 11.1.0, ~/.claude/skills/loki-mode was a symlink to ~/.bun/install/global/node_modules/loki-mode, which no longer existed. `loki doctor` FAILed and told the user to run `loki setup-skill`, an extra command the product should never ask for.
348
- - Mechanism (measured): `cmd_setup_skill` creates the provider skill links once and only on request. Nothing re-checked them on install, upgrade or first run, so a package-manager move or reinstall left a dangling link (or one to the previous install). Raw cause is a one-shot writer with no owner for the link's lifetime.
349
- - Law: L0 (the harness repairs what it can verify is its own instead of delegating to the user), L2 (the doctor verdict is about the host as it is, and the fix must make that true without a command).
350
- - Siblings: swept every place Loki writes an install-time path. Provider skill links ~/.claude, ~/.codex, ~/.cline, ~/.aider skills/loki-mode (all four covered by the shared mechanism); `cmd_remote` ln -sf of the Claude skill link in autonomy/loki (only creates when SKILL.md is absent, covered by the same repair on the next run); shell completions via `completion --postinstall` and the shim (already self-healing, stamped by VERSION in ~/.loki/completions-version); no PATH shims or bin links are written by Loki (the package manager owns bin/loki). Real directories and foreign links are deliberately left to doctor.
351
- - Mechanism of the fix: autonomy/lib/skill-link-heal.sh `loki_skill_link_heal` repoints a symlink only when its target is a Loki install path (node_modules/loki-mode or a Homebrew loki-mode dir) that is missing or is a different install than the running one, and only when the running root is itself an install path. Atomic temp link plus rename, one stderr line, silent no-op when correct, LOKI_NO_SKILL_LINK_HEAL=1 opts out. Called from bin/loki-postinstall.js (install and upgrade) and the bin/loki shim (first run).
352
- - Fixture: tests/test-skill-link-heal.sh (dangling, other install, real dir, foreign link, correct link, opt-out, dev checkout), red before autonomy/lib/skill-link-heal.sh existed.
353
- - Amendment (FC30-DURABLE): the repair repointed to WHATEVER install was running, so a one-off `npx loki-mode@x` or a scratch-prefix install would hijack the user's link to a path that later disappears and dangles again. Rule: repoint only when the running install is durable (under `npm root -g`, the bun global dir, or a Homebrew prefix). An npx cache (_npx), a temp dir or any other prefix is refused with one stderr line and the link is left untouched. The rule lives only in autonomy/lib/skill-link-heal.sh (`_loki_skill_root_durable`); there is no TS copy, and bin/loki-postinstall.js and the bin/loki shim both call it. The check runs only on the repair path, so the steady state stays subprocess-free.
354
- - Amendment siblings swept: other Loki writers that derive a user-owned path from the running install location: `cmd_remote` ln -sf of the Claude skill link (creates only when SKILL.md is absent; shares the same later repair, no repoint from the running root); shell completions (stamped by VERSION, content not install-path dependent); no PATH shim or bin link is written by Loki. No other self-repair writes from the running root.
355
- - Amendment fixture: tests/test-skill-link-heal.sh cases 8-12 (npx cache refused, temp or arbitrary prefix refused, brew prefix healed, bun global healed, foreign link untouched), red on the npx and temp cases before the fix (11 passed, 2 failed), with npm stubbed and HOME and HOMEBREW_PREFIX faked.
356
-
357
- ## FC-31 A slice gate skips the suites that guard its own files, and main goes red in Tier B
358
- - User saw: main 27447cf46 failed Tier B on tests/test-doctor-blocker-parity.sh (run 37697291383). The slice R1-20 (8ff988516) changed loki-ts/src/commands/doctor.ts and gated only on tsc and bun test. The local-ci fast tier had timed out (rc=124) on a loaded host, so no shell suite ran.
359
- - Law: L2 (a verdict is about the tree it names; a gate that did not run the guarding suites says nothing about them) and the evidence rule (a timeout is not a pass).
360
- - Cause (measured): scripts/select-tests.sh rule R4 handled loki-ts/src files with `continue` after selecting bun tests, so rule R3 (shell suites that reference the file) never ran for them. test-doctor-blocker-parity.sh runs `bin/loki doctor`, never the string doctor.ts, so no path or basename match could have found it anyway.
361
- - Mechanism: one shared mapping, scripts/select-tests.sh. R4 now also selects shell suites that name the full path, and for loki-ts/src/commands/X.ts those that run `loki X`. One gate command, `bash scripts/local-ci.sh --impacted <base>` (scripts/impacted-gate.sh), runs only the selected shell and moat suites, each under `timeout -k`, prints per-suite rc, and exits non-zero on any failure or timeout.
362
- - Siblings swept (file-to-suite mapping by source tree): loki-ts/src was the only tree whose rule skipped the shell grep (fixed). mcp/, providers/, autonomy/ (other than run.sh and loki), scripts/ get R3 path and basename grep. dashboard/ gets R3 plus the R5 pytest area. web-app/ gets R3 plus R5. Still thin: a loki-ts/src file that is not a command module (runner/, util/) maps to shell suites by full path only, not by the subcommand a suite drives; dashboard and web-app shell suites that drive an API route by URL are not mapped to the handler file; mcp/server.py tools are not mapped to suites that call the tool by name. Follow-up: a per-tree needle table once a second miss appears.
363
- - Fixture: tests/test-select-tests.sh, "FC-31: doctor.ts selects doctor parity shell suite" (red before the fix: 39 passed, 1 failed).
364
-
365
- ## FC-32 Hand-maintained file lists drift from the import graph
366
- - User saw: Tier B run 37706008817 on main 6a01413bd failed two suites: "no hardcoded home-directory paths in tests" (and its cascade, "Structural checks catch planted defects (D44-C)") and "Loki control plane wiring (CP-04)". Both slices had passed tsc and the full bun test.
367
- - Law: L2 (a verdict is about the tree it names; a gate that did not run the guarding suites says nothing about them).
368
- - Cause (measured): faf1d0ace added tests/test-parity-goal-score.sh with hardcoded /opt/homebrew/bin and /usr/local/bin grep paths (flagged by tests/test-no-hardcoded-paths.sh). The R1 router slices (0f3e3d55a, c15b8157b, 1b9e479c1) added loki-ts/src/engine10/cost.ts, runner/router/flag.ts and runner/router/route_block.ts to the control-plane value-import closure, but Dockerfile.control-plane lists its COPY files by hand. RC-SLICE-SHELL-GATE: the FC-31 impacted gate selected neither shell structural guard, including for a touched tests/*.sh.
369
- - Mechanism (P0 slice, interim): the parity test discovers greps with `type -ap grep ggrep` (no absolute paths); the three missing files were added to Dockerfile.control-plane.
370
- - Mechanism (root fix, 11.2.2 FC-32-ROOT): Dockerfile.control-plane now COPYs the whole loki-ts/src tree (2.4MB; the root Dockerfile already did). CP-04 (tests/test-control-plane.sh) no longer compares a list to the import graph: it stages exactly what the Dockerfile build stage COPYs (WORKDIR and relative or absolute destinations honored) and runs the real `bun build` of the server and ask-tools bundles there. Docker is not needed.
371
- - Why not the import-graph generator: a whole-tree COPY is viable (size 2.4MB, evidence: `du -sh loki-ts/src`), so the CTO's option 1 applies.
372
- - Regression fixtures (CP-04): the legacy per-file list (tests/fixtures/cp04-legacy-copy-list.txt) bundles the unmodified tree, goes RED when a brand-new loki-ts file is imported (needs a list edit), while the tree COPY passes with no edit; a really missing import target and a deleted loki-ts COPY both go red.
373
- - Gate selection (RC-SLICE-SHELL-GATE): scripts/select-tests.sh now selects tests/test-no-hardcoded-paths.sh and tests/test-structural-checks.sh for any changed tests/*.sh, and tests/test-control-plane.sh for any loki-ts/src file in the control-plane import closure (computed at selection time; python3 missing selects it for all loki-ts/src). Fixtures in tests/test-impacted-gate.sh (red before the fix: 2 failed on tests/*.sh; CP-04 red once the test stopped naming the files by accident).
374
- - Siblings swept: tests/test-no-hardcoded-paths.sh scans every tests/ file (3 passed, 0 failed after the fix); the CP-04 closure guard covers every loki-ts/src value import (25 passed, 0 failed); no other hits.
375
- - Sweep of hand-kept lists (root slice): package.json files[] has hand-listed web-app/{server,auth,models,crypto}.py and Dockerfile:146 the same four (a new web-app module would not ship; follow-up, change both to a glob together). tests/test-runtime-libs-are-packaged.sh hand-lists tools/*.py and autonomy/lib/*.py to assert (drift means a new tool is unasserted, not unshipped; follow-up: derive from the directory). tests/test-detectors-are-packaged.sh derives its list from run.sh (already the right shape). action.yml and .github/actions/*: no file lists. tests/test-release-dist-guard.sh: checks map content, no list.
376
- - Fixture: the two existing guards, red on 6a01413bd and green here.
377
-
378
- ## FC-33 Duplicate implementations kept in sync by tests (FC-DUP)
379
- - User saw: `loki doctor` printed different checks, counts and verdicts depending on the route (bash vs bun), which `bin/loki` picks from bun on PATH, LOKI_LEGACY_BASH, a missing dist, LOKI_TS_ENTRY or `--airgap`. Two parity tests (test-doctor-blocker-parity.sh, test-doctor-install-integrity-parity.sh) existed only to keep the two copies equal, and each new doctor check had to be written twice. Raw Claude Code has one doctor.
380
- - Law: L2 (one verdict has one author), L0 (the harness must not hold two answers to one question).
381
- - Siblings (twin sweep):
382
- - Command twins and their guards: version, provider, memory (loki-ts/tests/all-ported.test.ts); stats (tests/test-bash-bun-parity.sh check 6); proof verify (loki-ts/tests/proof_verify_parity.test.ts); crash (crash.test.ts:308); status (status.test.ts:398); rollback, trust, wiki (unguarded).
383
- - Runner twins: run.sh vs loki-ts/src/runner, guarded by test-bash-bun-parity.sh, test-parity-*.sh, the gate-failures-cap parity test and test-pricing-parity. goal_score.ts vs run.sh has NO guard. These collapse when the D57 default-loop flip lands.
384
- - Mechanism: `_loki_bun_delegate <cmd> "$@"` in autonomy/loki (bun resolved PATH then @oven/bun-*, dist then src, `version` preflight under 5s, then exec). Doctor is the first command moved onto it; bash keeps only a minimal "bun route unavailable" diagnostic. `cmd_control` is the precedent. Remaining twins move onto the same helper as their slices land.
385
- - Known gap: the delegate preflight only runs `<cli> version`. A throw inside doctor.ts AFTER the CLI loads (during the doctor run itself) is not covered by the preflight and surfaces as that crash, not the minimal fallback.
386
- - Fixture: tests/test-doctor-single-impl.sh.
387
-
388
- ## FC-34 Test imports a temp copy of src in-process under bun --coverage
389
- - User saw: Tier B on main 83109bc1a failed in "Bun tests on ubuntu-latest bun=1.3.13" (`cd loki-ts && bun test --coverage`): every test printed (pass), no (fail) line and no summary, the output stopped mid coverage table and the job ended "exit code 1". The table was full of /tmp/loki-rmx-*/src/... rows. Raw view: a green test list with a dead process.
390
- - Law: L2 (a verdict must be produced by the runner, not lost with it), L0 (the harness must not depend on a reporter surviving deleted files).
391
- - Cause (measured): loki-ts/tests/engine10/route_matrix_mutation.test.ts (e49c7d29f) copies src into a mkdtemp dir 31 times and route_matrix_lib.ts matrixViolations(root) dynamically imports those copies in the test process; afterAll removes the dirs before the coverage reporter reads them. macOS exits 0 but prints 2170 loki-rmx rows; Linux died after 4 dirs.
392
- - Siblings swept: grep of loki-ts/tests and loki-ts/test for in-process dynamic imports from tmp/mkdtemp roots and for cpSync tests that import. Only route_matrix_lib.ts (lines 57-60, 178-179) imports from a variable root. graph.test.ts and providers.test.ts copy fixtures, not src; providers.test.ts imports real src inside a spawned script. route_matrix.test.ts passes the real src dir, which coverage tracks normally. No other hits.
393
- - Mechanism: the mutation test evaluates each copy through `Bun.spawn([process.execPath, route_matrix_child.ts, root])` (no --coverage), reading violations as JSON. A non-zero exit or unparsable output throws, so a crashed child can never pass a red case or the control case vacuously.
394
- - Fixture: the "FC-34 guard" describe block in loki-ts/tests/engine10/route_matrix_mutation.test.ts (red when the file imports route_matrix_lib in-process or does `await import(join(root` against a temp root; verified red by reverting the fix locally).
395
-
396
- ## FC-35 The router start line, receipt and plan disagree about the route
397
- - User saw: CTO smoke with LOKI_ROUTER=1 on a node off-by-one fixture. The start line said "route: executor haiku-5.5, advisor opus: Opus routes at plan time", the receipt said routed:false, units:[], executor:null, "no route recorded by implement", every session ran claude-sonnet-5-5, no plan-scope.json was written (plan logged "plan sonnet (fast tier)"), and shape_key was null.
398
- - Law: L7 (what is advertised is what is recorded); also L2 (a null shape key feeds no history, R1-16) and founder design (Opus routes; Sonnet is the default executor; Haiku only when Opus assigns it).
399
- - Cause (measured): routeStartLine ignored all run data and defaulted the executor to haiku; the plan stage skipped small tasks even with the router on and ran the router-on plan on the fast tier (Sonnet) unless the advisor was unavailable; nothing recorded a route when the plan returned none, and the receipt read only implement.route, which no stage writes; shapeKeyForRepo was never called and ignores the run's project-model.answer.json.
400
- - Mechanism (one shared): runner/router/route_record.ts. The plan stage always writes <runDir>/route.json (a route, or routed:false with the reason) while the router is on. The receipt route block (buildRouteBlock, via plan output or route.json) and the PR line derive from that one record; the start line is printed before any stage, so it states the pre-plan state only ("decided at plan time, per-unit executors not applied") and never names an executor. The routing decision runs on Opus (Sonnet only after an Opus failure, recorded as NOT PROVEN) and no longer skips small tasks; since D93 the routing decision is one short structured-output Opus call and the full plan session is conditional (see Amended). shape_key comes from shapeKeyForRun (the run's project-model.answer.json, then the repo model). The "plan sonnet (fast tier)" downgrade is not printed while the router pins the plan to Opus.
401
- - Siblings swept: routeStartLine (supervisor.ts), buildRouteBlock (seal.ts), routePrLine/withSealRoute (pr.ts, via the sealed route_line), modelDowngrades (start line and run.started), doctor.ts router check (reads no executor); all other prints of an executor name go through modelLabel in route_block.ts. Router-off output is byte-identical (router_optout_golden plus the new off assertions).
402
- - Parked: implement does not yet apply per-unit executors (R1-11 wiring). The record carries applied:false (PER_UNIT_EXECUTORS_APPLIED), so the receipt and PR line say "<run model> (per-unit assignment not applied)", list units as "assigned haiku-5.5 (not applied)" and add a route.executor NOT PROVEN line; none claims haiku ran. plan_model records the model the fast tier really resolves to (fastTierModel), and the plan downgrade is suppressed only when the plan is really pinned to Opus.
403
- - Fixture: loki-ts/tests/engine10/route_record_agreement.test.ts (red before the fix).
404
- - Amended (D93, CTO, 2026-10-08): under LOKI_ROUTER=1 Opus ALWAYS makes the routing decision, as one short structured-output call (cache-stable prefix, effort low, output {size: trivial|small|medium|large, units[], needs_full_plan: bool}, measured target under $0.01). The full Opus plan session runs only when needs_full_plan is true; trivial and small tasks go straight to the routed executor on the lean path. The implement LOKI_NEED_PLAN marker is a second input to the same plan gate (it can raise the gate, never lower it). R-B is unchanged: the plan stage always emits a route (route.json) or a recorded reason; a failed or malformed routing call records routed:false with the reason and falls back to the full plan. Slice: COST-HALF LD-01 (docs/v11/COST-HALF.md); its Wall check requires the routing call below 10% of total cost on the trivial-sum B9 row.
405
-
406
- ## FC-39 A hand-kept file list drifts from the import graph (second instance, in a fixture)
407
- - User saw: train-11.3.1 red at 1613125eb: tests/test-control-plane.sh "legacy fixture sanity failed" (24 passed, 1 failed). The RABC and T1 slices added loki-ts imports (route_record.ts, cost_preview.ts, pr_lessons.ts and their closure) that tests/fixtures/cp04-legacy-copy-list.txt did not list.
408
- - Law: L2 (a verdict is about the tree it names). FC-32-ROOT removed the hand-kept list from the product Dockerfile but left the same class alive in the test fixture.
409
- - Cause (measured): the fixture was a hand-kept per-file COPY list; a sweep of the import closure of its entry points found 11 unlisted files (cache, repomap, provider_failover, runner/types, cost_preview, history, history_store, atomic, engine_origin, pr_lessons plus route_record).
410
- - Mechanism (one shared): the legacy list is derived at fixture time from the import closure of serve.ts and tools_server.ts (legacy_copy_list in tests/test-control-plane.sh); the checked-in list file is deleted. The red-first case still holds because plant_import runs after derivation.
411
- - Siblings swept (tests/ and scripts/): the derived walk itself must not drop files: it maps .js/.mjs specifiers to .ts (latent class: no file in the serve.ts/tools_server.ts closure imports a .js or .mjs specifier today, so this guards future imports; the real tree list is unchanged), skips comment-only lines, and fails loudly naming any relative specifier it cannot resolve (fixtures in tests/test-control-plane.sh, red when the .js mapping is removed). No other hand-kept loki-ts/src file list. Remaining lists are not source-closure lists: tests/detect-test-mutations.sh HARNESS_FILES, tests/test-registration-nonstandard.sh CORPUS_FILES, scripts/license-audit.sh PACKAGE_FILES, tests/managed_memory/test_sdk_isolation.sh ALLOWLIST. Open (from FC-32, unchanged): Dockerfile:146 and package.json files[] hand-list web-app/{server,auth,models,crypto}.py.
412
- - Fixture: tests/test-control-plane.sh (25 passed, 0 failed; the sanity case is green by construction and cannot drift).
413
-
414
- ## FC-37 A flag accepted on one route is silently dropped on another
415
- - User saw: `loki start "<task>" --no-pr --attempts 2` returned rc 0 and VERIFIED but ran ONE attempt, with no attempts receipt and no second worktree. `bin/loki` sent the positional task to engine10 before the --attempts diversion, and engine10 glued `--attempts 2` onto the task text. The same shape hid `--budget`: engine10 has no such flag, so `--budget 3` became task words and no cap applied.
416
- - Law: L0 (a flag the user typed is honored or refused, never ignored), L2 (one dispatch point owns each flag).
417
- - Siblings (sweep of start flags by route; routes: engine10 positional, Bun start.ts, bash cmd_start, attempts engine10 child):
418
- - `--attempts`: engine10 positional was DROPPED (fixed); Bun route honored; bash route accepted only 1; attempts child n/a. Now parsed once in bin/loki before any route split: 1 is stripped, 2-5 exec the Bun start command, anything else or a missing value exits 2, N>=2 without bun exits 1.
419
- - `--budget` / `--budget-limit`: engine10 positional was DROPPED into the task (fixed: translated to `--max-cost`); Bun and bash routes honored; attempts child DROPPED it (fixed: passes `--max-cost`).
420
- - `--no-pr`: engine10 honored; Bun route rejected it as unknown (fixed: accepted as a no-op, the Bun loop opens no PR); bash route refuses it with `Unknown option` and exit 1 (loud, kept); attempts child always passes it.
421
- - `--provider`: engine10, Bun and bash routes honored; attempts child forwards it.
422
- - Mechanism: one flag block ahead of the route split in `bin/loki` (`# FC-37`), plus the engine10 arm's budget translation and the attempts child argv in loki-ts/src/commands/start.ts.
423
- - Known gap: a new start flag still needs an entry in each route; the sweep is a table here, not a generated check.
424
- - Fixture: loki-ts/tests/runner/attempts-dispatch.test.ts (stub engine; red on ffe808687 with 0 pass 3 fail, green here with 3 pass 0 fail).
425
-
426
- ## FC-38 Legacy loop reachable from a user command
427
- - User saw: `loki start "<task>" --attempts 2` exited rc 0 but the attempts receipt showed requested 2, ran 1, winner null, governor "hold: governor unknown or unreadable". The one run was the legacy RARV loop (runAutonomous), which seals no receipt, so a start returned success with nothing to verify. The swarm usage governor, absent in a clean environment, forced the fallback.
428
- - Law: L0 (a user command that says it verified must have run the verifying engine), L2 (one engine behind `start`).
429
- - Siblings (sweep of every `start` route):
430
- - `bin/loki` engine10 arm (issue refs, multi-word tasks): engine10, kept; now also hard (never falls through to bash).
431
- - `bin/loki` Bun start (PRD path, one-word brief, flags): ran runAutonomous (fixed: runs engine10 per attempt, N=1 included).
432
- - `bin/loki` no-bun and early fallbacks to `$BASH_CLI`: ran the bash loop (fixed: `_loki_bash_or_refuse` and the no-bun check refuse `start` with exit 1).
433
- - `bin/loki` legacy-only flags (--parallel, --github, --sandbox, --issue, --dry-run, --detach) and the opencode provider: diverted to the bash loop (fixed: refused, exit 2).
434
- - `LOKI_SDK_LOOP` branch in `bin/loki`: removed.
435
- - attempts child (loki-ts/src/commands/start.ts): engine10 only; the governor (`LOKI_ATTEMPTS_GOVERNOR_MAX`, usage files) is deleted from product code, N attempts run as N (1-5).
436
- - `loki start` flags only the legacy loop honored (--max-iterations, --max-retries, --completion-promise, --base-wait, --max-wait, --simple, --complex, --allow-haiku, --regen*, --skip-memory, --aider-model, --aider-flags, --cline-model): were accepted by the Bun start and dropped (FC-37 class). Now refused with exit 2; the accepted set (--provider, --budget, --session-model, --prd, --brief, --attempts, --no-pr, no-op --yes/--no-plan/--no-mirofish/--no-dashboard) all reach engine10.
437
- - `autonomy/loki` `cmd_start` callers (quick and run at 11335/11337/11826, demo at 15382, the `start` dispatch at 20928, 29705/29716), `autonomy/run.sh` run directly, and the dashboard spawns of run.sh (dashboard/control.py:572, dashboard/server.py:4356, 4640, 5226): NOT a `loki start` route, still reach the legacy loop. OPEN: requires a CTO waiver or a follow-up slice; ~dozens of shell suites drive cmd_start/run.sh directly with stubs, so refusing there is its own slice.
438
- - Mechanism: `bin/loki` start block (`# FC-38`) plus `_loki_bash_or_refuse`; `runStart` always goes through `runAttempts` with an engine10 runner; `formatAttemptsSummary` prints requested, ran, winner and losers from the attempts receipt.
439
- - Scoping (11.3.1, FC38-SCOPE): the refusals above apply only to `start --attempts` (in any form). Plain `loki start` keeps the 11.3.0 routing (bash fallbacks, `LOKI_LEGACY_BASH`, opencode and legacy-only flags on bash) because the unscoped change regressed P5.egress-blocked-start-seal-verify and P5.local-provider-no-claude-sidecalls. `--attempts N` (2-5) is checked for legacy-only flags and unsupported providers before it execs the Bun start. Fixtures: tests/test-engine10-dispatch.sh and loki-ts/tests/runner/attempts-dispatch.test.ts.
440
- - Missed sibling (FC38-SCOPE2): FC38-SCOPE scoped only `bin/loki`. The Bun start (loki-ts/src/commands/start.ts) still refused the runner flags (--max-iterations and the rest) and ran engine10 on every plain start, so P9's bun legs (`dist/loki.js start injected-prd.md --max-iterations 2`) exited 2. Fixed: start.ts is the 11.3.0 file for every plain start (`startEngine` returns "runner", runAutonomous); the refusal, the 1-5 check, the FC-40 --no-pr rule and the token-free env apply only when --attempts is present. Fixture: loki-ts/tests/commands/start.test.ts ("FC-38 scope" block) and the two `--budget` pins in tests/test-engine10-dispatch.sh.
441
- - Separate finding (not FC-38, OPEN): P9.injection-cannot-reach-token still fails on the planted leg with start.ts restored. The fsmonitor plant runs holding the real GitHub token and the real SSH agent socket on `bin/loki start octocat/hello#42` (the engine10 issue route), before engine10 withholds tokens. Passes on bee8ac363, fails on 4b24c4f55; the regressing commit is in that 49-commit range, not yet bisected.
442
- - Sweep closed (T5-D2): `--attempts` (any value, either form) is refused with exit 2 and a pointer to `loki start --attempts N --no-pr` by `bin/loki` for quick, run, demo and tour (before any route split), by `autonomy/loki` main() for start, run, quick, demo and tour (so every `cmd_start` caller, including quick/run/demo and the dispatch at 29705/29716, is covered), and by the `autonomy/run.sh` argument parser (the target of the dashboard spawns in dashboard/control.py and dashboard/server.py, which carry no attempts field and now cannot forward one). Plain invocations without `--attempts` are unchanged. Fixture: tests/test-engine10-dispatch.sh section 4b (stub provider marker, one case per entry point).
443
- - Fixture: loki-ts/tests/runner/attempts-dispatch.test.ts and loki-ts/tests/runner/attempts.test.ts (red on e7be7289b: dispatch 1 pass 6 fail, unit 16 pass 10 fail; green here).
444
-
445
- ## FC-DOCTOR-COUNT Doctor's failure count changed between identical runs (provisional id; Release Manager renumbers)
446
- - User saw: `loki doctor` printed "2 failed" on one run and "1 failed" on the next, same environment. Raw cause: the Claude login row depended on a live `claude auth status` subprocess (5s timeout, no retry). When it returned nothing, doctor fell back to a clock-dependent check of the cached OAuth access token's expiry and printed FAIL "login has EXPIRED" (an expired access token is normal; a refresh renews it). Reproduced on 11.2.2 with a claude shim whose probe prints nothing plus an expired credentials file: "19 passed, 2 failed"; probe answering: "1 failed".
447
- - Law: L5 (an inconclusive probe was reported as a user fault) and L3 (a heuristic on the clock beat executed evidence).
448
- - Siblings swept: text Summary vs `--json` summary (two separate hand-kept counters), 39 hand-written `tally.X++` sites, and the Blocking (N) trailer (read the same counters). Other probes checked: tool version probes (a timeout leaves version null, which passes, so no count change), readEffectiveProvider (deterministic; failure degrades to warn), disk, skills, detectors, PATH installs (all local and deterministic). Under 16 concurrent doctor runs the counts were identical, so the flip needs an inconclusive login probe. Not touched: run.sh claude_login_state, which has its own fixture (tests/test-claude-login-state.sh).
449
- - Mechanism: one counting function, `summarizeStatuses(list)` in loki-ts/src/commands/doctor.ts. The text tally is now a list of recorded statuses (`makeTally`, `bump`) with pass/fail/warn derived from it, and `--json` builds its summary from the same function. The login row goes through the pure `evaluateClaudeLogin`: one retry on an empty probe, then an explicit UNKNOWN that is always a counted warning and never a fail, independent of the clock.
450
- - Fixture: loki-ts/tests/commands/doctor_count.test.ts (claude shim: yes / empty / flaky-first-call, with an expired credentials file; asserts failed equals the number of FAIL lines, is equal across probe modes, and that `--json` summary.failed equals the text count).
451
-
452
- ## FC-36 The harness parses ecosystem manifests itself (L0)
453
- - User saw: the supply guard's own pyproject/requirements/package-lock parsers misfired into false FAILED verdicts: a ruff rule or keyword read as a dependency "e501", a new npm workspace key "packages/web" probed on npmjs, PEP 508 git/file references and poetry path deps probed on PyPI, private-registry 404s read as nonexistent. Each fix exposed the next format quirk.
454
- - Law: L0 (hardcoded ecosystem knowledge in the harness will keep misfiring; the ecosystem's own tool is the authority), L2 (one verdict has one author).
455
- - Siblings (sweep of loki-ts/src for harness-side manifest reads): runner/repo_profile.ts, runner/quality_gates.ts (package.json scripts.test), engine10/testmap.ts, engine10/modernize/inventory.ts, features/wall_manifest.ts, engine10/stages/verify.ts, commands/doctor.ts read manifests for detection or test commands; none of them issues a FAILED verdict on a parsed dependency name. Not changed here.
456
- - Mechanism: loki-ts/src/supply/supply_guard.ts. The implement session DECLARES new registry deps in <repo>/.loki/supply-declared.json (bounded reader, brief instruction only while the guard is on); the harness only proves each declared name with the ecosystem resolver (npm view, pip index versions, cargo search, go list) under the user's own config. A manifest or lockfile change with no declaration is a warning, never FAILED. Only a declared dep that does not resolve is FAILED; any tool or network failure is NOT PROVEN.
457
- - Known gap: resolver commands are a small table in supply_guard.ts, not read from the Project Model.
458
- - Fixture: loki-ts/tests/engine10/supply_guard.test.ts (R1 new npm workspace, R2 PEP 508 git/file refs and poetry path, none FAILED).
459
-
460
- ## FC-40 A child is spawned with the full parent env
461
- - User saw: nothing yet. Raw: every `--attempts` engine10 child inherited the parent's GH_TOKEN, GITHUB_TOKEN and SSH_AUTH_SOCK although attempts force `--no-pr` and never push, and the winner's PR push read `origin` from the shared .git/config AFTER the agents ran, so an attempt could `git remote set-url origin <attacker>` and receive the credentialed push.
462
- - Law: moat P9 (the token reaches only the harness's own push, to a destination fixed before any agent ran), L2 (one credential path).
463
- - Siblings swept: loki-ts/src/commands/start.ts attempt child (fixed), runner/attempts.ts git helper and gh pr create (fixed: safeGit, explicit env), engine10/supervisor.ts worker env (already withholdGithubTokens on a copy), engine10/stages/pr.ts push script (pinned origin, already correct), util/pr_lessons.ts and commands/proof.ts gh spawns (credentialed by design, explicit env), engine10/preflight.ts (env passed), runner/checkpoint.ts, council.ts, quality_gates.ts and engine10/session.ts and verify.ts git spawns (read-only local git; session.ts and verify.ts still pass process.env and are NOT converted here). OPEN gap: under allowToken the repo-local credential.helper, core.sshCommand, url.*.insteadOf and pushInsteadOf are still honored (CREDENTIAL_CONFIG keeps the user's helper and ssh command), so a hostile attempt can write them into the shared .git/config; a helper receives the token on store and sshCommand runs code with the token in its env. 11.3.1 therefore refuses `start --attempts N` (N>1) without --no-pr (exit 2, before any pin, worktree or spawn). The 11.3.2 fix (T5-PUSHPATH) is to push from a fresh temp bare repo with clean config, or to pass explicit -c overrides plus GIT_CONFIG_GLOBAL only.
464
- - Mechanism: `tokenFreeEnv` / `safeGit` (loki-ts/src/util/safe_git.ts); attempts pin `remote.origin.url` in memory in productionDeps before the first attempt and push to that literal URL with allowToken, deriving `gh --repo` from it (no GitHub parse, no PR).
465
- - Fixture: loki-ts/tests/runner/attempts-env-token.test.ts (attempt child env token-free), attempts-origin-pin.test.ts (rewritten origin cannot redirect the push), attempts-push-pr.test.ts (winner pushed only via engine10-push.sh push-pr; hostile worktree config sees no credential), util/credentialed_push_guard.test.ts (no credentialed push in loki-ts/src outside push-pr).
466
-
467
- ## FC-41 safeGit blanks diff.external, so every patch-producing call dies
468
- - User saw: nothing yet. Raw: `diff.external=` (blank, set to stop a repo-planted external diff) made git try to run an empty program, so any `diff`, `show -p` or `log -p` through safeGit failed with "external diff died" unless the caller remembered `--no-ext-diff`.
469
- - Law: moat P9 (no repo-chosen command runs in the token-holding process), L2 (one mechanism, not per-caller flags).
470
- - Siblings swept: reviewer_brief.ts, engine10/supervisor.ts, engine10/stages/xreview.ts, runner/attempts.ts (all carried a private --no-ext-diff workaround, removed); stash show and other non-patch commands do not use an external diff and pass through unchanged; GIT_EXTERNAL_DIFF in the caller env is now stripped.
471
- - Mechanism: `safeGitArgs` / `safeGitEnv` in loki-ts/src/util/safe_git.ts inject `--no-ext-diff --no-textconv` after diff, show, log and whatchanged, drop a caller `--ext-diff` / `--textconv`, and delete GIT_EXTERNAL_DIFF. A user-configured external diff or textconv is never re-enabled.
472
- - Fixture: loki-ts/tests/util/safe_git_extdiff.test.ts (configured marker script never runs; output is a real patch).
473
-
474
- ## FC-42 A tracked file the task's own setup wrote is refused as an operator change (FC-15 family)
475
- - User saw: D50 baseline (docs/v10/METRICS.md:565-566), aiq-52-searchbar, Loki+haiku and Loki+sonnet: the engine refused to start in 0.7-1.3s with "dirty tracked tree: M frontend/package-lock.json" (the task `setup` ran npm install). No model call, so the no_change_needed outcome was unreachable. Raw: the raw arm has no tree check and finished the task.
476
- - Law: L1 (never below raw), L5 (a harness refusal is not the user's problem), L2 (the refusal must be keyed on the thing that makes it unsafe, not on a hand-kept list).
477
- - Cause (measured): intake.ts:69-71 refuses any dirty tracked file. E-164 (7b0ba4122, landed 10 minutes after the baseline commit) already allows lockfile-only dirt, but preexisting_dirty.ts LOCKFILE is a hand-kept list of 7 names (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock/b, poetry.lock, Cargo.lock, go.sum). Every other generated lockfile (Gemfile.lock, composer.lock, Pipfile.lock, uv.lock, pubspec.lock, mix.lock, flake.lock, npm-shrinkwrap.json, Package.resolved, deno.lock, gradle.lockfile, packages.lock.json) still refuses the run the same way.
478
- - What separates setup output from an operator's work (the only line the engine can draw from the tree alone): a lockfile is machine-written, never hand-edited, and never part of the run's change. The protection that makes allowing it safe is already in place and is path-generic: splitDirty records the blob at intake, Seal/Commit drop recorded paths from staging (seal.ts dropSet), and untouchedSinceIntake attributes a path to the run only if its bytes changed. Anything that is not a lockfile (source, config, docs, a manifest such as package.json) is operator-shaped work and still refuses with the file named, because a run that edits the same file would mix its change with the operator's unrecorded intent.
479
- - Siblings swept: intake.ts dirtyTrackedFiles is the only refusal in engine10/e10ext/commands (treeswap.ts:314 is a different, primary-checkout guard). stop_restore.ts and commit_filter.ts already honour preexisting_dirty.
480
- - Mechanism: one shared predicate `isLockfile` in loki-ts/src/e10ext/preexisting_dirty.ts, matching the lockfile family by name pattern (`*.lock`, `*-lock.json|yaml|yml`, `*.lockb`, `*.lockfile`, `lockfile`, go.sum, npm-shrinkwrap.json, Package.resolved), used by splitDirty. No new engine path. The `*-lock.*` branch is a deliberate trade: a hand-written src/door-lock.yaml would match, and the byte-based intake protection (the blob recorded at intake, dropped from staging) bounds the risk.
481
- - Fixture: loki-ts/tests/engine10/intake.test.ts describe "FC-42" (nested frontend/package-lock.json as in aiq-52, plus Gemfile.lock and uv.lock; each red before the fix where the name was unlisted; a dirty source file or package.json alongside still refuses).
482
-
483
- ## FC-43 A model that ends LOKI_DONE with an empty diff is failed without one correction (FC-19 family)
484
- - User saw: D50 baseline (METRICS.md:566, PROGRESS.md:1871), pub-humanize-174, Loki+sonnet: "implement ran 95s and produced an empty diff with no already_done marker; FAILED, no PR". Raw sonnet completed the same task (2/3 on rerun). The user got a FAILED receipt for a session that simply stopped.
485
- - Law: L1 (never below raw), L5 (a harness verdict must not be the first and only reaction to a recoverable stop). The false BLOCKED "spec conflict against existing tests" in the same row is FC-19 plus D50-F2-S3 (40ca5f378): the brief's append-only test rule had no exception for a spec-stated assertion value; both are already on main (context.ts:23-24, conflict_resume.ts), so the conflict half is not reproduced on main here.
486
- - Cause (measured): implement.ts classifies a marker-less or LOKI_DONE exit 0 as "done" without looking at the tree; verify.ts:252 then fails "empty diff without an already_done marker" and seal.ts:152 seals FAILED. FC-19 resumes a spec-conflict exit once; the done-with-nothing exit had no equivalent. Every stage rule is correct (empty diff is not VERIFIED); the gap is that nothing asked the model to either change the code or claim LOKI_ALREADY_DONE with evidence before the verdict.
487
- - Siblings swept: LOKI_SPEC_CONFLICT (resumed, FC-19); LOKI_ALREADY_DONE (honoured, then confirmed by already_done.ts evidence); fix.ts rounds reuse buildImplementBrief but do not go through this stage's classification, so they are unchanged.
488
- - Mechanism: one shared resume, util/conflict_resume.ts `resumeAfterEmptyDone`, called from implement.ts when the final exit is "done" and `git diff <base>` plus untracked (the exact set verify.ts changedFiles uses) is empty. One correction: "your tree has no changes; make the change, or finish with LOKI_ALREADY_DONE: <file:line evidence>, or LOKI_SPEC_CONFLICT: <reason>". No wording of the model's output is inspected (L0). A resume that errors, is killed, or ends without a marker keeps the first result, so the verdict is never worse than before. Cost: one extra turn, only on empty-diff done exits.
489
- - Fixture: loki-ts/tests/engine10/empty_done_resume.test.ts (empty tree + done resumes once with the correction; a resumed session that edits the tree completes as done; no resume when the tree has changes; no second resume; a failed resume keeps the first result).
490
-
491
- ## FC-45 A TL review ran a narrower suite than the files the slice touched (PRICE-TRUTH left 7 red tests on main)
492
- - User saw: nothing yet. Raw: PRICE-TRUTH (84d601721) changed loki-ts/src/runner/budget.ts and loki-ts/data/model-pricing.json, but its review ran only tests/util; tests/runner/budget.test.ts and budget_haiku55.test.ts went red on main 7e6692a8b (58 pass, 7 fail) and blocked the 11.3.1 cut.
493
- - Law: evidence or it did not happen (OPERATING-MODEL discipline): a review verdict covers only the suites it ran.
494
- - Causes (each decided against the pricing page, read 2026-10-08): CODE bug, no claude-haiku-4-5 row, so the exact id fell through to the haiku alias (now Haiku 5.5, $0.10) while the page prices Haiku 4.5 at $1/$5 (fixed: exact-id row in model-pricing.json and the budget.ts fallback). TEST stale: opus 5/25 (page: Opus 5.5 $4/$20), haiku alias 1/5 (alias = claude-haiku-5-5 per model_catalog.json cli_aliases), rounding case, immutable-table row without cache tiers, over_100k without cache_write_5m/1h, source date 2026-10-07 vs the 2026-10-08 read.
495
- - Siblings swept and fixed in the same slice: the bash route (run.sh check_budget_limit) keys by exact string with a sonnet fallback, so claude-haiku-4-5 AND claude-haiku-5-5 priced at the sonnet row there (12.0 per 1M in+out, measured red); run.sh _write_pricing_json lacked 4-5; dashboard _DEFAULT_PRICING lacked both. All id-keyed tables now carry both ids. autonomy/loki is title-case family keyed.
496
- - Mechanism (CTO ruling): a TL review runs `scripts/select-tests.sh` on the slice diff plus the always-run guards, and pastes the command and its rc in the verdict. A review without the selector output is incomplete.
497
- - Fixture: loki-ts/tests/runner/budget_haiku55.test.ts (claude-haiku-4-5 stays $1/$5, red before the row existed) and the bash-route section of tests/test-pricing-parity.sh (claude-haiku-4-5 6.0 and claude-haiku-5-5 0.6 per 1M in+out through the real check_budget_limit).
498
-
499
- ## FC-46 Suites deferred to nightly let train drift reach main (D90 family)
500
- - User saw: the 11.3.1 cut blocked on run 37737159605 (main 7e6692a8b) with six red full-tier suites, none of which the fast gate had run on the trains that introduced the drift. Raw: each train was green at merge.
501
- - Law: evidence or it did not happen (OPERATING-MODEL.md Discipline); a verdict covers only the suites it ran.
502
- - Cause (measured): D90 (699d4ed25) moved suites over 80s to nightly, so DEP-01 (docker-build-check.yml, full-suite.yml rows), DOC-02 (future-slice cards in docs/v11/MARKET-SLICES.md), agent-types-loaded (README rewrite dropped the reviewer-selection sentence), analytics-toggle (a new bin/loki case guard shifted a brittle grep anchor), train-verdict-reuse (first-run-gate.yml deleted) and shellcheck SC2148 on tests/real-run/scenarios drifted with no gate in front of them.
503
- - Siblings swept: every one of the six suites was run individually red then green; the fast tier covers the same files only through select-tests.sh diff selection.
504
- - Mechanism: a change that touches a file a deferred suite reads (workflows, README, docs/v11, bin/loki, tests/real-run) must name that suite in its slice card Wall checks and run it before merge; the nightly is the backstop, not the first detector.
505
- - Fixture: the six suites themselves (tests/test-dep-inventory.sh, tests/test-docs-cli-drift.sh, tests/test-agent-types-loaded.sh, tests/test-telemetry-analytics-toggle.sh, tests/test-train-verdict-reuse.sh, shellcheck -S error tests/real-run/scenarios/*.sh).
506
-
507
- ## FC-44 The receipt understates facts the console measured
508
- - User saw: a billed trivial-sum run printed 40s and 107k tokens on the console while its receipt said time.wall_s 8.3 and cost.input_tokens 24.
509
- - Raw: seal summed `duration_s` from stage outputs, but machine.ts put it only on the emitted event, so only implement carried one; the receipt cost block omitted cache tokens (the bulk of input) and supervisor.ts's summary tokens omitted them too, disagreeing with the E-59 total. Cache keys no result file carried were also written and summed as 0, which reads as a measured zero, and a cache total from only some sessions was published as the run total.
510
- - Law: L2 (one fact has one author: the receipt and the console must read the same measurement), evidence-or-it-did-not-happen (a receipt figure that contradicts the console is a false claim).
511
- - Author of total_s: the first line of events.jsonl, the earliest timestamp the supervisor writes, so it precedes worker boot and preflight; the end is the seal clock. The result-line duration_ms is carried separately as cost.sdk_duration_ms (model time only) and is never used as total_s.
512
- - Reconciliation (1%): `time.stages` is a disjoint partition of total_s: setup (first event to machine start), each stage, a parallel group such as plan||wall as ONE bucket ("plan+wall", union of its intervals), orchestration (gaps) and seal. Per-member seconds are in `time.stage_s`. Any consumer may add the buckets. `reconciledTotalS` (loki-ts/src/engine10/receipt_time.ts) returns total_s only when the buckets are non-negative numbers summing to it within 1%; a gap or a clock fault reads NOT RECORDED downstream (receipt.md Total line, recordRun history, receiptBlock actual), never a number. total_s ends at seal, so the PR stage is excluded and the receipt.md line says "to seal".
513
- - Siblings swept: engine10/pr_body.ts stageTimes (same reduction, now fed), runner/router/cost_preview.ts receiptBlock/recordRun/estimateFromHistory (reconciled total only; null wall reads NOT RECORDED), history_store.ts (version marker wallKind "total-v1"; unmarked old rows hold the stage sum and are ignored for time estimates, still used for dollars), autonomy/lib/proof-template.html (reads the cache keys, now populated), stages/seal.ts renderReceiptMd, supervisor.ts summary.cost.tokens (summaryTokens), control-plane packages/control-plane/src/server/runs.ts:37 (inputTokens now includes cache read and creation), routes/cost.ts:24 (sums that column), ui/src/pages/cost/index.tsx (tile and column relabelled "Tokens in (incl. cache)"). Not changed: control-plane ingest and mcp/cp_tools.py read the run.completed event wall_s (already true elapsed), not receipt.time.
514
- - Mechanism: machine.ts records duration_s on every completed stage output and a per-run timeline; receipt_time.ts builds and checks the time block; sdk_stream_parser writes a cache key only when usage carries it, and cost.ts sumResultCosts marks a cache total seen only when EVERY session carried the key and costTotalsOf maps them, so an unseen key is omitted from the receipt. All additive: absent means NOT RECORDED, old receipts still verify (receipt hash covers the whole body).
515
- - COST-RECORDS: the SDK result `usage` is main-loop only while `total_cost_usd` covers the whole pipeline. writeResultCost now also records `modelUsage` (per-model tokens and costUSD), `num_turns`, the main-loop 5m/1h cache split, `session_id` and `resumed_from`. When the per-model costs sum to total_cost_usd within 1%, cost.tokens_scope is "all-models", tokens come from modelUsage and cost.per_model is published; otherwise tokens_scope is "main-loop" and no per_model. cost.cache_creation_main_loop is always labelled main-loop scope (modelUsage has no split).
516
- - FIX-RESUME: the SDK says a resumed session's total is cumulative only when its transcript saved one. A resumed result whose total is below its predecessor's is provably separate and is summed (cost.resume "separate"); an equal-or-higher total fits both cases, so the iteration joins `missing`, usd reads NOT RECORDED and cost.resume is "ambiguous". Never subtracted, never guessed. An ambiguous file is excluded from every summed figure (measuredCount, partial dollars, tokens, cache, per_model, turns, sdk_duration_ms), and input_tokens/output_tokens (required numbers) then sum only the sessions that count, with cost.tokens_measured {k, n} written only when k < n (receipt.md prints "Tokens: partial: X input / Y output for k of n sessions"; real-run.sh prints NA for them; the per-session cost event omits its token keys). The control-plane fold counts the cost events that carried usage against all cost events (below). And per_model/turns/cache split/all-models tokens are published only when every session in the run produced a non-ambiguous file carrying them. Reachability: ambiguous is reachable in DEFAULT runs, no flag: resumeAfterConflict (FC-19, conflict_resume.ts) and resumeAfterEmptyDone (FC-43, the "-e" iteration) pass resumeSessionId on claude, and LOKI_E10_FIX_RESUME=1 adds the CH-03 plan->implement->fix chain on top; each is covered by a test. The real resumed-session capture (LOKI_E10_FIX_RESUME=1) was attempted once on the trivial-sum fixture (claude-sonnet-5-5, CH-03 chain on): the task was solved on the first implement attempt, so no fix round ran and no session was resumed (4 sessions, none with resumed_from). Cumulative vs separate is therefore UNMEASURED and "ambiguous" stays the shipped rule. That run did validate the rest of the receipt: total_s 39.687 equals the sum of time.stages, and per_model costs reconcile to total_cost_usd. Fixtures for the resume branch are shaped from sdk.d.ts.
517
- - Readers of unrecorded usage (sibling sweep, R4): the ambiguous session's efficiency record (cost.ts writeEfficiencyRecord) now omits cost_usd and every token key and carries tokens_measured:false (a resume provably below its predecessor, found among the sibling result-cost files, keeps its numbers). Every reader treats the flag as NOT RECORDED, never a partial sum: loki-ts/src/metrics/kpis.ts (recordsMeasured gate, unmeasured_iterations only when >0), loki-ts/src/commands/stats.ts (JSON null, text "NOT RECORDED", per-iteration rows), autonomy/lib/efficiency_cost.py collect_efficiency (token fields None, tokens_partial and tokens_unmeasured_records only when >0), dashboard/server.py /api/cost (totals and by_phase/by_model None, unmeasured_sessions only when >0; the context-tracker fallback is skipped), while _compute_budget_snapshot and _compute_cost_timeline already count a record with no measured field as unmeasured. Control plane: runs.ts stores token_sessions (nullable, migration 0006, null on rows ingested earlier) = cost events that carried usage; cost routes add token_sessions and token_sessions_total only when some session lacked usage; the cost tile, the cost table and the run page print "partial: k of n sessions". loki-ts/src/runner/budget.ts calculateCostFromRecords reads cost_usd or prices tokens, so an unrecorded session adds $0 to the budget breaker exactly as any unpriced session did before; not changed. Tests: tests/engine10/cost.test.ts R4-1, kpis.test.ts, stats.test.ts, tests/test_efficiency_cost_availability.py, tests/dashboard/test_cost_endpoint_honesty.py, packages/control-plane/test/server/runs-cache-tokens.test.ts. The cost event (loki-ts/src/engine10/session.ts recordCost) shares the tokensUnmeasured predicate (cost.ts) with writeEfficiencyRecord (CostResult.tokens_measured set, or an ambiguous resume): such a session emits no token keys, so the control plane never reads a missing session as 0 (R5-1; test in tests/engine10/cost.test.ts, plus the FC-19 "-r" case). budget.ts recordPartialStreamCost (a killed session with no partial-usage file) sets tokens_measured {k:0,n:1}, so its efficiency record and cost event carry no zero token keys (R5-2; tests in tests/engine10/cost.test.ts). Migration 0006 leaves pre-existing runs at NULL, pinned by ask_schema.test.ts (R5-4).
518
- - Fixture: loki-ts/tests/engine10/seal.test.ts (real-shaped boot/stages/seal stream reconciles at total_s 35; gap and 1% boundary read null; parallel overlap), tests/engine10/cost.test.ts (cache keys through the real reader and worker mapping), tests/runner/cost_preview.test.ts (old unmarked entry ignored for time).
519
-
520
- ## FC-49 Weekly integrity audit ran the gitleaks-dependent suites without installing gitleaks (issue 223)
521
- - User saw: issue 223 "Weekly Integrity Audit Failed - 2026-10-05". Raw: run 37311758053 (head dee1553234d4), pytest 6 failed / 3637 passed, and the shell suite "security-audit.yml gitleaks config isolation" FAILED (21 passed, 1 failed).
522
- - Law: evidence or it did not happen; a guard that silently does not run is an absent measurement (_require_gitleaks fails under CI by design).
523
- - Cause (measured): integrity-audit.yml had no install-gitleaks.sh step, unlike test.yml, full-suite.yml and release.yml. Failing tests: tests/test_release_required_ci.py::PollLoopPriorityIsExercisedForReal::{test_e157_clean_range_still_reuses, test_e157_eligible_bump_reuses_parent_security_audit_success, test_e157_hex_debugid_is_still_eligible, test_e157_merge_release_hiding_a_leak_in_second_parent_is_not_reused, test_e157_secret_in_allowlisted_changelog_block_is_not_reused, test_e160_parent_main_stubs_plus_one_train_success_still_reuses), all "gitleaks is not installed in CI"; shell: "no gitleaks binary under CI".
524
- - Siblings swept: every workflow with a bare `python3 -m pytest` step (only integrity-audit.yml lacked the install).
525
- - Mechanism: install step added before the test steps; a pytest guard requires the install in every workflow that runs bare pytest.
526
- - Fixture: tests/test_release_required_ci.py::EveryWorkflowThatRunsThePythonSuiteInstallsGitleaks (red before the step existed).
527
-
528
- - Sibling instance (ALIAS-FWD): tests/cli/test-alias-forwarding.sh carried 3 stale reds on main: help entry bound 23 vs 27 shipped commands (test stale, no decision caps the page), a local-ci Dashboard-normalizer assertion for the bun-parity matrix removed in 5adff01a9 (test stale, assertion retired), and the main-parity export case whose extracted main script could not locate the skill dir (harness stale, fixed with SKILL_DIR). Product had not drifted in any of the three; the suite is not in the fast tier.
529
-
530
- ## FC-47 select-tests --run returned success having run nothing (SEL-RUN-FC47)
531
- - User saw: nothing yet. Raw: `scripts/select-tests.sh --run` on an R0 diff (package.json, tests/run-all-tests.sh, unparseable, unknown shape) exited 0 with no suites executed, because every early `emit ...; exit 0` returned before the DO_RUN check.
532
- - Law: Engine Laws (a gate that can pass by doing nothing is theatre); FC-16 class (green with zero work).
533
- - Siblings swept: R0 broad-blast, R0 unparseable, R0 unknown shape, docs-only, empty diff and --guards-only all exited before the run step (all fixed).
534
- - Mechanism: one finish() function that every exit path calls; with --run it executes the selection, prints "suites executed: N", and exits nonzero with "NOTHING RUN" when N is 0, except the documented empty diff.
535
- - Fixture: tests/test-select-tests-run.sh (fake repo with a stub full suite; mutation: restore the broad-blast `exit 0` and the test goes red).
536
-
537
- ## FC-48 The diff-selected gate skipped the repo-wide guards
538
- - User saw: nothing yet. Raw: a slice touching only loki-ts/src/runner/attempts.ts was gated by the impacted-test planner, which selects by diff, so fc25_raw_spawn_guard and the other repo-wide guards never ran; a raw git spawn passed 3 reviews.
539
- - Law: Engine Laws (a guard that only runs when its own file changes is a checklist, not a guard); D26 guard 5.
540
- - Siblings swept: scripts/select-tests.sh (fixed, rule R8), scripts/ci/fast-gate.sh plan (fixed, appends --guards-only even when every path is R0-class), scripts/impacted-gate.sh (fixed, runs R8 bun guards), scripts/local-ci.sh --impacted (goes through impacted-gate.sh).
541
- - Mechanism: scripts/global-guards.tsv is the one declared list; select-tests.sh emits it as R8 on every non-empty diff (docs-only included) and via --guards-only.
542
- - Fixture: tests/test-select-tests.sh (attempts.ts-only diff and docs-only diff both select every declared guard; --guards-only lists them).
543
-
544
- ## FC-50 Cold wasm render inside bun's default 5s test budget reddens the nightly (NIGHTLY-COCKPIT)
545
- - User saw: the 11.3.1 cut blocked by D90 because Nightly was red since 10-04. Raw: run 37686475225, job "Bun tests on macos-latest bun=1.3.13 (nightly)", `(fail) cockpit render orchestration > renders a real inline image on a graphics terminal via the bundled wasm [5479.10ms]` followed by `this test timed out after 5000ms`; the next wasm test in the same file passed in 2142ms once warm.
546
- - Law: evidence or it did not happen (a red gate is read from the log, then reproduced); never weaken the assertion or skip on macOS to turn a timeout green.
547
- - Cause (measured): the first test in the file to rasterize pays the one-time cost of importing @resvg/resvg-wasm, reading the vendored .wasm, initWasm and loading the font (raster.ts caches all three per process), and a cold macOS runner spends over 5000ms on it, which is bun's default per-test timeout. No logic failed.
548
- - Siblings swept: grep for rasterize, resvg and render( across loki-ts/tests finds only tests/cockpit.test.ts; its two real-render tests (inline image, font-bearing PNG) both got the explicit timeout. All other cockpit tests are pure string work.
549
- - Mechanism: the cold one-time init is warmed in a beforeAll with its own 60s budget, and every test that performs a real rasterize passes an explicit RENDER_TIMEOUT_MS (30s); a test that does real wasm work must not rely on the 5s default.
550
- - Fixture: loki-ts/tests/cockpit.test.ts. Red evidence: with RENDER_TIMEOUT_MS=1 and the warm disabled both render tests fail "timed out after 1ms"; with the fix 26 pass, 0 fail.
551
-
552
- ## FC-TEST-BULK-FIXTURE-DEFAULT-TIMEOUT A test that writes thousands of files and runs git reddens on a loaded host at bun's 5s default (MAIN-RED P12-12d)
553
- - User saw: P12-12d "untracked growth is seen even with a very large tracked change list" red on origin/main 4b0bbb0ca and bc2e0a8ff. Raw: `(fail) ... [21552.81ms]` then `this test timed out after 5000ms` at 1-min load 27; no assertion failed.
554
- - Law: evidence or it did not happen; never weaken the assertion. The test and session.ts are unchanged since 2026-10-16/17 (git log -S2100), so this is a cost-versus-default-budget fault in the test, not a product regression.
555
- - Cause (measured): the test writes 2100 files twice plus git add/commit, about 20s under load; bun's default per-test timeout is 5000ms.
556
- - Siblings swept: same family as FC-50; the only other bulk-file git fixture in session.test.ts is the sibling test in the same describe, which is small.
557
- - Mechanism: the fixture size is a named SPILL=1100 (just over the 1000-file per-list budget it must exceed) and the test carries an explicit 60_000 timeout.
558
- - Fixture: loki-ts/tests/engine10/session.test.ts. Red: unmodified, `bun test tests/engine10/session.test.ts -t P12-12d` fails "timed out after 5000ms"; green: 2 pass, 0 fail. Mutant (one shared file budget across both lists in diffSignature) fails the test by assertion, 1 pass 1 fail.
559
-
560
- ## FC-51 A publish-path change silently dropped a field a later gate depends on (PROMOTE-GITHEAD)
561
- - User saw: 11.3.1 stuck on the npm `next` tag; Promote run 37784566223 failed "FATAL: loki-mode@11.3.1 has no gitHead on npm", so the release never reached latest, Docker latest or Homebrew. Every later release would stick the same way.
562
- - Law: evidence or it did not happen; a change to one stage must keep every field a downstream stage reads.
563
- - Cause (measured): D90 (6247695f5) changed release.yml to `npm pack` then `npm publish <tgz>`. A tarball publish records no gitHead (only a directory publish reads .git); 11.3.0 and 11.2.0 have it, 11.3.1 does not. promote.yml requires gitHead to prove the build is an ancestor of origin/main.
564
- - Siblings swept: grep of .github/workflows and scripts for gitHead finds only promote.yml (reader). The other publish in release.yml (loki-mode-sdk, `npm publish` from sdk/typescript, a directory publish) still records gitHead and promote does not read it, so it is unaffected. sbom.yml packs but does not publish.
565
- - Mechanism: (1) release.yml stamps `git rev-parse HEAD` into the manifest before `npm pack`, restores package.json, and asserts the packed manifest gitHead equals HEAD. (2) promote.yml, when npm has no gitHead, falls back to the commit of tag v${VERSION} only if the tag is annotated (release.yml creates it with `git tag -a`), that commit has a successful Tests run on main, and the published tarball's loki-ts/dist/loki.js embeds the version; the source used is logged as GITHEAD_SOURCE. The 40-hex, ancestor-of-origin/main and never-move-latest-backwards checks apply to both paths unchanged.
566
- - Fixture: tests/test-promote-head-stamp.sh (static check of both workflow definitions plus 8 negative mutations, one per required piece). Red on a0d599d0c (11 requirement failures), green after.
567
-
568
- ## FC-53 A host-dependent or pipe-dependent assertion reddens the nightly on one OS (NIGHTLY-FLAKE)
569
- - User saw: Nightly 37790534786 (a0d599d0c) red, blocking releases under D90. Raw: Bun macOS (both bun versions) `(fail) gate-failures cap head/tail parity (W4 L1) > doctor per-provider install hint goes to STDERR` "Expected: <= 1, Received: 2" at gate-failures-cap-parity.test.ts:81; and Full suite Shell shard 1/8 `TEST 1: basic onboard on loki-mode repo ... FAIL` after `echo: write error: Broken pipe`.
570
- - Law: evidence or it did not happen; an assertion must test the property it names, not a host side effect; never retry, lengthen a sleep or skip to turn a gate green.
571
- - Cause (measured): (1) The doctor test counted any stdout line containing "Install:". On macOS the system python3 is 3.9, so doctor prints a Python 3.12 warning ending "Install: brew install python@3.12", a second match. It failed on the same SHA in the dispatched run 37780519439 too (job 113322165616, same line), so it was never flaky: deterministic on macOS, hidden because that run's overall verdict was success. (2) `loki onboard` ran `find ... | wc -l` under `set -euo pipefail` (autonomy/loki cmd_onboard); when find exits non-zero on an unreadable or concurrently vanishing entry (other suites create and delete temp files in the repo root during shard runs) the whole command died silently, rc 1 and 0 bytes. Reproduced locally under 14-way parallel load, trace ends at the find|wc line; the same suite passed in run 37780519439 (job 113322165426). In the log the test also saw `echo: write error: Broken pipe` because test-onboard-command.sh used `echo "$output" | grep -q` under pipefail on a 37KB report.
572
- - Siblings swept: all 11 `echo "$output" | grep -q` sites in test-onboard-command.sh converted. Other suites use the same idiom (grep over tests/*.sh finds it in ~100 files) but only misfire when the producer output exceeds the pipe buffer; none of those showed a nightly failure, so they are tracked by the flake tracker rather than bulk-edited. Bun tests: the only bare "Install:" counter in loki-ts/tests was the doctor test.
573
- - Mechanism: (1) the doctor test counts only lines carrying a provider install command and requires the sole allowed one to be the claude fallback. (2) Product fix: the two find/grep count pipelines in cmd_onboard tolerate a non-zero producer (`|| true`, the count still comes from wc). Test fix: grep reads the variable through a here-string, which has no upstream writer in the pipeline status. Shared guard: scripts/nightly-flake-tracker.sh compares saved job logs per SHA and emits FLIP (pass in one run, fail in another on one SHA, with a BOARD-ready line) or STABLE-FAIL (red in every run, a real defect).
574
- - Fixture: test-onboard-command.sh TEST 11 (unreadable directory makes find exit 1; red on the unguarded line with rc 1, green after) and tests/test-nightly-flake-tracker.sh (fixtures in tests/fixtures/nightly-flake). Replay of the real logs: `nightly-flake-tracker.sh SHA:a:<job 113356420172 log> SHA:b:<job 113322165616 log>` reports the doctor test as STABLE-FAIL.
575
- ## FC-54 A path guard selected every file under tests/ and ran helpers as suites (MAIN-RED 6ecedaefe)
576
- - Seen: Tests run 37792537759 on main went red on 4 fast-gate shards for a diff that only touched release.yml, promote.yml and tests/run-all-tests.sh. Raw: rows such as `py_test tests/lib/scan-workflow-rc.py` (rc=5, pytest collected nothing), `shell_test tests/shard-durations.tsv`, and `shell_test tests/run-all-tests.sh` (rc=124, the whole suite run bare).
577
- - Law broken: a gate runs only what it can run; a selection row is a runnable test or it is not a row.
578
- - Cause (measured): scripts/ci/fast-gate.sh guards_for wrote `grep -rlF -- PATTERN tests --include=...`. After `--` the --include flags are file operands, so the filter was silently dropped and any file under tests/ mentioning the workflow name was planned (py_test or shell_test by extension). Separately scripts/select-tests.sh match_kind accepted run-*.sh, which admitted the aggregate tests/run-all-tests.sh. Latent since D90; exposed when workflow files first changed together with run-all-tests.sh.
579
- - Siblings swept: no other `grep ... -- ... --include` in scripts/, autonomy/ or tests/*.sh. tests/cli/test-alias-forwarding.sh is a legitimate selection with its own red, fixed by ALIAS-FWD 164eb7da2 on the train.
580
- - Mechanism: options before `--` in the three guards_for greps; match_kind returns no kind for run-all-tests.sh.
581
- - Fixture: tests/test-fast-gate.sh FC-54 case (release.yml + run-all-tests.sh + a registering test plans only test-*.sh, test_*.py, run[-_]*.sh). Red before, green after.
582
-
583
-
584
- ## FC-60 Parallel slices each pass a repo-wide cap or allowlist alone, and the merged train is over it (TRAIN-GREEN)
585
- - User saw: the 11.3.2 train (832fa4b57) was red before push on four repo-wide guards that each merged slice passed on its own base: loki-ts/tests/engine10/l0_guard.test.ts ("src/commands/verify_pr.ts npm test" and "package.json" unlisted), budget.test.ts (core engine10 5462 lines against the 5,000 cap; features 3158 against 3,000), and tests/cli/test-alias-forwarding.sh ("help: front-page entry count in [12,27] -- got 28").
586
- - Law: evidence or it did not happen; D33 (core cap never raised), D66 (features cap), L0 (guard allowlist may only shrink without a stated reason).
587
- - Cause (measured): at 6ecedaefe core is 4734 and features 2977 lines. Each slice (COST, RECEIPT-TRUTH, SARIF, AI-BOM, UNDO-2, VPR-2 and others) was reviewed against a base with room, and no merge step re-ran the repo-wide guards on the union. UNDO-2 added the 28th front-page command after ALIAS-FWD pinned the bound at 27. The VPR-2 package-manifest read in verify_pr.ts predates the guard being selected for it (FC-48).
588
- - Siblings swept: budget.test.ts, l0_guard.test.ts, the alias-forwarding bound and tests/test-structural-checks.sh are the only cumulative caps in the fast tier. Core and features caps are NOT fixed here: D33 forbids raising the core cap, so the overage is handled by moving code out (EXT-CORE-2-r2 for core, EXT-FEAT-r2 for features), never by raising a cap.
589
- - Mechanism: (1) l0_guard.allowlist gains two exact file-and-token rows for verify_pr.ts with a stated sunset (the PR own manifest test script is read as data); any other file or token is still red. (2) The alias bound moves 27 to 28 with the added command named in the comment. (3) Train rule: the merge of the last slice must run the guards named in this row on the union before push.
590
- - Fixture: l0_guard.test.ts red then green; test-alias-forwarding.sh help bound red (got 28) then green.
591
-
592
- ## FC-61 A test asserted on the first FAIL line, so one earlier real failure masked every later planted-defect check (TRAIN-GREEN)
593
- - User saw: tests/test-structural-checks.sh "6 passed, 5 failed" with T6, T7 and T9 reporting "committed en dash not caught", "added line starting with + not scanned", "task NOTES.md dash not caught", although the emoji/dash check was working.
594
- - Law: an assertion must test the property it names, not whichever line prints first.
595
- - Cause (measured): line_of "$out" "FAIL" | head -1 returns the first failing check. The real line-budget failure (core 5421 >= 5000) prints before "emoji/dash on changes", so the grep for emoji/dash never saw its own FAIL line. T9 asserted rc 0 for the exempt case, which also failed for the unrelated budget reason. T2, T3 and T5 passed only because their checks print before line budgets.
596
- - Siblings swept: all seven line_of call sites in the file now name their check label through failed_check; T1 stays the one assertion that the whole tree is clean.
597
- - Mechanism: failed_check matches `^FAIL ... <label>` anywhere in the output.
598
- - Fixture: with the budget red (as on the train), T6, T7, T9 are red before and green after; T1 remains red until the budget ruling.
599
-
600
- ## FC-62 A test built an expected path from a raw $TMPDIR and macOS leaves a trailing slash (NIGHTLY-REDS)
601
- - User saw: tests/test-caveman-flags.sh `bootstrap cwd custody` FAIL on macOS (exit=0, cwd=/var/folders/.../T/loki-caveman-XXXX/bootstrap-tmp/loki-caveman-bootstrap.YYYY).
602
- - Law: an assertion must test the property it names, not a host path spelling.
603
- - Cause (measured): macOS $TMPDIR ends in a slash, so `${TMPDIR:-/tmp}/loki-caveman-XXXXXX` became `T//loki-caveman-...`; the product records the canonical single-slash cwd, so the `case "$bootstrap_cwd" in "$BOOT_TMP"/...` pattern never matched although the bootstrap behaved correctly. Reproduced locally: 44 passed, 1 failed before; 45 passed, 0 failed after.
604
- - Siblings swept: other suites use the same mktemp spelling but only this one compares the result against a recorded cwd string.
605
- - Mechanism: the test strips one trailing slash from TMPDIR before mktemp.
606
- - Fixture: tests/test-caveman-flags.sh section 5c is the regression (red on macOS before the strip).
607
- - Note: the other two Nightly 37790534786 reds (doctor hint, onboard) were already fixed by FC-53 (0c0069df3, in this train); re-verified green here (bun test gate-failures-cap-parity 3 pass; test-onboard-command.sh 11/11).
608
-
609
- ## FC-63 A subprocess test pinned PATH so tightly the product under test never ran, and its host-dependent counter had no host-independent fixture (NR-DOCTOR)
610
- - User saw: the macOS bun Nightly leg (run 37790534786 on a0d599d0c) reported `doctor per-provider install hint goes to STDERR` "Expected: <= 1, Received: 2"; locally the same test passed, so the red looked unreproducible.
611
- - Law: evidence or it did not happen; an assertion must exercise the property it names; a green that cannot go red is not a gate.
612
- - Cause (measured): the red assertion was the bare "Install:" counter already fixed by FC-53 (0c0069df3 is not in a0d599d0c; HEAD f22648a11 contains it and the test passes). Remaining defect: the test ran `bin/loki doctor` with PATH=/usr/bin:/bin, which hides bun on hosts where bun lives elsewhere (here ~/.bun/bin), so the shim printed its minimal "Bun doctor is unavailable" report and the test passed vacuously; it only ran the real doctor where bun sat on the system PATH. On this host python3.12 is installed under /opt/homebrew, so the python hint never printed and the CI condition could not be reproduced.
613
- - Siblings swept: other loki-ts tests that spawn bin/loki with a pinned PATH were not found to assert on doctor output; only this test counted install hints.
614
- - Mechanism: the PATH keeps /usr/bin:/bin (providers stay absent) plus dirname(process.execPath) so the real Bun doctor always runs; the counting moved into providerInstallLines.
615
- - Fixture: gate-failures-cap-parity.test.ts "provider install-hint counter ignores the unrelated Python 3.12 hint" replays the CI stdout shape (python hint + claude fallback): the old bare counter yields 2 (red, asserted in the test), providerInstallLines yields 1 (green), and a leaked codex hint yields 2.
616
-
617
- ## FC-64 A test piped a large captured value into grep -q under pipefail and died of SIGPIPE (NR-PIPE)
618
- - User saw: Nightly 37790534786 (a0d599d0c) full-suite shard 1 red. Raw: `tests/test-onboard-command.sh: line 51: echo: write error: Broken pipe`, then "Missing package manager detection" (rc 141 from `echo "$output" | grep -q npm`).
619
- - Law: an assertion must fail only on the property it names; a pipeline whose exit status depends on how fast the reader exits is not an assertion.
620
- - Cause (measured): grep -q exits at the first match while echo is still writing a value larger than the pipe buffer; pipefail reports 141. Reproduced deterministically with a 400KB value: legacy form rc=141, here-string rc=0 (tests/test-sigpipe-guard.sh section 1). The onboard fix itself was already in this train (FC-53, 0c0069df3).
621
- - Siblings swept: 1789 occurrences of the legacy form across pipefail suites (test-v10-pulse.sh 319, test-branch-lifecycle.sh 79, test-compound-cli.sh 65 ...); only commands with large output can trip it, and the onboard family is now zero. The rest are bounded outputs and are held by a ceiling, not bulk-rewritten.
622
- - Mechanism: grep -q ... <<<"$var" (no pipe, no writer to break). tests/test-sigpipe-guard.sh enforces it in test-onboard-*.sh and ratchets the repo-wide count (lower the ceiling as suites convert; never raise it).
623
- - Fixture: tests/test-sigpipe-guard.sh (red against the pre-fix test-onboard-command.sh from a0d599d0c: 11 legacy lines; green on this tree).
624
-
625
- ## FC-57 A continue-on-error job hides a real failure from the workflow conclusion and the D90 gate (NIGHTLY-TRUTH)
626
- - User saw: Nightly run 37780519439 (a0d599d0c, 2026-10-08) concluded SUCCESS while "Bun tests on macos-latest bun=1.3.13 (nightly)" and "bun=latest" both FAILED on `gate-failures cap head/tail parity (W4 L1) > doctor per-provider install hint goes to STDERR`; the same legs failed again in run 37790534786. release.yml required-ci and D90 read the run conclusion, so the release gate saw green on a deterministic failure.
627
- - Law: evidence or it did not happen; a red leg must reach the verdict that gates releases. Informational is a per-leg decision about upstream drift, never a job-wide switch over pinned code.
628
- - Cause (measured): nightly.yml job `bun-tests-nightly` had job-level `continue-on-error: true` (header lines 13-14: "informational so a macOS or bun=latest flake cannot hold a release"). It covered the pinned bun 1.3.13 macOS leg, whose failure is a product/test defect, not drift. first-run-gate (stub, pinned bun, no network) had the same switch. The test itself was fixed separately in FC-53 (0c0069df3: macOS python 3.9 adds an unrelated "Install:" line).
629
- - Siblings swept (continue-on-error, `|| true` on test commands, pipes without pipefail, always() aggregators, matrix legs outside the verdict) across all 13 workflows: integrity-audit.yml "Run shell tests" step had continue-on-error with only an outcome printout, so the weekly audit could never fail on the shell suite (fixed: now blocking). test.yml and security-audit.yml "Reuse a train run's verdict" steps: fail-safe by design (failure means reuse=false, full run). security-audit.yml pip-audit requirements step: findings baseline, followed by a step asserting a parseable report for every manifest. release.yml Docker Hub description: cosmetic after a successful publish. post-release-smoke.yml notify (`if: always()`): notifier only, the smoke jobs it reads fail the run themselves. `!cancelled()` job conditions in test.yml and security-audit.yml are run gates, not aggregators. full-suite.yml and release.yml required-ci have no masking pattern (`|| true` only on API lookups that fail closed into a pending/blocked verdict).
630
- - Mechanism: nightly.yml pinned legs block; only matrix legs with `experimental: true` (bun=latest) use `continue-on-error: ${{ matrix.experimental == true }}`; first-run-gate blocks. tests/test-workflow-no-masked-failures.sh parses every workflow and fails on an unconditional continue-on-error outside a justified allowlist, an experimental pinned leg, `|| true` or an un-pipefailed pipe on a test command, and an unlisted `if: always()` job.
631
- - Fixture: tests/test-workflow-no-masked-failures.sh. Red on the train-1132 workflows (3 findings: nightly bun-tests-nightly, nightly first-run-gate, integrity-audit shell tests), green after; 5 single-rule mutations each go red.
632
-
633
- ## FC-65 (LOW follow-up to FC-57) Contrived shell bypasses of the masked-failure guard (NIGHTLY-TRUTH)
634
- - User saw: nothing; found by adversarial review of tests/test-workflow-no-masked-failures.sh. No realistic workflow edit triggers these; each needs a deliberate hiding of the exit status.
635
- - Law: a guard blocks on what a realistic edit would do; the rest is tracked, not built (CTO ruling, round 7).
636
- - Cause (measured): the guard is a lexical interpreter of the `||` handler, not a shell. Covered and red: Y01 to Y11 shapes below the allowlist (echo, printf without -v, true, `:`, cat, grep between a `VAR=$?` capture and its exit; any other command voids the capture), grouped tests, redirections, exit codes 1..255.
637
- - Not covered (open): TEST_CMD is lexical, so a test hidden behind a function (Y18 `t() { <test>; }; t || true`), a variable (Y19 `C="<test>"; $C || true`) or an unlisted runner (M16 `bun run test || true`) is not seen; `eval` of a string that holds the test; `trap 'exit 0' EXIT`; `set +e`; a dead-branch `exit "$rc"`.
638
- - Siblings swept: Y01, Y02, Y04 to Y09 (writes hidden in `$(( ))`, `printf -v` or `read` through an indirect name, a function that writes rc), exit 256, eval: all fail closed inside a capture-to-exit window and are red self-tests.
639
- - Mechanism: none planned. Revisit only if a real workflow edit triggers one; the structural fix would be running the step under a shell with `set -e -o pipefail` and asserting on exit status in a fixture instead of reading source.
640
- - Fixture: Y01, Y02, Y04, Y05, Y08, Y09, Y10, Y11, X09, X20, X05 self-tests in tests/test-workflow-no-masked-failures.sh.
641
-
642
- ## FC-66 A fixture pushed the real .gitleaksignore as new content and gitleaks flagged one of its own comments (PP-GITLEAKS-FIXTURE)
643
- - User saw: Tests red on main 103278fe8 (run 37833691425, shard 1), deterministic and local: tests/test-pre-push-gitleaks.sh case c8 `FATAL: setup_push_clone's base push failed ... RuleID: generic-api-key File: .gitleaksignore Line: 123`.
644
- - Law: a guard's own config must pass the guard; evidence or it did not happen (the red was latent, not a train regression).
645
- - Cause (measured): setup_clone, setup_push_clone and the case-26 clone copied the real .gitleaksignore, comments included, into a fixture repo and pushed it as new content. Line 123 (a comment that quoted the default receipt key file path) matches generic-api-key. `.gitleaksignore`, `.githooks/pre-push` and the suite are byte-identical between 75bbbb946 and 103278fe8, so the red is latent; the suite only ran once diff selection picked it.
646
- - Siblings swept: `gitleaks dir .gitleaksignore` over every other comment and fingerprint line: line 123 was the only finding. The three fixture copy sites all go through one helper now.
647
- - Mechanism: (1) the comment is reworded so it no longer trips a rule; (2) copy_real_gitleaksignore copies fingerprint lines only (comments carry no suppression), so a future prose comment cannot break fixtures; (3) a guard case scans the real .gitleaksignore with the pinned gitleaks and fails on any finding. No fingerprint added, hook untouched.
648
- - Fixture: tests/test-pre-push-gitleaks.sh "FC-66: the real .gitleaksignore scans clean as content". Red before the fix (suite rc=1 at c8, gitleaks reports 1 leak at line 123), green after.
649
-
650
- ## FC-68 A release can drop a verification check silently and still smoke green (WALL-SMOKE-GUARD)
651
- - User saw: 11.3.2 receipts carried 1 executed check instead of 2. The Wall test was authored, then discarded before verify ("wall base run not_run: 1", "wall test discarded: loki_wall_sum.test.js (not_run)"). Nothing asserted the count, so the release and its smoke stayed green.
652
- - Law: evidence or it did not happen; a green that cannot go red is not a gate. Guard class: the root cause is FC-67 (WALL-NOTRUN holds that row); this row is the detection layer.
653
- - Siblings: any other check a receipt could lose without a failing exit (lint, mutation proof, cross review) is not yet counted; the guard asserts only checks[] executed >= 2 and the Wall test.
654
- - Mechanism: scripts/assert-wall-executed.sh reads the keys seal.ts writes (checks[].result, wall.files, wall.passed, not_proven[]). scripts/real-run.sh applies it to scenarios with SC_WALL_EXECUTED=1 (trivial-sum, two-bug). Post-Release Smoke npm-smoke runs it as a blocking step.
655
- - Q17-04 sibling guard: a scenario sets SC_EXPECT_CHECKS=lint,mutation,review and assert-wall-executed.sh (3rd arg) fails when a named check is absent or not_run in checks[]; unset keeps today's behavior.
656
- - Limit: the real trivial-sum and two-bug runs are billed and CI has no provider key, so Post-Release Smoke cannot run them unattended; the blocking step proves the guard logic on the released commit and runs the real scenario when a key is supplied.
657
- - Fixture: tests/test-wall-executed-guard.sh (11.3.1 shape passes, 11.3.2 shape fails, each leg alone fails).
658
-
659
- ## FC-70 The bump-only allowlist drifted from the files scripts/release.sh bumps (BUMP-REUSE; number may renumber at merge)
660
- - User saw: release run 37837110730 (v11.3.3, eee891534) required-ci printed `normalizer: not on the allowlist: deploy/helm/autonomi/Chart.yaml`, then "Not eligible for parent reuse", then failed on Tests@eee891534 = failed although parent 54a519a22 had Tests success.
661
- - Law: a bump-only commit inherits its parent's Tests verdict; evidence or it did not happen.
662
- - Cause (measured): `git diff --name-only 54a519a22 eee891534` lists 21 paths; four were missing from ALLOWLIST (helm/loki-mode/Chart.yaml, deploy/helm/autonomi/Chart.yaml, deploy/helm/control-plane/Chart.yaml, docs/CLI-REFERENCE.md), all written by scripts/generate-stale-zero.sh, which bump_all_version_files runs (SZ-01) after the allowlist was last edited. Because test.yml's gate uses the same allowlist, its heavy jobs also ran at the bump SHA, and that completed failure then beat the parent reuse (has_conclusion failure is checked before parent reuse, by design: only a missing/cancelled run falls through).
663
- - Siblings swept: release.sh bump_all_version_files slots and generate-stale-zero.sh targets (README.md facts block carries no version and is unchanged by a bump; SKILL.md, Chart.yaml appVersion, CLI-REFERENCE.md generated line are the only generated version carriers). Both copies of the allowlist (release.yml STEP 1, scripts/ci/version-bump-only.sh) updated; they are byte-compared by tests/test-version-bump-only.sh.
664
- - Mechanism: one derivation, not two lists. tests/test-version-bump-only.sh now derives the version-bearing paths from scripts/release.sh (update_version_slot targets) and scripts/generate-stale-zero.sh (Chart.yaml and CLI-REFERENCE targets) and asserts each is in the allowlist of both copies.
665
- - Fixture: tests/test-version-bump-only.sh "FC-70" cases (static subset check, per-file chart/cli-ref pure-bump fixtures, and a replay of 54a519a22 -> eee891534 when both objects exist). Red before the fix (ALLOWLIST lacks the four paths), green after.
666
-
667
- ## FC-71 Promote blocked on a green legacy-titled Post-Release Smoke (PROMOTE-SMOKE-SHA)
668
- - User-visible vs raw: user-visible. Promote run 37838898754 printed "Post-Release Smoke for 75bbbb9... is none" and refused 11.3.2 although smoke run 37833604487 at that head_sha concluded success.
669
- - Law broken: evidence or it did not happen; a gate must read the evidence that exists, not only the newest title format.
670
- - Sibling sweep: the nightly and Tests gates in promote.yml already match by head_sha; only the smoke gate matched by display_title alone.
671
- - Shared mechanism: the smoke gate also accepts a completed workflow_run-event smoke run whose head_sha equals the released commit; newest matching run decides, API error stays fail closed.
672
- - Fixture: tests/test-promote-smoke-sha.sh (legacy-titled green passes, newer red blocks, no match blocks, API error blocks).
673
-
674
- ## FC-72 A release named new commands and flags in its CHANGELOG that never reached `loki help` or Tab completion (HELP-DRIFT)
675
- - User saw: 11.3.2 listed `loki plan --spec`, `loki start --spec` and `loki memory forget` in the CHANGELOG, but none appeared in the 11.3.2 help output or in bash/zsh completion. (Number may renumber at merge.)
676
- - Law: every shipped command or flag appears in help and in Tab completion; a feature a user cannot find does not exist to them.
677
- - Cause (measured): the bash help text, `loki memory help` and both completion files are hand-maintained; tests/test-help-discoverability.sh checks top-level commands only, so subcommands and flags drifted. The Bun route help in loki-ts/src/cli.ts already listed them.
678
- - Siblings swept: `loki export --sarif` (11.3.2) was also missing from completion; added to bash and zsh, along with `plan` flag completion and `memory lessons|learn|forget` in `loki memory help`.
679
- - Mechanism: tests/test-help-changelog-drift.sh reads backticked `loki ...` invocations from the newest 2 CHANGELOG sections and fails when a subcommand or flag is absent from `loki help` / `loki <cmd> --help` / `loki <cmd> help`, or a flag is absent from completions/loki.bash. Prose is never read.
680
- - Fixture: tests/test-help-changelog-drift.sh. Red on origin/main (6 failures: memory forget, plan --spec, start --spec, their completions, export --sarif completion), green after (11 passed, 0 failed).
681
- - Recurrence (origin/main 7d6f1634f, e808a5b29 R100-008a #362): `loki runs` shipped in the Bun route, the CLI registry and both static completions but not in `loki help all`, so tests/test-help-discoverability.sh failed 16/1 ("1 command(s) absent from 'loki help': runs"). The command-name footer printed by `loki help all` in autonomy/loki is hand-maintained; `runs` is added to it. Siblings swept: completions/loki.bash and completions/_loki already list `runs` (tests/test-completion-coverage.sh 6/0); test-help-changelog-drift.sh 27/0. Mutant: remove `runs` from the footer, test-help-discoverability.sh goes red (16/1).
682
-
683
- ## FC-73 Release-commit R0 guard selection ran suites with no green baseline (RELGATE-REDS)
684
- - User saw: Tests red on main eee891534 (run 37837111102, shards 0, 3, 8, 11): test-quick-receipt-order rc=124, test-autonomy-and-stop OPTOUT_FAIL, test-watch-command stale "v6.33.0", test-manifest-truncation.js run under bash (rc=2), node_lint web-app rc=127.
685
- - Law: evidence or it did not happen; a gate may only select checks that are green on an unchanged tree.
686
- - Cause (measured): guards_for() greps tests/ for the literal path and basename of every R0 path. For VERSION and package.json that selects every test that mentions the word (fixtures, prose), including suites absent from tests/run-all-tests.sh that nobody runs, plus one registered suite over the 100s item limit. The package.json glob also returned a .js test that shell_test ran with bash. node_lint ran `npm run lint` in a shard that never installed web-app deps. A genuine eslint error in ProjectWorkspace.tsx was hidden behind that.
687
- - Siblings swept: all 179 G rows of the release plan checked against the full-suite registry (3 unregistered .sh: test-watch-command, test-magic-rarv, test-manifest-truncation.js); other node_lint and pytest rows; the other shard-8 and shard-11 items.
688
- - Mechanism: guards_for keeps the grep as a candidate set filtered by has_baseline (run by the full suite, local-ci or nightly), skips the grep for the ROOT VERSION, package.json and requirements* whose guards are the explicit lists (nested manifests keep the exact-path grep, dropping only the shared basename grep), emits .js guards as node_test, and node_lint installs the target's locked deps when the linter binary is absent.
689
- - Fixture: tests/test-fast-gate.sh "FC-73" cases (release plan of VERSION, package.json, dist; loki-ts/package.json keeps test-sdk-version-sync.sh; a fixture tree proves an unregistered suite is dropped, mutating `} | has_baseline` to `} | cat` goes red). Red before (selects unregistered suites and quick-receipt-order), green after. Stale tests fixed at the root: watch help assertion, autonomy opt-out needs the claude binary, eslint no-unused-expressions.
690
-
691
- ## FC-74 Large source-body captures piped into grep -q under pipefail died of SIGPIPE (RELGATE-REDS)
692
- - User saw: Tests red on main eee891534 (run 37837111102, shard 8): tests/test-verify-runner-selection.sh `printf: write error: Broken pipe`.
693
- - Law: a test must not depend on producer/consumer timing; same class as FC-64.
694
- - Cause (measured): `printf '%s' "$_gate_body" | grep -q ...` under `set -o pipefail`; grep -q exits at the first match, printf of a large captured function body gets SIGPIPE, the pipeline reports failure. Five sites in the suite. Not reproducible on the fast local machine (the pipe buffer absorbs it), so the mechanism is proven by test-sigpipe-guard section 1 and the static pattern.
695
- - Siblings swept: the FC-64 ratchet covers the repo-wide count; this suite now joins the enforced family.
696
- - Mechanism: here-strings (`grep -q ... <<<"$var"`) at all five sites; tests/test-sigpipe-guard.sh section 2 now enforces the suite.
697
- - Fixture: tests/test-sigpipe-guard.sh against the pre-fix copy of the suite fails (5 legacy lines, 3 passed 1 failed); against the fixed suite 4 passed 0 failed.
698
- ## FC-69 FORCE_COLOR in the user's env colored the test output and the Wall base run read no counts (WALL-COLOR)
699
- - User saw: with FORCE_COLOR=3 (node v26) `loki start "sum() skips the first element; fix it" --no-pr` sealed NOT PROVEN "wall base run not_run: 1, wall test discarded" with 1 executed check instead of 2. With FORCE_COLOR unset the same base run is {pass:0, fail:1} and the Wall works. Reproduced 3x by a peer session on 11.3.2; likely present in 11.3.1 and earlier.
700
- - Law: a parser reads what the tool said, not how it was painted; a user's environment must not change a verdict; a Wall test that did not execute is never silent and never VERIFIED (L3 evidence).
701
- - Cause (measured): harness-run test commands inherited process.env, so the runner emitted ANSI. stages/wall.ts classify (nodeIsRed `^(?:#|ℹ) fail [1-9]`, pytest, jest/vitest/bun counters) tested the raw output, so the escape prefix hid the counter, classify returned not_run and the Wall test was discarded. classifyCheck and testCount already stripped ANSI; wall.ts classify, load_owner and the quality_gates strip (CSI only) each had their own or no stripping. Separately, seal listed a not_run Wall only as a count with no reason named.
702
- - Siblings swept: stages/wall.ts RealBaseTestRunner (Wall base run; also the mutation proof via seal.ts and F2P), stages/wall.ts classify, stages/verify.ts runOnce (verify checks, flaky retry, already-done base run in subtractBase), runner/load_owner.ts runOnBase, runner/quality_gates.ts summaryFailedCount (attempts scoring). project_model/install.ts is a dependency install with no parsed output, unchanged.
703
- - Mechanism: util/check_result.ts plainTestEnv() (FORCE_COLOR=0, NO_COLOR=1, CLICOLOR_FORCE removed, CI untouched) for every harness-run test or verify child, no per-runner special case; the single exported stripAnsi() at capture and at classify entry (belt and braces). stages/seal.ts names every not_run Wall file as "Wall test not executed: <file>: <reason>" on the receipt and the console NOT PROVEN line. The verdict still comes from the checks that executed; an unexecuted Wall test is disclosed, not a downgrade.
704
- - Fixture: loki-ts/tests/engine10/fc69_ansi.test.ts (real node --test base run under FORCE_COLOR=3; raw ANSI node spec, TAP, jest, pytest), loki-ts/tests/engine10/fc69_wall_e2e.test.ts (stub-claude engine run under FORCE_COLOR=1 and 3, always-run via scripts/global-guards.tsv), seal.test.ts "FC-69 not_run Wall". Red before the fix (base run not_run:1, Wall file discarded, wall.files 0), green after.
705
- ## FC-55 Intake discovery session retried or hung on a repo with nothing to discover (INTAKE-FAST)
706
- - User saw: intake taking 10-44s (median 28s) on a trivial repo, one replay hanging 120s, before any work began. Raw: the Project Model discovery session is almost all of intake; a no-UI repo answers `ui: {present: false, cite: []}` and validateAnswer rejects it with "packages[0].ui.cite: needs at least one citation", which spends the one retry (a second full session).
707
- - Law: evidence or it did not happen; a validator may demand evidence only for a claim the answer makes; a stage must be bounded by a recorded fallback, never a stall.
708
- - Cause (measured): schema.ts demanded a non-empty ui.cite whether or not ui.present was true. Separately, a repo whose every tracked file is at the root has no package below the root to learn, yet still paid a full session, and nothing bounded a session that never returned.
709
- - Siblings swept: every other optional field in validateAnswer (commands.* null, ui.boot null, install, dependsOn) already accepts its empty form; ui.cite was the only citation demanded for an absent claim. Consumers of an unknown model (loadProjectApi in verify, wall packageOf, implement, run_cap, visual evidence, testmap, before_after) all resolve to null and keep repo-root behavior; commandFor takes its runner command from the harness, and a root-only model never changes it.
710
- - Mechanism: (1) ui.cite is checked only when ui.present is true (a supplied cite is still verified). (2) discoverProjectModel returns unknownModel("", "single-directory repo") with no session when shallowDirs is exactly ["."], after the committed and cached checks. (3) Each discovery session is raced against its limit plus 5s; a hang aborts the session, emits `project_model.fallback`, and returns an unknown model with owner=provider and no retry. Session ceiling 120s to 60s.
711
- - Fixture: loki-ts/tests/project_model/intake_fast.test.ts. Red on origin/main (3 of 7 fail incl. the hang case), green after.
712
-
713
- ## FC-82 A live-server E2E suite failed collection on a CI runner without its client libraries (RED-PURPLE)
714
- - User saw: Tests red on main 082cd57a4 (run 37848611813, shard 4): `FAIL rc=2: py_test tests/docker/test_purple_lab.py`, `ModuleNotFoundError: No module named 'requests'`.
715
- - Law: a test needing an optional dependency or a live service must skip honestly, never error at collection (same family as FC-73 baseline rule).
716
- - Cause (measured): the Purple Lab server still exists (web-app/server.py, autonomy/loki 'loki web', deprecated v7.44.0), so the suite is not stale. It hit `import requests` at module top on a runner without requests, and would also need a server on localhost:57375 that CI never starts.
717
- - Siblings swept: tests/docker/test_polling.py imports checked, no unguarded optional import of this kind.
718
- - Mechanism: pytest.importorskip for pytest_asyncio, requests, websockets plus a module-level skip when BASE_URL is unreachable.
719
- - Fixture: `python3 -m pytest -q tests/docker/test_purple_lab.py` with no server exits 0 reporting a skip; before the fix it exited 2 on a runner without requests.
720
- - r2 (main 4511d9f32): the module-level skip (importorskip / skip(allow_module_level=True)) collected zero tests, so pytest exited rc=5 (no tests collected) and the fast gate reported `FAIL rc=5: py_test`. The skip is now per-test: imports are guarded with try/except, the module collects all tests, and a pytestmark skipif (same reasons) skips each one, giving N skipped with rc=0. Assertions are unchanged and the tests run when deps and server are present.
721
-
722
- ## FC-84 A demo non-TTY test depended on a provider CLI being installed on the host (RED-PLAN)
723
- - User saw: Tests red on main 082cd57a4 (shard 9): tests/test-plan-command.sh `[FAIL] demo non-TTY refuse -- exit=2 (124=timeout/hang)`, 26 passed 1 failed. The exit was 2, not a hang; the label is misleading.
724
- - Law: a test must not depend on host state it did not set up (same class as the "assumed host state" and "autonomy opt-out needs the claude binary" fixes).
725
- - Cause (measured): `loki demo` without --dry-run runs provider_offer_gate before the estimate. With no provider CLI on PATH (CI runner) it prints "No AI provider CLI found" and exits 2, so the case's grep for the estimate and the confirmation message failed while the exit code matched. A dev host with claude installed passes. Reproduced with PATH=/usr/bin:/bin:/usr/sbin:/sbin: rc=2, no estimate.
726
- - Siblings swept: the dry-run case skips the gate (verified passing without a provider); the other provider-gated suites (test-provider-preflight.sh) already build stub PATHs.
727
- - Mechanism: the case now puts a stub claude on PATH so the refusal path under test is reached on every host.
728
- - Fixture: tests/test-plan-command.sh test 25; red with the stub removed on a provider-less PATH, green with it.
729
-
730
- ## FC-80 Shard-coverage watch fails when a guessed duration row displaces LPT packing (MAIN-REDS-1134)
731
- - User saw: Tests red on main 082cd57a4 (run 37848611813, Structural guards and shard 8): tests/test-shard-coverage.sh "expected 5 watched suites in 5 distinct shards", trust-core and E2e Features both in shard 1.
732
- - Law: a data table that drives a deterministic packer must hold measured values; a guessed row silently reshuffles the packing.
733
- - Cause (measured): the HELP-DRIFT merge added the row "help drift (CHANGELOG loki invocations in help and completion)" at 25s; the suite runs in 3.0s locally. The inflated row shifted the LPT rotation so two watched heavy suites landed together. Deterministic.
734
- - Siblings swept: every registered run_test name has a row in tests/shard-durations.tsv (0 missing); the other rows added this train (1, 3, 8s) are small and did not move the watched five.
735
- - Mechanism: row set to the measured 3s; test-shard-coverage passes 19/0.
736
- - Fixture: tests/test-shard-coverage.sh T5 (red at 25, green at 3).
737
-
738
- ## FC-81 Scanner invoked in a test inherits stdin and reads the CI plan instead of _NM_FILES (MAIN-REDS-1134)
739
- - User saw: Tests red on main 082cd57a4 (shard 11): tests/test-nomock-data-render.sh "extension resolution must be deterministic across Python hash seeds", 67 passed 1 failed.
740
- - Law: a probe must use the transport the product uses; an inherited stdin is an input.
741
- - Cause (measured): no_mock_scan.py reads stdin first and falls back to _NM_FILES. The seed loop passed only _NM_FILES, so under the fast-gate runner loop (`done <"$plan"`) the scanner consumed plan lines as the changed-file list, found no hit and printed PASS. Hash seeds were never the cause. Reproduced locally: running the suite with a non-empty file on stdin gave exactly 67/1; with stdin from the terminal it passed.
742
- - Siblings swept: verify.sh and completion-council.sh already pipe the list on stdin; the three stdin-shape probes in the same suite already set stdin explicitly.
743
- - Mechanism: the seed loop feeds the list via here-string on stdin (the production transport). The scanner's extension order is already an explicit tuple.
744
- - Fixture: tests/test-nomock-data-render.sh run with `< nonempty-file`: 67/1 before, 68/0 after.
745
-
746
- ## FC-85 FC-71 changed the promote smoke contract but left the older sibling test stale (RED-NEXTTAG)
747
- - User saw: CI Tests red on main 082cd57a4, suite "release publishes to npm next; promote.yml gates latest on the first-run gate (A-01)": 3 failed, 60 passed in tests/test_release_next_tag.py.
748
- - Law: a contract change updates every test that pins the old contract; a guard is rewritten to the new contract, never deleted or weakened.
749
- - Cause (measured): FC-71 (04905f46c) intentionally taught promote.yml's smoke gate to also accept a workflow_run-event run whose head_sha equals the released commit. Three tests still asserted the old rule (head_sha is never a key; untitled workflow_run runs fail closed; no ".head_sha" in the select). The PROMOTE-SMOKE-SHA merge ran only its own direct suite (test-promote-smoke-sha.sh); the sibling pytest was already registered in tests/run-all-tests.sh and tests/shard-durations.tsv, so the full-suite CI Tests was the first to report it.
750
- - Siblings swept: nightly and Tests gates in promote.yml (structure tests PromoteNightlyGate unchanged and green); no other test greps promote.yml for ".head_sha".
751
- - Mechanism: the tests now assert the new contract: head_sha counts only with event == "workflow_run" and equality to the released commit; dispatch, push and schedule events, a different head_sha, and an empty list still fail closed; the reader test requires exactly one .head_sha, inside the workflow_run clause.
752
- - Fixture: tests/test_release_next_tag.py. Mutation: removing the event restriction in promote.yml turns 3 tests red; restored it is 64 passed.
753
-
754
- ## FC-83 A privacy test spawned real runners and a dashboard install, riding its caps until the shard timed out (RED-FUNNEL)
755
- - User saw: Tests red on main 082cd57a4 (shard 9): tests/test-funnel-privacy.sh FAIL rc=124 past the item limit (est 8s); the log shows three 26s gaps (telemetry-off, DO_NOT_TRACK, analytics-off), a 13s gap on bare `loki`, then `rm: cannot remove .../fx/.loki: Directory not empty`.
756
- - Law: a test must not depend on the host's login state, network or a detached process; every child it starts must exit by its own means, not by the cap.
757
- - Cause (measured): (1) the stub `claude` exited 1 for every call, so on Linux the login state is "unknown" (fails open) and `loki start` entered the real runner, a retry loop that ignores SIGTERM: timeout -k 5 20 = 25s per case (reproduced locally with LOKI_SKIP_AUTH_PREFLIGHT=1: rc=137 elapsed=25s; the macOS keychain made the preflight refuse in 1s and hid it). The runner also bootstrapped caveman through npx (network). (2) bare `loki` runs cmd_ui: control serve, then the classic dashboard with a venv pip install on the fresh HOME and a detached dashboard server (rc=124 at the 20s cap, server left behind, racing the sandbox rm -rf). (3) the Bun route case rides its cap by design.
758
- - Siblings swept: every cap_command and run_cli call site in the suite (3 telemetry cases, disclosure_start, once, route_bash all go through the stub; route_bash already refuses at the preflight).
759
- - Mechanism: the stub `claude` answers `auth status` with loggedIn false so the preflight refuses at once on every platform; run_cli sets LOKI_CONTROL=0 and LOKI_UI_NO_CLASSIC=1 so bare loki starts nothing; cap_command takes CAP_SECS and the Bun route case uses 8s.
760
- - Fixture: tests/test-funnel-privacy.sh itself. Measured locally: bare loki rc=124 at 20s before, 0s after; suite 51s before, 19-20s after (3 runs, rc=0).
761
-
762
- ## FC-86 A plan far above the shard cap overflowed every shard with no fail-safe (PLAN-OVERFLOW)
763
- - User saw: main red. Tests runs 37852209020 (4f8b6386b) and 37851435720 (4511d9f32) lost "Impacted suites" shards 6, 8 and 10 to the job's timeout-minutes: 6 ("The operation was canceled"); shard 8 was about 60% through its pytest run. The plan log read "fast-gate: base=v11.3.2 head=HEAD files=85 planned=1086".
764
- - Law: a bounded resource needs a fail-safe path when the input exceeds the bound; an over-capacity plan must go FULL, never be packed into shards that cannot finish.
765
- - Cause (measured): the packer sets n = min(MAX_SHARDS=12, ceil(total/SHARD_TARGET_S=60)). When total is far above 12*60s the min silently clamps n, so every shard carries more than the target and overflows the 6 minute job cap. Nothing in the plan path checked the clamp.
766
- - Siblings swept: the other plan bounds each already have a FULL row (unparseable-diff, empty-diff, too-many-files at 150, selector-failed, selector-answered-R0, global-guards-unreadable) and the plan step timeout forces full=true on a nonzero rc; the shard count was the one bounded quantity without one.
767
- - Mechanism: in the python packer, ceil(total/target) > maxs writes the single row "FULL\tplan-over-capacity\t0\t0" (write_matrix then emits full=true and full-suite.yml runs) and prints total, target and maxs on stderr. Timeouts, MAX_SHARDS and SHARD_TARGET_S defaults are unchanged.
768
- - Fixture: tests/test-fast-gate.sh PLAN-OVERFLOW (FAST_GATE_MAX_SHARDS=1, FAST_GATE_SHARD_TARGET_S=1 gives FULL and full=true; default env on the same diff gives no FULL).
769
-
770
- ## FC-87 A guard keyed on literal text missed a helper wrapping the same behavior (ENVGUARD-PLAINENV)
771
- - User saw: main red. Full suite / Bun tests failed loki-ts/tests/util/full_env_spawn_guard.test.ts on 9fcd59459 with mismatched [runner/load_owner.ts, engine10/stages/verify.ts, engine10/stages/wall.ts].
772
- - Law: a guard that detects a behavior by its literal spelling must also recognize every helper that performs the same behavior, and the helper must itself enforce the property the guard protects; a refactor to a helper must not make the guard blind.
773
- - Cause (measured): 34fea2c3f (FC-69, WALL-COLOR) replaced `env: process.env` and `...process.env` with `plainTestEnv()` at one spawn site in each of the 3 files. plainTestEnv copied the full parent env (token family and SSH_AUTH_SOCK included) and the bareParent regex did not match it, so counts fell below the allowlist counts. Second defect found: the SAFE test ran over a 15 line window, so a later call's safe helper excused an earlier call's bare process.env (load_owner.ts pgrep).
774
- - Evidence: the engine10 worker env is built at engine10/supervisor.ts:136-137 (withholdGithubTokens, tokens become a sentinel, SSH_AUTH_SOCK deleted at runner/github_token.ts:229) and asserted at engine10/worker.ts:21-34, so stages/verify.ts and stages/wall.ts run token-withheld, except under the operator opt-out LOKI_ALLOW_AGENT_GITHUB_TOKEN=1. runner/load_owner.ts is also imported by project_model/package_suite.ts, outside the worker, so it was not provably token-withheld.
775
- - Mechanism: plainTestEnv (util/check_result.ts) now starts from tokenFreeEnv(base), so a test command never carries credentials; the guard treats plainTestEnv as token-free (SAFE) and a call window now ends at the next call line. Allowlist: verify.ts and wall.ts entries removed (0 violations), load_owner.ts@2 lowered to @1 (the pgrep call, still a real full-env spawn).
776
- - Fixture: tests/util/plain_test_env_tokens.test.ts (planted token family in the parent; plainTestEnv, a child, and a real Wall base run see none; red when plainTestEnv is mutated back to a full copy) and planted cases in full_env_spawn_guard.test.ts (raw process.env spawns still flag 1, plainTestEnv flags 0).
777
- - Sibling question (fast gate): the WALL-COLOR merge did not run this guard. scripts/select-tests.sh rule R4 maps a changed loki-ts/src file only to bun tests whose text contains that file's src-relative stem (for example engine10/stages/wall); full_env_spawn_guard.test.ts walks all of src and names none, so it is never selected, and the fast gate only runs bun typecheck for it. Not fixed here; a follow-up slice should make a loki-ts/src change select the walk-all-src guard tests.
778
-
779
- ## FC-89 A guard that walks all of loki-ts/src names no stem, so the fast gate never selected it (FASTGATE-GLOBAL-GUARDS)
780
- - User saw: WALL-COLOR (FC-69) reached main red; the diff-selected fast gate ran only a typecheck. Third miss of the CTO rule that global guards always run.
781
- - Law: a guard whose scope is the whole tree must be selected by what it is, not by which file changed; a selection keyed on a name in the test text cannot see a test that names nothing.
782
- - Cause (measured): R4 selects a bun test only if its text contains the changed file's src-relative stem. Tests such as budget.test.ts and l2_destructive_registry.test.ts (and pr_body_golden, session, term_width, project_model) read every file under loki-ts/src and name none, so a diff to engine10/stages/wall.ts never selected them. The hard-coded scripts/global-guards.tsv rows covered some guards by hand and rotted for the rest.
783
- - Siblings swept: every loki-ts/tests/**/*.test.ts that walks a src-rooted path (17 found, listed by the marker); three false positives that only walk tmp or tests dirs carry the audited opt-out.
784
- - Mechanism: a test that walks the src tree carries the line "// select: walk-all-src". scripts/select-tests.sh emit_marked_walkers finds them by content and emits each as an R8 row on every diff (skipping any already covered by a global-guards.tsv row or directory row), so a new walker needs no list edit.
785
- - Fixture: tests/test-select-tests.sh FC-89 (a diff touching only loki-ts/src/engine10/stages/wall.ts selects full_env_spawn_guard, fc25_raw_spawn_guard, l0_guard, spawn_env_guard, budget, l2_destructive_registry, never_below_raw; every marked file is selected; a completeness scan fails on any src-walking test with neither "select: walk-all-src" nor "select: not-src-walker (reason)"). Mutation: deleting the marker from l2_destructive_registry.test.ts turns it red on both the selection and the completeness check.
786
-
787
- ## FC-90 Deleting the token env vars left the user's gh login and git credential helpers reachable (GH-KEYRING)
788
- - User saw: nothing yet (found by audit). With GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN and SSH_AUTH_SOCK removed, `gh auth token` exited 0 and `gh api user` returned the user's login on the founder machine, and git held `osxkeychain` plus `!gh auth git-credential` helpers over an HTTPS origin.
789
- - Law: a child spawned by the harness must not reach the user's GitHub credential by default or by accident, through any store the child's env can resolve (env vars, gh config and keyring, git credential helpers), not through the env vars alone; one shared mechanism builds the withheld env, so no env builder can delete a subset and call it token-free. This is not a sandbox.
790
- - Cause (measured): tokenFreeEnv (util/safe_git.ts) and plainTestEnv (util/check_result.ts) only deleted the vars. gh then falls back to GH_CONFIG_DIR/hosts.yml and the OS keyring, and git calls its configured helpers on an HTTPS push. withholdGithubTokens already closed this for the worker, but only for the worker env. Measured with gh: GH_CONFIG_DIR alone leaves `gh auth token` exit 0 (keyring); a sentinel token alone makes `gh auth status` fail but a named-account read (`gh auth token --user`) still reads hosts.yml; both together close it.
791
- - Mechanism: util/credential_env.ts hardenCredentialEnv is the one function: the 4 vars become a non-working sentinel, GH_CONFIG_DIR is a per-process empty mode-700 directory, credential.helper is reset through GIT_CONFIG_COUNT/KEY/VALUE appended after existing entries (the user's gitconfig is never edited and identity survives), GIT_TERMINAL_PROMPT=0 and GCM_INTERACTIVE=never. tokenFreeEnv (so plainTestEnv, safeGitEnv) and withholdGithubTokens both call it; the LOKI_ALLOW_AGENT_GITHUB_TOKEN opt-out stays in withholdGithubTokens only, test envs are always hardened. The empty dir is made once per process (not per call), under LOKI_RUN_TMP when it is a real directory, else the OS temp dir, and removed by exact path at exit (the old per-call scoped dirs leaked).
792
- - Siblings swept: withholdGithubTokens (now shared), tokenFreeEnv callers (attempts, wall_snapshot, verify_pr, start), plainTestEnv callers (wall, verify, load_owner), the worker assertion (sentinel prefix now exported from credential_env.ts), and the bash route (_loki_withhold_github_tokens at autonomy/run.sh main() already sets the sentinel, a scoped GH_CONFIG_DIR, the helper reset and an EXIT trap removal). Raw `process.env` spawns elsewhere inherit an already-withheld process env (runner after autonomous.ts withholdGithubTokens, worker after supervisor.ts) and are policed by full_env_spawn_guard.
793
- - Fixture: tests/util/fc90_credential_env.test.ts (a fake HOME with a planted gh hosts.yml and a planted global git credential helper, each with a positive control on the raw env; red on the old tokenFreeEnv, red when the GH_CONFIG_DIR line, the credential.helper reset or the sentinel is removed; plus a count of loki-gh-config-* dirs in an isolated TMPDIR before and after).
794
- - CI note (fix-forward, cc8e17512 red on ubuntu): the named-account control `gh auth token --user` returned empty on gh for Linux because a flat hosts.yml is not read by a named lookup there (the multi-account `users:` shape is), while the default `gh auth token` reads only the flat one on the same gh. The fixture now probes the installed gh with each shape in a scratch home, plants the one that works in a separate named home, and skips the named-account test (control and hardened assertion together) when no shape works. Verified on ubuntu:24.04 with gh 2.102.0.
795
- - Residual: code running as the same user can unset the variables and call gh or git directly (or read the keyring itself); this closes default and accidental reach, it is not a sandbox. A parent GIT_CONFIG_PARAMETERS helper is neutralised by appending the reset last. SSH keys under ~/.ssh and GIT_SSH_COMMAND are closed only for the worker env (withholdGithubTokens), not for plainTestEnv; a test command can still `ssh` with the user's default keys.
796
-
797
- ## FC-88 The release gate repeated the Tests workflow's typecheck and bun suite on the same tree (WF-GATE-DEDUP)
798
- - User saw: every release waited 6m40s in release.yml's gate step "Bun typecheck + test (protect published loki-ts dist)" (run 37859034694, v11.3.4) for work the Tests workflow had already finished green on the same tree, or on the bump-only parent (FC-70).
799
- - Law: a gate does only what the verdict it depends on cannot; a second copy of a check is latency with no added safety, and it hides which check is the real one.
800
- - Cause (measured): the step ran `bun install`, `bun run typecheck` and the full `bun test` plus a Python and pytest setup for the Wall tests. Tests already runs tsc (test.yml "Typecheck loki-ts (tsc)", lines 193-195) and the bun suites (impacted shards, full-suite fallback), and `release` needs required-ci (release.yml, `needs: [gate, required-ci]`), whose REQUIRED list names Tests and accepts the parent verdict for a bump-only commit. The one thing Tests cannot see was never checked in the gate: that the committed dist is a fresh build of the release SHA and that the packed tarball carries it.
801
- - Siblings swept: publish-npm's pack check (release.yml "Pack once and verify the PACKED tarball", it also rebuilds through prepack; untouched), local-ci.sh's own typecheck and bun test (local tier, not a CI gate; untouched), version-bump-only reuse (unchanged), test.yml typecheck-build (kept as the single typecheck).
802
- - Mechanism: scripts/ci/verify-release-dist.sh. `build` rebuilds loki-ts/dist and compares all four files to the committed dist byte for byte, ignoring only the debugId; `tarball` does the same for the dist inside `npm pack --ignore-scripts`. The gate runs these two and nothing that Tests runs.
803
- - Fixture: tests/test-release-gate-dedup.sh (red when the gate reintroduces bun test, pytest or typecheck, loses either dist check, when release stops needing required-ci, or when required-ci stops requiring Tests; plus verifier behavior on a debugId-only tarball and a real byte difference).
804
-
805
- ## FC-99 Worktree sprawl exhausted the disk and stalled builds (AUTO-REAP)
806
- - User saw: .claude/worktrees grew to 377 worktrees / 56G (cap 15) on the founder's laptop; the volume hit 96% full and ENOSPC stalled builds.
807
- - Law: whatever creates a resource owns its removal; a count cap that only reports (WORKTREE_COUNT at 15) is a control nobody acts on.
808
- - Cause (measured): slice worktrees were created per slice and removed only by a manual scripts/prune-worktrees.sh run; nothing removed them on merge or abandonment, and nothing refused a new one when the disk was nearly full.
809
- - Siblings swept: prune-worktrees.sh (manual, 30 min safety model; kept as the conservative operator tool), the pulse WORKTREE_COUNT (report only), run.sh and sandbox.sh worktrees (inside user projects, not .claude/worktrees; unchanged).
810
- - Mechanism: scripts/v10-worktree-reap.sh removes (non-force, by exact path) clean worktrees merged into origin/main or idle past 6h by HEAD reflog mtime and not on an active BOARD row; idle dirty ones are salvaged to wt-save/<name> first (node_modules and .rv deleted, loki-ts/dist discarded); detached unmerged HEADs get a wt-save ref; locked and fresh ones are kept. It runs after each main sync in train-cycle.sh and from .githooks/post-merge. scripts/v10-worktree-add.sh enforces the 40G floor (LOKI_WORKTREE_DISK_FLOOR_GB), reaping first. Pulse emits WORKTREE_SPRAWL above 20.
811
- - Fixture: tests/test-worktree-reap.sh (merged, idle, detached, dirty-idle, .rv, dist-only, dirty-fresh, locked, fresh, BOARD-held, floor refusal, pulse 20 vs 21).
812
- - Known limits: a locked worktree whose owning agent is dead is kept (the reaper cannot tell dead from alive, so it never touches a locked one); and slice worktrees created by the agent harness bypass the disk floor unless the dispatcher creates them through scripts/v10-worktree-add.sh.
813
-
814
- ## FC-100 The merge gate accepted a proof not bound to the PR head (MERGE-HEAD-PIN)
815
- - User saw: a PR could merge on a VERIFIED verdict that was earned by a different commit than the one merged (last-proof-id pointed at whatever ran last, and gh pr merge named no expected head).
816
- - Law: a verdict binds to the exact bytes it judged; the merge must prove the head it lands equals the head that was proven.
817
- - Cause (measured): _merge_pr_gated read .loki/state/last-proof-id.txt without comparing facts.git.head_sha to the PR headRefOid, and called gh pr merge without --match-head-commit; a scan that stopped at the first matching proof let an older VERIFIED override a newer NOT_PROVEN.
818
- - Siblings swept: verify-pr respond headRefOid compare (P12-04, loki-ts); the queue path shares _merge_pr_gated and is pinned too.
819
- - Mechanism: the gate reads headRefOid, requires every clean (non-dirty) proof for that head to agree on VERIFIED, refuses with gate: head when none exists or the head is unreadable or moved, and merges with --match-head-commit.
820
- - Fixture: tests/test-loki-merge-queue.sh (pin flag, stale proof, proof resolved by head, older VERIFIED vs newer NOT_PROVEN, dirty proof, unreadable head, head moved).
821
- - Known limits: a proof without facts.git.head_sha never matches (fails closed); dirty is ignoring .loki/ only.
822
-
823
- ## FC-101 A receipt field the writer always emits was treated as optional, so deleting it passed as UNSIGNED (RECEIPT-FIELD-OPTIONAL)
824
- - User saw: a hand-edited receipt with events_sha256, time or time.stages removed (or set to null or a string) verified as UNSIGNED, and --allow-unsigned accepted it.
825
- - Law: a verifier may treat a field as optional only if some writer omits it; absence of an always-written field is evidence of editing, never a pass.
826
- - Cause (measured): checkEventLog returned null when events_sha256 was not a string, and the P12-01b stages check accepted any non-object time; seal.ts has written both fields since ad80fd4e6.
827
- - Siblings swept: the group receipt (seal_group.ts), Python proof receipts and control-plane fixtures never carry these fields and are not gated; signed receipts already cover both fields through receipt_sha256 and the attestation. The control-plane verify fixture (packages/control-plane/test/server/verify.test.ts signed()) and the undo_plan, undo_apply and workspace e2e receipt/1 fixtures are gated and now carry seal-shaped fields (empty-log hash with zero stages).
828
- - Mechanism: verify_cmd.ts checkEventLog, for schema loki.v10.receipt/1 only, returns TAMPERED naming the missing field (events_sha256 not 64-char lowercase hex, time absent or null, time.stages not a plain object) and ends "Re-run the task to produce a fresh receipt."
829
- - Fixture: loki-ts/tests/engine10/verify_cmd.test.ts P12-01c (field missing, malformed hash, time removed with stages at top level, time null, stages string, stages array, honest zero-stage receipt stays UNSIGNED); hand-built receipt/1 fixtures in seal_group, log_seal and visual_evidence tests now carry both fields.
830
- - Known limits: closed by FC-103 (a schema variant or no schema field is now TAMPERED).
831
-
832
- ## FC-102 A resumed run lost intake's deferred already_satisfied flag (D61-04, P12-07j) (RESUME-ALREADY-SATISFIED)
833
- - User saw: after `loki start --resume`, a run whose intent was already satisfied committed and verified its own diff and opened a redundant PR instead of short-circuiting to ALREADY_SATISFIED. Raw: the deferred hit was applied in memory only.
834
- - Cause (measured): the deferred hit mutates intake's data after intake's stage.completed was emitted (machine.ts keeps the same object for that reason), so resumeMachineOpts restored intake from a payload that never had the flag.
835
- - Law: L4 (the durable log must carry every fact a later stage reads).
836
- - Siblings swept: every other stage output restored by resumeMachineOpts is final before its stage.completed emit; intake is the only stage mutated afterward. Readers of intake.already_satisfied: machine.ts mustJump, seal.ts verdictOf, discard.ts.
837
- - Mechanism: the worker emits `already.satisfied` (already_done_async.ts, after hitsUnchangedFromBase and apply succeed). SupervisorLog.ingest attests it as the SUPERVISOR_ONLY event `already.attested` only for a well-formed deferred hit while implement is in flight; a worker line of type already.attested is dropped. resumeMachineOpts restores the flag only from already.attested that precedes implement's stage.completed with no later intent.revised (stale evidence). Absent, malformed or rejected means not satisfied; old logs restore as before.
838
- - Guarantee: a restored flag jumps past verify. VERIFIED is impossible because verdictOf returns ALREADY_SATISFIED, or PARTIAL without proof, whenever intake.already_satisfied is true.
839
- - Fixture: loki-ts/tests/engine10/fix_resume.test.ts P12-07j (survives, old log, unattested worker event, malformed, intent.revised, post-implement, ingest attestation, verdictOf); tests/engine10/already_done.test.ts (emit payload and ordering).
840
- - Known limits: a compromised worker can still fake a well-formed already.satisfied while implement is in flight, the same claim it can already make through the LOKI_ALREADY_DONE marker; the attestation closes forgery outside that window and log edits (tamper.detected). discard.ts restores staged paths on this flag, so that window remains the trust boundary. comment and comment_argv are not restored.
841
-
842
- ## FC-103 An unrecognized or missing receipt schema skipped every schema-gated check and read UNSIGNED, and a verify test turned a failed verification into SKIP (RECEIPT-SCHEMA-UNKNOWN)
843
- - User saw: a re-hashed unsigned receipt whose schema was renamed or removed verified as UNSIGNED (exit 0 under --allow-unsigned); tests/test-mcp-tasks-verify-readonly.sh printed SKIP and exited 0 when the signed verify returned a non-VERIFIED result with no verdict line.
844
- - Law: a verifier dispatches on a closed set of schemas; anything outside the set is evidence of editing, never a pass. A test may skip only on a named absent precondition, never on a verification result.
845
- - Cause (measured): schema-gated checks (FC-101, group section) were opt-in per schema, so a receipt that matched no schema matched no check; the test's catch-all case branch mapped every output without "verdict: " to SKIP.
846
- - Siblings swept: DSSE envelope and composite.json dispatch before this point and have their own hash checks; group/1 without a group section was closed in P12-01d; the other verify-path tests (test-loki-verify-*, verify_cmd.test.ts) assert on verdicts, not on skips.
847
- - Mechanism: verify_cmd.ts verifyReceipt returns TAMPERED naming "unrecognized receipt schema" unless schema is loki.v10.receipt/1 or loki.v10.group/1 (P12-01d, extended here by tests); the MCP verify test (classify_non_verified) fails on every non-VERIFIED result and SKIPs only when the output contains the engine message "cannot run on this machine: no working bun" (bin/loki checks PATH and the bundled @oven/bun-*, so a PATH probe is the wrong precondition).
848
- - Fixture: loki-ts/tests/engine10/verify_cmd.test.ts P12-01d loop (renamed, removed, non-string, empty, null, case-variant); tests/test-mcp-tasks-verify-readonly.sh run with PATH lacking bun, a bundled node_modules/@oven/bun-*/bin/bun and LOKI_TS_ENTRY at a crashing stub (old: SKIP 6/0, new: FAIL) plus the classify_non_verified cases.
849
- - Known limits: the only SKIP is the engine-reported "no working bun"; a bun that exists but crashes fails. The unsigned cases of the same script always run.
850
-
851
- ## FC-104 A steer claim linked to a normal archive was swept as "applied" although the consumer died before applying it (STEER-APPLIED-UNPROVEN)
852
- - User saw: an operator steer vanished: never injected, never reported, and Seal passed.
853
- - Law: "applied" must be provable from a durable record written before the evidence that could contradict it is removed; an inode match proves only that a link happened.
854
- - Cause (measured): consumeSteer linked the claim to human-input-<stamp>.md, unlinked the claim, then recorded revisions. P12-02f's sweep removed any claim sharing an inode with a normal archive, so a kill between link and record (or unlink and record) lost the steer silently.
855
- - Siblings swept: ORPHAN and REJECTED archives (already reported when made, still removed quietly); the no-hardlink rename fallback in archiveTo (EPERM, ENOTSUP, EXDEV, EMLINK), which now also runs the record and marker step right after the rename (no claim survives there, so the order is record after rename); a dropped call there left the sink and intent-revisions.jsonl empty and Seal passing.
856
- - Mechanism: archiveTo runs an onLinked step before the claim unlink: revisions are recorded, then a .applied-<archive> marker is written. sweepOrphans removes a claim linked to a normal archive only when that marker exists; otherwise it archives it ORPHAN and reports "orphaned claim" so Seal refuses.
857
- - Fixture: loki-ts/tests/e10ext/steer.test.ts (P12-02g kill-before-record, marker-before-unlink order, applied-then-unlink-failed removed quietly).
858
- - Known limits: markers whose archive is gone are pruned by the sweep; the marker is agent-writable like the revision file; a kill between record and marker reports a recorded steer as unapplied (fails closed).
859
- - Known limits (no-hardlink filesystems): a kill after the rename fallback and before the revision record leaves no claim to sweep, so that steer is lost unreported; the order is unchanged from P12-02f.
860
-
861
- ## FC-REQ Recovery treated a flaky pass, a protocol violation and a requirements conflict as the same transient failure (P12-11) (RECOVERY-CLASSES)
862
- - User saw: a test that failed once and passed on rerun was reported as a failure or retried blindly; a conflicting spec was re-prompted until the budget ran out.
863
- - Law: a class is claimed only from structured evidence, never from agent prose; an unrecognized failure stays transient and is retried (retry_class.ts fail-safe).
864
- - Cause (measured): decideRecovery knew only permanent, transient, build_failed, tree_corrupt and provider_unavailable; supervisor.ts hardcoded `flaky: []` and nothing remembered failures across runs.
865
- - Siblings swept: verify.ts already reruns once and marks fail-then-pass "flaky" (the evidence source); retry_class.ts classifyFailure is unchanged; the permanent-failure stop and the repeated-signature breaker still run before any new class.
866
- - Mechanism: recovery_policy.ts adds RecoverySignals rerun (same non-empty tree on fail and pass), protocolError and conflictingRequirements. Results: flaky -> retry, protocol_error -> revise, conflicting_requirements -> escalate. util/failure_memory.ts recordFingerprints (extends the existing failure-memory owner; flaky.json and failures.jsonl keep no per-run rows) appends fingerprints to .loki/state/fingerprints.jsonl (append-only, newest 200 kept) and supervisor.ts feeds verify's flaky list into it and the summary.
867
- - Fixture: loki-ts/tests/runner/harness_intelligence.test.ts "P12-11 recovery classes (FC-REQ)" (classifier table, fingerprints across two runs, bound).
868
- - Known limits: protocolError and conflictingRequirements have no producer wired yet (callers must set them from structured signals); fingerprints are recorded for flaky only and not yet read back by a policy rule; verify's flaky list reaches decideRecovery as the rerun signal (P12-11b) and Q17-03 adds failedChecks so a real failure beside a flaky check still revises (failedChecks has no production producer yet, so absent data keeps flaky-wins); engine10 renderMainOutput still passes flaky: [] (names reach the user via seal notProven); the bounding rewrite is best effort and a concurrent append can lose a row.
869
-
870
- ## FC-P12-32 Router cost objective is built but dark in production (RESOLVED by P12-32b)
871
- - User-visible vs raw: no route decision changes today; the expected accepted-outcome cost ranking (history.ts routeScore, rankExecutorsByCost, haikuFloorExecutor) has no production caller.
872
- - Law broken: a control nobody reads is theatre (reader without a live caller).
873
- - Cause (measured): grep of loki-ts/src finds zero callers of resolveExecutor (decision.ts) and none that sets its historyFloor input, so haikuFloorExecutor is unreachable outside tests.
874
- - Siblings swept: decision.ts envOverride, model_rank.ts nextState and issues_epic.ts are the only decision.ts importers; none builds a ResolveInput.
875
- - Mechanism: seal.ts recordRun now also records FAILED as a code-owned loss (cost_preview.ts outcomeFor), so acceptance is measured; the remaining step is a caller that builds ResolveInput with historyFloor = haikuFloorExecutor(...) when the router resolves the stage executor.
876
- - Fixture: loki-ts/tests/runner/cost_preview.test.ts "P12-32 outcomeFor", router_history.test.ts P12-32 cases.
877
- - Resolved (P12-32b): implement_route.ts routeStart passes a lazy repoHistoryFloor to unit_model.ts routeRecord, which applies history.ts historyRaisedRoute (through resolveExecutor) to a valid cheapest-tier route; history only raises, and no history, below-floor history or corrupt history gives today's routing. Fixture: router_history.test.ts "P12-32b live cost routing".
878
- - Known limits: acceptance counts only VERIFIED and FAILED; PARTIAL runs are not recorded; history never selects a cheaper unproven model, by design.
879
-
880
- ## FC-M25W Modernize final report and reslice event are built but never written in production (MODERNIZE-REPORT-DARK)
881
- - User-visible vs raw: a real `loki modernize` run leaves no `.loki/modernize/<mid>/report.json`, so no sealed COMPLETE or NOT PROVEN verdict reaches the user or the M-27 pilot; units re-sliced is never recorded.
882
- - Law broken: a control nobody reads is theatre (writer without a live caller; event declared with no producer).
883
- - Cause (measured 2026-10-16, M-27 HIGH review): `git grep writeReport -- loki-ts/src` finds only its definition at report.ts:106; `git grep unit.resliced -- loki-ts/src` finds only the declaration at types.ts:10 (reslice.ts never appends it).
884
- - Siblings to sweep: every name in the modernize types.ts event list must have at least one producer in loki-ts/src; cost has no per-unit producer either (only budget.hit spent_usd at scheduler.ts:203).
885
- - Mechanism: the coordinator (loki-ts/src/util/modernize_coordinator.ts) calls writeReport once at the end of a run; reslice.ts appends unit.resliced; a guard test asserts every declared event name has a producer.
886
- - Fixture: loki-ts/tests/engine10/modernize/e2e.test.ts asserts report.json exists after the stub run; a new producer-coverage case in the existing modernize test file.
887
-
888
- ## FC-M25W-b Modernize events unit.planned and codemod.skipped are declared with no producer (MODERNIZE-EVENT-DARK)
889
- - User-visible vs raw: the modernize event log never shows which units were planned or why a codemod was skipped, so a user cannot see the plan or a missing-tool reason; the raw cause is two names in the types.ts event list with no append call.
890
- - Law broken: a control nobody reads is theatre (event declared with no producer).
891
- - Cause (measured 2026-10-16, M-25w build): `grep -rn "unit.planned\|codemod.skipped" loki-ts/src` finds only types.ts:8-9 and comments in codemod.ts:4,10; the M-25w producer guard allowlists both names.
892
- - Siblings to sweep: every name in the modernize types.ts event list; the allowlist in the M-25w producer guard must shrink to empty.
893
- - Mechanism: the planner appends unit.planned once per unit; the codemod caller appends codemod.skipped with the reason codemod.ts already returns; then delete both allowlist entries so the existing guard covers them.
894
- - Fixture: the M-25w producer-coverage case with an empty allowlist.
895
-
896
- ## FC-COST-PARTIAL A cost total built from ledger lines alone is reported as complete when the main records carry tokens but no dollar figure (COST-PARTIAL-HIDDEN)
897
- - User-visible vs raw: `loki` cost and the C-F1 benchmark show a small total with cost_partial false, so a run looks cheaper than it was and the 2x cost claim could rest on an understated number.
898
- - Law broken: a total that omits unpriced spend must say it is a lower bound.
899
- - Cause (measured 2026-10-17, COST-WALL HIGH review F1): autonomy/lib/efficiency_cost.py lines 316-325; a main efficiency record with tokens and no cost_usd plus one priced child-ledger line gives usd 0.01, cost_partial False. engine10 omits cost_usd when a session reports no dollar figure.
900
- - Siblings to sweep: every reader of cost_partial (autonomy/lib/cost-summary.py, the C-F1 benchmark row_true_spend, dashboard cost views).
901
- - Mechanism: when main records exist and cost_available is false, set cost_partial true in the one reader; siblings consume that flag.
902
- - Fixture: tests/test_efficiency_cost_availability.py case with token-only main record plus a priced ledger line asserts cost_partial true.
903
-
904
- ## FC-REALRUN-FIXTURE A pinned scenario expectation changed without the fixture receipt that the harness self-test checks it against (REALRUN-FIXTURE-DRIFT)
905
- - User-visible vs raw: v11.17.8 never published; Tests run 38986362613 failed only "real-run harness scenarios, dry install, doctored receipt" with `FAIL wall-guard: expected check "node:sum.test.js" executed in checks[]`.
906
- - Law broken: a change to an expectation must update every fixture that exercises it in the same slice.
907
- - Cause (measured 2026-10-17): Q17-04b (e1bdde7d3) set SC_EXPECT_CHECKS=node:sum.test.js in tests/real-run/scenarios/trivial-sum.sh; tests/test-real-run.sh good.json still named its checks "a" and "b". The slice gate ran tests/test-wall-executed-guard.sh but not tests/test-real-run.sh.
908
- - Siblings to sweep: every user of --check-receipt or SC_EXPECT_CHECKS (grep finds only tests/test-real-run.sh, tests/test-wall-executed-guard.sh, scripts/real-run.sh).
909
- - Mechanism: the fixture names the pinned check; slice cards touching tests/real-run/scenarios list tests/test-real-run.sh in their commands.
910
- - Fixture: tests/test-real-run.sh good receipt passes (13 PASS, rc=0 locally after the fix).
911
-
912
- ## FC-COST-PARTIAL-b Partial cost still readable as complete in sibling consumers (COST-PARTIAL-SIBLINGS)
913
- - User-visible vs raw: a run whose cost is only partly measured can still be shown as a complete number in the runner equivalence report, the bench-run report, or a spend-cap message without its lower-bound label.
914
- - Law broken: one shared mechanism (FC-COST-PARTIAL fixed attribution.py only; siblings read cost without the partial flag).
915
- - Cause (COST-WALL-d HIGH reviews 2026-10-17): equivalence_report.py:334-374 and report.py:280 ignore adapter.provenance.cost_partial; row_spend_is_partial returns False for cost_usd None with true_cost_usd set; efficiency_cost.py counts ledger_invalid_lines without setting cost_partial; a stored cost_usd 0.0 with a larger true_cost_usd renders complete.
916
- - Mechanism: one helper decides partial (null cost, cost below ledger or true cost, invalid ledger lines, unpriced lines) and every cost consumer calls it.
917
- - Fixture: cases in benchmarks/bench/tests/test_attribution.py and tests/test_efficiency_cost_availability.py for each sibling.
918
-
919
- ## FC-25c-ci Git env overrides leaked by an earlier suite silence repo-config controls (GIT-ENV-LEAK)
920
- - User-visible vs raw: v11.17.9 never published; Tests run 38988194257 failed 2 FC-25c(a) controls on ubuntu while they passed locally per file.
921
- - Law broken: a test must be hermetic; a control that depends on process-wide state is not a control.
922
- - Cause (reproduced 2026-10-17): runAutonomous (autonomous.ts:448) and issues_run.ts:512 call withholdGithubTokens on process.env inside the shared bun process, leaving GIT_SSH_COMMAND=false and a GIT_CONFIG_COUNT credential.helper reset; both override repo config. Running the old file with those four variables set reproduces exactly the 2 CI failures.
923
- - Siblings to sweep: every test that asserts a repo-local git config value runs (fsmonitor, filter, ssh, credential, uploadpack).
924
- - Mechanism: hermeticGit wrapper in fc25_safe_git.test.ts removes GIT_SSH, GIT_SSH_COMMAND, GIT_CONFIG_PARAMETERS and GIT_CONFIG_COUNT/KEY/VALUE for the test and restores them after.
925
- - Fixture: the same file run with GIT_SSH_COMMAND=false GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=credential.helper GIT_CONFIG_VALUE_0= passes 14/0.
926
-
927
- ## FC-TEN-QUALITY-01 The fix session gets a bare check name with no assertion text (FIX-BRIEF-BLIND)
928
- - User-visible vs raw: the user saw fix rounds repeat or stall on a failing test. Raw pytest printed `E assert 4 == 5`; the fix brief carried only the check name and the rerun command.
929
- - Law broken: L4 (the fixer must see the evidence the verifier saw).
930
- - Siblings: every runner whose decisive block differs: pytest `E ` lines, jest/vitest Expected/Received, node AssertionError, go `--- FAIL`, tsc/ruff diagnostics; holdout checks must stay hidden.
931
- - Mechanism: one failureExcerpt() in util/check_result.ts (redacted, repo-relative, at most 1500 bytes per check, 6 KB total), held in a stage-local map in verify.ts. It is never part of the check signature, never emitted in test.result, never computed for holdout checks.
932
- - Fixture: fix.test.ts (failing pytest `assert add(2,2) == 5`), check_result.test.ts (one fixture per runner plus size cap and AKIA redaction), wall_holdout.test.ts (holdout text absent from failures_grouped, events and brief).
933
-
934
- ## FC-TEN-QUALITY-02 Pre-existing lint cannot be told apart from lint the change introduced (LINT-NO-BASELINE)
935
- - User-visible vs raw: the user saw a fix loop spending rounds on lint that was already in the repo. Raw base output showed the same diagnostics before the change.
936
- - Law broken: L4 (attribution must compare head to base, as tests already do via attribute()).
937
- - Siblings: ruff concise, shellcheck gcc, eslint unix parsers; tsc is out of scope; a file new on head has no base.
938
- - Mechanism: one pure attributeStatic(tool, headOut, baseOut) in runner/repo_profile.ts next to attribute() and attributeFullSuite(). Multiset keyed path/code/message (line and column dropped). Fails closed: unparseable head, empty parse with non-zero exit, missing base and unknown tool all count as introduced.
939
- - Fixture: harness_intelligence.test.ts cases for same, extra, changed-message, shifted-line, garbage and null-base inputs, plus a recorded real sample per parser.
940
-
941
- ## FC-TEN-QUALITY-03 A pre-existing lint failure drives fix rounds because verify never asks the baseline (LINT-DRIVES-FIX)
942
- - User-visible vs raw: the user saw lint:ruff in failures_grouped for a file whose base already failed it. Raw base content run through the same tool gave the same diagnostics.
943
- - Law broken: L4, and the one-mechanism rule (tests already excuse pre-existing failures via attr.preExistingChecks / preRed.names).
944
- - Siblings: ruff, shellcheck, eslint per-file checks; tsc stays untouched; files new on head are never excused.
945
- - Mechanism: runLintChecks in verify.ts feeds base content on stdin to the tool and calls attributeStatic from QUALITY-02, joining the existing preExistingChecks path (at most 4 core lines).
946
- - Fixture: verify_lint.test.ts with a repo fixture and a stub ruff: base has the unused import (excused), head adds a new one (still reported), a new-file F401 (never excused).
947
-
948
- ## FC-TEN-QUALITY-07 The fix brief says the diff is unavailable because its only reader has no writer (DEAD-DIFF-STAT)
949
- - User-visible vs raw: every real fix brief read "(diff stat is not available)". The working tree had the changed files.
950
- - Law broken: reader/writer key contract: fix.ts:62 reads prior.implement.diff_stat and no stage writes it.
951
- - Siblings: buildFixBrief and buildFixResumeBrief both consume the diff; any other reader of implement.diff_stat.
952
- - Mechanism: one currentDiff(repoDir, baseSha, maxBytes=6000) in runner/session_resume.ts via safeGit, computed fresh each fix round; excludes .loki/, names untracked files without touching the index, truncates with an explicit note.
953
- - Fixture: fix.test.ts and fix_resume.test.ts: temp repo with a modified file and no diff_stat shows "1 file changed"; round 2 shows the hunk; .loki/ never appears; a 50 KB diff cuts at 6000 bytes with the note.
954
-
955
- ## FC-TEN-COST-06 The xreview claude judge spends tokens with lean flags off and no ledger line (XREVIEW-UNMETERED)
956
- - User-visible vs raw: cost totals omitted the cross-review judge. Raw claude calls ran with full settings and no child-session line.
957
- - Law broken: cost accounting must cover every model call (see FC-COST-PARTIAL).
958
- - Siblings: the codex argv (unchanged); other stages that spawn claude children and already use withChildLedger.
959
- - Mechanism: reviewArgv adds leanReviewerFlags() and --output-format json for claude; the run is wrapped in withChildLedger(run, lokiDir, 'xreview'). Verdict rules unchanged: no VERDICT line or bad JSON is not_run, never PASS.
960
- - Fixture: xreview.test.ts: argv contains the lean flags and json format; a stubbed run writes a phase xreview line to .loki/metrics/child-sessions.jsonl; JSON `VERDICT: BLOCK` parses to block; non-JSON gives not_run.
961
-
962
- ## FC-TEN-DEPLOY-08 The web app reports a deployed URL it never obtained and stages everything on push (DEPLOY-FABRICATED-URL)
963
- - User-visible vs raw: a deploy with no URL in its output returned `https://vercel.app` as success. Raw provider output had no URL, and no receipt gate ran.
964
- - Law broken: a success claim must key on a parsed confirmation (fallback success is a false ack); the CLI deploy gate is the only path.
965
- - Siblings: deploy_session and github_push_session in web-app/server.py; hardcoded netlify.app and github.io fallbacks; the bulk-stage and `--public` push path; web-app consumers of url.
966
- - Mechanism: both handlers call the gated CLI (`loki deploy --execute --dir <dir>`, `loki github publish`) as argv lists and read the newest .loki/deploys record; url is deployed_url or null; refusals return 4xx with reasons.
967
- - Fixture: web-app/tests/test_server.py with monkeypatched subprocess; a grep for the three fallback URLs in server.py must exit 1.
968
-
969
- ## FC-TEN-STABILITY-01 bench-suite builds VAR=1 hyperfine commands that hyperfine 1.20 refuses (BENCH-ARGV-ENV)
970
- - User-visible vs raw: the v11.17.6 backstop went red. Raw hyperfine 1.20 refused any positional command matching ^[A-Z_]+=, and the script skipped those commands without failing.
971
- - Law broken: a gate that skips its own work must fail, not pass quietly.
972
- - Siblings: benchPair and benchRoute call sites in loki-ts/scripts/bench-suite.ts; fixed /tmp/bench-*.json export paths; partial SKIP exit code.
973
- - Mechanism: one exported pure argv builder emits `env LOKI_LEGACY_BASH=1 <shim> <cmd>` (works on hyperfine 1.18 and 1.20); exports go under a mkdtemp dir; any SKIPPED command exits non-zero.
974
- - Fixture: loki-ts/tests/scripts/bench_suite_argv.test.ts with a fake hyperfine on PATH that exits 1 with the 1.20 message on a VAR= command.
975
-
976
- ## FC-TEN-STABILITY-05 wall_concurrent races the 5s bun test timeout (WC-TIMEOUT-RACE)
977
- - User-visible vs raw: nightly 38938949858 failed. Raw duration was 5004.82ms against the 5000ms default; assertions were correct.
978
- - Law broken: a test must be hermetic and not depend on machine speed.
979
- - Siblings: other engine10 suites that init, add and commit a repo per test.
980
- - Mechanism: build the repo fixture once in beforeAll and copy it per test; give the concurrency case an explicit timeout sized from measured CI duration; every assertion unchanged.
981
- - Fixture: `bun test --timeout 1500 tests/engine10/wall_concurrent.test.ts` under CPU load, and a 20-run loop of the plain command.
982
-
983
- ## FC-TEN-UX-02 Every failed start prints the same generic Next line (NEXT-NOT-SPECIFIC)
984
- - User-visible vs raw: auth failure, credit exhaustion, a failed check, a spec conflict and a stall all told the user `loki doctor` or `loki undo`. Raw reason strings from output.ts reasonOf name the actual cause.
985
- - Law broken: the failure message must name the exact fix for the cause that happened.
986
- - Siblings: every literal reason produced by reasonOf (output.ts:74); the supervisor call site that passes the reason.
987
- - Mechanism: nextOf in util/run_summary.ts takes reason and outcome and maps each failure class to one command; verified, done-unverified, budget and stopped output stays byte-identical.
988
- - Fixture: run_summary.test.ts table rows copied from the real reason literals, a pin of the old outputs, and a source scan that every reasonOf literal has a mapping; tests/features/contract.test.ts covers fatal:auth end to end.
989
-
990
- ## FC-AGENT-PROOF A VERIFIED verdict proven only by agent-authored evidence (TEN-GATES-01)
991
- - User-visible vs raw: the receipt says VERIFIED, but every executed pass is a test the implementer added in the same diff or an LLM judge verdict; raw shows no check the agent could not write itself.
992
- - Law broken: success needs evidence the implementing agent did not author (exogenous gates are the moat).
993
- - Siblings to sweep: engine10 seal proof, proof-generator headline, deep council pass.
994
- - Mechanism: util/check_result.ts checkProvenance classifies each check (exogenous, independent, agent_authored, llm_judge; unknown is exogenous) and hasExogenousProof is the one predicate every consumer calls.
995
- - Fixture: loki-ts/tests/engine10/check_result.test.ts "TEN-GATES-01 check provenance" (added test file, same path pre-existing, Wall path, council, unknown name).
996
-
997
- ## FC-TEN-GROUP-AUTH Group child sessions lose the OS identity vars and report no cost (SCOPED-ENV-NO-IDENTITY)
998
- - User-visible vs raw: every unit session of a group run printed "Not logged in" and failed in about 6s at $0 on macOS Claude OAuth; the run then said nothing about the dollars the surviving units spent.
999
- - Law broken: a scoped child env must keep what the provider CLI needs to authenticate (USER and LOGNAME key the macOS keychain lookup) while still dropping unrelated secrets; spend that happened must be reported, never silently unmeasured.
1000
- - Siblings: run_group.ts ENV_KEEP (broken); commands/start.ts START_ENV_NAMES and util/skill_checks.ts SKILL_CHECK_ENV_ALLOW already keep USER, LOGNAME, SHELL; features/speed/fix_units.ts suiteTail (the group's fix-unit suite run) was a fourth allowlist and now keeps the same three vars; a git grep of env allowlists in loki-ts/src found no other. The remaining child-env helpers copy the full env and remove named secrets, so they never dropped identity.
1001
- - Mechanism: ENV_KEEP in run_group.ts gains USER, LOGNAME, SHELL; parseGroupCost and groupCostLine sum the per-unit cost rows of every backlog.py launch and print one measured, partial or not-measured `Cost:` line like a single run, and writeGroupCostRecord rolls the sum into the parent .loki/metrics/efficiency so collect_efficiency, receipts and the bench read it. parseGroupCost ignores output with no Summary table. No shared base list: the allowlists differ in everything but the identity trio, so a shared list would not remove the copies.
1002
- - Fixture: tests/features/run_group_env_cost.test.ts (scopedEnv keeps USER and LOGNAME and drops an unrelated secret; group cost equals the sum of unit costs, unmeasured rows read partial, none reads not measured; a runGroup test with a fake launcher asserts the Cost line and the efficiency record); tests/runner/spawn_env_guard.test.ts stays green.
1003
-
1004
- ## FC-BUNDLE-SDK-LEAK A server bundle pulls loki-ts SDK code through a one-constant import (v11.17.14)
1005
- - User-visible vs raw: v11.17.14 never reached npm and the 11.17.15 docker images failed to build; raw was `Could not resolve: "@anthropic-ai/sdk"` while bundling packages/control-plane src/server/serve.ts.
1006
- - Law broken: a module imported by the control-plane server must not reach runner code that needs loki-ts runtime deps; a shared constant lives in a leaf module.
1007
- - Siblings: deep.ts was the only engine10 stage importing runner/quality_gates.ts (git grep); xreview.ts already imports providers/claude_flags.ts. pack-npm also lacked a loki-ts install, fixed in 386e73f32.
1008
- - Mechanism: REVIEW_GUARD_DENYLIST moves to providers/claude_flags.ts; quality_gates.ts re-exports it; deep.ts imports the leaf.
1009
- - Fixture: tests/test-control-plane.sh builds serve.ts with a metafile and fails if runner/sdk_invoker appears (red with the old import: 3 hits; green: 0).
1010
-
1011
- ## FC-TEN-COST-23 Decomposer emits a unit with an empty write set and unit mode refuses it (EMPTY-WRITESET-UNIT)
1012
- - User-visible vs raw: every multi-unit run of coord-1-api-web exited 1 with "unit launcher exit 1; 2 unit(s) did not pass" after about 50s, the spurious unit having failed with exit 2 in 1s and $0; the raw reason ("unit mode was requested but its spec file is invalid") never reached the user.
1013
- - Law broken: a decomposer output must be runnable by the stage that consumes it; unit_mode.ts parseText rejects writeSet.length === 0 and the engine refuses an unfenced run, so a file-less prose bullet ("`limit` must be an int ...") became a unit that could never start.
1014
- - Siblings: decompose.ts items with no matching file (also shared-file-only items); fix_units.ts units (always carry a write set); the u1 PARTIAL exit 1 in the same evidence is the separate no-test-runner seed cause (TEN-COST-20, TEN-COST-21), not this class.
1015
- - Mechanism: decompose() folds every item that names no write-set file into the first unit that owns files; if no item names a file all items form one unit and route.ts falls back to the sequential run (fewer than 2 units).
1016
- - Fixture: tests/engine10/decompose.test.ts "no unit has an empty write set" and "a spec where no item names a file collapses to one unit"; repro: LOKI_UNIT_SPEC with writeSet [] and bin/loki start gives rc=2 "spec file is invalid".
1017
-
1018
- ## FC-TEN-UX-01 First run in an empty folder dies on "not a git repository", and a logged-out Claude CLI fails only after spend starts (FIRST-RUN-DEAD-END)
1019
- - User-visible vs raw: a new user running `loki start "<brief>"` in a fresh folder got "not a git repository ... run loki from inside a git checkout" with no next step; a logged-out claude CLI surfaced later as a failed first iteration.
1020
- - Law broken: a first run must either work or name the exact command that fixes it, before any spend.
1021
- - Siblings: preflight.ts git identity check (stays fatal, names the git config command, never set automatically); doctor.ts login probe (same `claude auth status` parse, now shared); non-empty folder without a repo (stays fatal, now names `git init && git add . && git commit -m init`).
1022
- - Mechanism: util/first_run_repo.ts initialises only a truly empty folder (LOKI_AUTO_GIT_INIT=0 refuses); preflight reuses doctor claudeAuthStatusLoggedIn: loggedIn false is fatal, an unknown answer is a warning only.
1023
- - Fixture: tests/engine10/preflight.test.ts "first run (TEN-UX-01)" cases (empty folder, one-file folder, LOKI_AUTO_GIT_INIT=0, fake claude loggedIn false / unknown / API key set).
1024
- - Recurrence (2026-10-17, TEN-UX-01-F1): the mechanism was wired only into preflight.ts, but supervisor.ts exits 2 on `git rev-parse --show-toplevel` before preflight runs, so `bash scripts/first-run-gate.sh --stub --entry start-empty` still printed "engine10: not inside a git repository" (TEN-UX-08 TL review). Fix: the supervisor repo lookup calls the same initEmptyRepo and prints its notice or its named-fix fatal; fixture is that first-run gate entry plus a supervisor-level test that runs the real start path in an empty folder, not preflight in isolation.
1025
-
1026
- ## FC-TEN-P9-FIX The first-run login probe spawned the provider CLI holding the GitHub token (P9-PREFLIGHT-LEAK)
1027
- - User-visible vs raw: moat P9.injection-cannot-reach-token went red on v11.17.22 (canary GH_TOKEN/GITHUB_TOKEN, hosts.yml, credential.helper and SSH_AUTH_SOCK reached the provider stub in every scenario); the raw cause was preflight running `claude auth status` from the supervisor with its full env.
1028
- - Law broken: every spawn of a provider CLI gets the withheld env (moat P9); the supervisor env is never handed to an agent binary, probe or not.
1029
- - Siblings: doctor.ts provider probe at the json route (already tokenFreeEnv); modernize/oracle/search.ts `--version` probe (already tokenFreeEnv); engine10 session spawns (worker env, assertWorkerEnv).
1030
- - Mechanism: doctor.ts claudeAuthStatusLoggedIn, the one shared login probe, spawns with tokenFreeEnv plus GIT_SSH_COMMAND=false unless LOKI_ALLOW_AGENT_GITHUB_TOKEN=1.
1031
- - Fixture: tests/engine10/preflight.test.ts "the login probe spawns the provider CLI without the GitHub token or SSH agent"; tests/moat/p9-rule-of-two.sh against a dist rebuilt from the tree.
1032
-
1033
- ## FC-TEN-SEC-PREPUSH-MERGE-WARN Pre-push warns about .gitleaksignore lines a merge did not add (MERGE-SECOND-PARENT-DIFF)
1034
- - User-visible vs raw: the v11.17.25 push printed 5 false ".gitleaksignore gained line(s)" warnings for merges c6d3894f1..0a16434e1 while the net .gitleaksignore diff of the push was empty; the raw cause was `diff-tree -m`, which also diffs a merge against its second parent (the older slice branch), so every line main gained since the fork looked added by the merge.
1035
- - Law broken: a warning names what a commit changed relative to what it was merged onto; it never reports lines the base already had.
1036
- - Siblings: the .gitleaks.toml name-only and patch diff-tree calls in .githooks/pre-push (same -m, fixed here; they could even FAIL the push on a toml main already had); the eval-fixture scan diff-tree -m in the same hook (kept: scanning both parents is conservative and fail-closed); scripts/security-audit-gitleaks.sh (comment only) and scripts/train-cycle.sh (plain non-merge name list).
1037
- - Mechanism: the per-commit drift check in .githooks/pre-push passes `--diff-merges=first-parent` instead of `-m`; a merge that adds a line itself still differs from its first parent, and each branch commit is its own entry in the pushed list.
1038
- - Fixture: tests/test-pre-push-gitleaks.sh case 19b (older-slice merge prints no warning; merge adding a line still warns); red with `-m` restored on all three calls, and red with the flag dropped from the ignore call.
1039
-
1040
- ## FC-TEN-STABILITY-09 Engine10 stub claude CLIs do not answer `claude auth status`, so the login probe logs a stage or stalls (STUB-AUTH-STATUS-SILENT)
1041
- - User-visible vs raw: raw (test infrastructure). main Tests failed on 11 of the last 13 runs after TEN-UX-01 added the preflight login probe: stub runs printed the extra "could not confirm the Claude login" line (A-130 8-line quiet cap, 9 seen), the issue-ref stub logged the probe as an "other" stage, and the SIGINT stub treated the probe as the implement session (slept 5s, never recorded cost).
1042
- - Law broken: a stub provider must honour every CLI call the engine makes before a session, not only `-p`; a probe the stub does not answer is indistinguishable from a real unknown login.
1043
- - Siblings: tests/engine10/fixtures/e2e/bin/claude, fixtures/issue-ref/bin/claude, inline stubs in supervisor_signal.test.ts and budget_stop_e2e.test.ts (all fixed). tests/engine10/fixtures/providers/bin/* are non-claude providers and are not probed. Two verify_lint.test.ts lint:tsc cases fail only in a worktree with no loki-ts/node_modules (environmental, not this class).
1044
- - Mechanism: every stub claude answers `auth status` with {"loggedIn":true}, no side effects.
1045
- - Fixture: tests/engine10/e2e.test.ts "every engine10 stub claude answers claude auth status as logged in".
1046
-
1047
- ## FC-TEN-STABILITY-15 queue add on a corrupt queue file silently overwrites every queued item (QUEUE-CORRUPT-OVERWRITE)
1048
- - User-visible vs raw: user-visible. A truncated .loki/issue-queue.json read as an empty queue, so `loki queue add` wrote a file holding only the new item and the overnight backlog vanished with no message.
1049
- - Law broken: a reader that maps a parse failure to "empty" must not be followed by a writer of the same file; unreadable state is quarantined and reported, never replaced.
1050
- - Siblings: util/outbox.ts runOnce (TEN-STABILITY-12, already quarantines); queue run reads the queue and returns "queue is empty" without writing, so it cannot overwrite; its end-of-run writeQueue only follows a successful read.
1051
- - Mechanism: queueAdd calls quarantineCorruptQueue, which renames the bad file to issue-queue.json.corrupt-<ts> and warns once on stderr before the read.
1052
- - Fixture: loki-ts/tests/commands/queue.test.ts "add on a corrupt queue file quarantines it".
1053
-
1054
- ## FC-TEN-STABILITY-18a Test pins a literal that an intentional change moved, and the pin is not updated in the same slice (STALE-PIN-AFTER-INTENT)
1055
- - User-visible vs raw: raw (test infrastructure). Nightly 39016259458 stayed red on main e32ee1b5d: pr_body_golden producer registry (util/check_result.ts moved the gate line there, TEN-STABILITY-13), sizing wall brief cap (710 measured vs 600; TEN-COST-14 output budget plus TEN-QUALITY-14 negative-case sentence), router_optout_golden (TEN-COST-11 haiku-first Wall), test-ui-bare-loki (TEN-UX-10 next-step lines), test-local-ci-gitleaks (TEN-SEC-GITLEAKS-01-F1 first-parent log-opts).
1056
- - Law broken: a slice that changes an asserted surface owns the guards that assert it; green on the slice branch is not green on main when the change landed after the pin.
1057
- - Siblings: the five pins above (all fixed). The sizing cap now records its measured number; the router golden pins LOKI_E10_WALL_FIRST=0 so it isolates router behavior (wall.test.ts owns haiku-first).
1058
- - Mechanism: each guard names the owning change in a comment and asserts the intended new shape (first line plus next-step lines; the exact log-opts string), never a weaker match.
1059
- - Fixture: tests/test-ui-bare-loki.sh "the URL is followed by the start and stop next-step lines"; tests/test-local-ci-gitleaks.sh exact log-opts grep and live invocation mirror.
1060
-
1061
- ## FC-TEN-STABILITY-18b Bun.which reads the PATH captured at process start, so a test or caller that mutates process.env.PATH is ignored (BUN-WHICH-STALE-PATH)
1062
- - User-visible vs raw: raw (CI only). verify.test.ts TEN-QUALITY-11 cases passed on a machine with uv installed and failed in CI without it: resolveTool asked Bun.which("uv") and never saw the fake uv prepended to process.env.PATH.
1063
- - Law broken: tool resolution must use the same PATH the child process will receive.
1064
- - Siblings: verify.ts resolveTool (fixed, both uv and fallback lookups); provider_failover.ts defaultInstalled, deep.ts:104 and modernize/lang/java.ts:159 call Bun.which without the live PATH when no path option is given (named, not changed: production PATH is fixed at start, no test mutates it).
1065
- - Mechanism: resolveTool's onPath helper passes {PATH: process.env.PATH}.
1066
- - Fixture: verify.test.ts TEN-QUALITY-11 cases, run with PATH=/usr/bin:/bin plus the bun directory (no uv): red before the fix (2 fail), green after.
1067
-
1068
- ## FC-TEN-STABILITY-18c A verify flag exists in the engine10 verifier and completions but the dispatcher rejects it and help omits it (VERIFY-FLAG-NOT-ROUTED)
1069
- - User-visible vs raw: user-visible. `loki verify --require-exogenous` (the CHANGELOG form) printed "[verify][error] unknown option" from the legacy verifier, and `loki verify --help` did not list the flag.
1070
- - Law broken: a documented flag must reach the implementation that owns it, and its help must list it.
1071
- - Siblings: --allow-unsigned and --export-dsse also lack a flag-first route in bin/loki (they work after a run id or receipt path); named, not changed here.
1072
- - Mechanism: bin/loki and autonomy/loki cmd_verify route --require-exogenous to engine10 like --pubkey; autonomy/verify.sh help lists it.
1073
- - Fixture: tests/test-help-changelog-drift.sh (flag in help); `bash bin/loki verify --require-exogenous` now reaches engine10 ("no runs found") instead of the unknown-option error.
1074
-
1075
- ## FC-TEN-STABILITY-18d The legacy echo-into-grep-q ratchet is breached by new tests and the ceiling is the only guard (SIGPIPE-RATCHET-BREACH)
1076
- - User-visible vs raw: raw (test infrastructure). tests/test-sigpipe-guard.sh counted 1737 legacy forms against a ceiling of 1728.
1077
- - Law broken: the ceiling is lowered when a suite is converted, never raised (the guard's own header).
1078
- - Siblings: 15 forms converted in test-next-resume-agree.sh (7), test-security-audit-config.sh (6) and test-doctor-ux.sh (2); the ceiling now equals the new count, 1722. Large holders remain (test-v10-pulse.sh 321, test-deploy-receipt-gate.sh 38).
1079
- - Mechanism: grep -q PATTERN <<<"$var" here-string form.
1080
- - Fixture: tests/test-sigpipe-guard.sh; SIGPIPE_GUARD_CEILING=1721 turns it red (count 1722 exceeds ceiling 1721).
1081
-
1082
- ## FC-TEN-STABILITY-19a A headline policy change silently disarmed a moat positive control (MOAT-CONTROL-POLICY-DRIFT)
1083
- - User-visible vs raw: raw (moat suite). `bash tests/moat/run.sh` printed "moat suite: FAIL (1 rule failure(s))": P8.ablation-dead-code-not-sealed got "positive control: load-bearing fixture headline=VERIFIED WITH GAPS, expected VERIFIED". The raw cause was TEN-GATES-10 (c6796b75b, D104): VERIFIED now needs one agent-independent fact beside the agent's suite, and the P8 fixture recorded only the suite (build marked not applicable), so every P8 fixture got an exogenous_evidence gap. The dead-code assertion "headline is not VERIFIED" then held for the wrong reason: removing the not_load_bearing rule from _compute_headline no longer turned it red.
1084
- - Law broken: a negative assertion is evidence only while its positive control reaches the green state under the CURRENT headline rules; a headline rule change must re-green every moat control it touches.
1085
- - Siblings: tests/test-proof-ablation-headline.sh (passes; it pins facts.ablation, not the headline); tests/test_proof_generator.py (updated by TEN-GATES-10 itself); P1-P7 and P9 were green on the same run, so no other moat control reads a suite-only VERIFIED.
1086
- - Mechanism: the P8 fixture records a real build fact (byte-compiling every source file, exit code recorded, no .pyc written) so the control meets D104; the moat rule and the generator are unchanged.
1087
- - Fixture: tests/moat/p8-load-bearing-proof.sh. Red with the build exit code forced to 1 (control headline=NOT VERIFIED), and red with the generator's not_load_bearing check mutated ("headline is VERIFIED although the checks pass with the change no-oped"), which was undetectable before this fix.
1088
-
1089
- ## FC-TEN-STABILITY-19b The first-run gate stub treated the login probe as a work session (STUB-AUTH-STATUS-SILENT recurrence)
1090
- - User-visible vs raw: raw (release gate). `bash scripts/first-run-gate.sh --stub` failed 7 assertions, all from "Reason: dirty tracked tree: M sum.js" at intake.ts:61, plus a python FileNotFoundError traceback from the cost-non-null probe. The engine preflight (TEN-UX-01, 08a8ae052) runs `claude auth status` in the repo before Intake; the gate's heredoc stub short-circuited only --help/--version, so the probe applied the sum.js fix and Intake refused the dirty tree. The real CLI's auth status is read-only, so users never saw this.
1091
- - Law broken: same as FC-TEN-STABILITY-09: a stub provider must honour every CLI call the engine makes before a session.
1092
- - Siblings: FC-TEN-STABILITY-09 fixed the loki-ts/tests stubs but not scripts/first-run-gate.sh (fixed here). scripts/b9-scoreboard.sh already answers auth. tests/test-quick-config-safety.sh, test-quick-receipt-order.sh, test-quick-artifacts.sh, test-json-schemas.sh and test-usage-governor.sh all pass on this base (their stubs do not edit the tree on the probe).
1093
- - Mechanism: the gate stub answers `auth status` with {"loggedIn": true}, no side effects; the cost-non-null probe reports an unreadable run record as a FAIL line instead of a traceback.
1094
- - Fixture: loki-ts/tests/engine10/e2e.test.ts "every engine10 stub claude answers claude auth status as logged in" now also pins the first-run gate stub (red with the stub line removed); the gate itself returns to 7 failures with that line removed.
1095
-
1096
- ## FC-TEN-STABILITY-20 A slice adds a CLI flag in the Bun route and CHANGELOG but not in the help-drift-checked help or completions (HELP-FLAG-DRIFT)
1097
- - User-visible vs raw: user-visible. `loki report kpis --since` and `loki undo --list` (v11.17.40) worked but were absent from `loki help` and Tab completion, so tests/test-help-changelog-drift.sh was red on 3 lines.
1098
- - Law broken: a documented flag must appear in the help output for its command and in completions/loki.bash.
1099
- - Siblings: swept the other newest-window invocations (proof list --json, proof share --dry-run, queue add, start --help, status --probe/--json); all already pass. completions/_loki carries no per-flag lists for report or undo; its undo description now names --list.
1100
- - Mechanism: autonomy/loki help text lists both flags; completions/loki.bash gains report and undo cases. The drift test already guards both.
1101
- - Fixture: tests/test-help-changelog-drift.sh (22 passed, 0 failed); removing the `undo --list` help line turns it red.
1102
-
1103
- ## FC-TEN-UX-26 Bash commands whose default case arm printed help and exited 0 on an unknown subcommand
1104
-
1105
- - User-visible vs raw: user-visible. `loki config frobnicate` and `loki api frobnicate` printed usage and returned 0, so scripts could not detect a typo (same class as `loki provider`, TEN-UX-25).
1106
- - Law broken: an unknown subcommand must fail non-zero with a one-line error; help stays reachable via help, --help and -h.
1107
- - Siblings: swept 24 bash commands with `<cmd> frobnicate`; only config and api (fixed here) returned 0, plus `loki voice`, a stub with no subcommands that prints a planned-feature notice (left as is).
1108
- - Mechanism: explicit help|--help|-h arm, default arm prints `loki <cmd>: unknown subcommand` to stderr and returns 2.
1109
- - Fixture: tests/test-help-discoverability.sh; restoring the old default arm turns the frobnicate assertions red.
1110
-
1111
- ## FC-TEN-STABILITY-50 A credentialed push/pr-ready child is spawned with spawnSync and no timeout, so one hung script blocks the run (UNBOUNDED-PUSH-CHILD)
1112
- - User-visible vs raw: user-visible. A hung engine10-push.sh froze the PR stage with no failure surfaced.
1113
- - Law broken: every child process is bounded and a timeout is a named stage failure.
1114
- - Sibling sweep: pr.ts push-pr, push-mr, pr-ready and status now share spawnBounded (LOKI_PR_PUSH_TIMEOUT_MS, default 50s). Still unbounded: src/util/pr_lessons.ts:348, src/runner/attempts.ts:376, src/engine10/supervisor.ts:374.
1115
- - One shared mechanism: spawnBounded and failReason in loki-ts/src/engine10/stages/pr.ts.
1116
- - Regression fixture: loki-ts/tests/engine10/pr.test.ts "hung push script".
1117
-
1118
- ## FC-TEN-SECURITY-26 The URL userinfo redact rule restarted inside one scheme-char run, so redaction was quadratic and the Bun suite went red on slow runners (URI-SCHEME-RESTART)
1119
- - User-visible vs raw: user-visible. `redact.test.ts` "assignment redact covers flag and separator-prefixed names" failed on GitHub runners (175-220ms against a 100ms budget, main v11.17.58 run 39048066486 and PR #225 run 39122713723). The slow input was `"--a".repeat(7000) + "token"`; the TEN-SECURITY-25 assignment rule took 0.1ms on it. The URL userinfo rule `\b[a-z][a-z0-9+.-]*://` restarted at every letter after - + or . and rescanned the run: 49, 212, 847ms at N, 2N, 4N.
1120
- - Law broken: redaction runs on every event, log line and proof field, so it must stay linear in input length; a quadratic rule lets one long token stall the engine writer, shipper or proof generator.
1121
- - Siblings: the same scheme shape in loki-ts/src/util/redact.ts, loki-ts/src/council/finding_schema.ts and autonomy/lib/proof_redact.py `_URI_CREDENTIAL`, all bounded in 01e81302c. A second, Python-only quadratic survived that fix: the password class `[^\s@]+@` scanned an "@"-free tail from every scheme start (`"a://" * 16000` took 5.6s with the bounded scheme). Left as is: the TEN-SECURITY-25 ASSIGNMENT rule's `(?:[A-Za-z0-9]+[_.-]+)*` group drops off the JSC regex JIT above about 80000 iterations (`"--a".repeat(80000)` 57ms, 320000 reps 393ms); still linear, roughly 0.5us per char past the cliff, far above the sizes the suite or real log lines use.
1122
- - Mechanism: scheme bounded to 64 chars in all three rules (01e81302c). For the Python password tail, `_redact_uri_credentials` runs `_URI_CREDENTIAL` only on each whitespace token up to its last "@"; no match can end past it. Differential fuzz against the plain regex: 0 diffs over 400k random strings, redaction count included.
1123
- - Fixture: loki-ts/tests/util/redact.test.ts and tests/test_proof_redaction.py `test_assignment_redaction_is_linear_on_long_input` (scheme inputs from 01e81302c; `"a://" * 50000` and `"a://u:" * 35000` added for the password tail, red on the bounded-only source, green after).
1124
- ## FC-ISSUE-FOREIGN-REPO Issue mode on a repo the user cannot push to ended in a raw 403, posted on someone else's issue, and sealed no Wall result (FOREIGN-REPO-NO-PROOF)
1125
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#21527` (a repo the operator cannot push to, no test runner) printed BLOCKED with NOT PROVEN carrying three raw lines of git stderr (`remote: Permission to anthropics/claude-code.git denied`, `fatal: ... 403`), a Reason cut mid-word at 200 chars, the intent card glued to the live line, and "wall result not recorded by verify". A no-PR rerun on the same repo sealed PARTIAL "no tests to run": the Wall wrote a bun:test file and verify never ran it.
1126
- - Law broken: a run must end in a sealed, honest result the operator can act on; a credential or permission miss must read as one line naming the cause, and Loki must not act on a third-party issue the operator does not maintain.
1127
- - Siblings: push-pr (direct push, no fork path), issue-comment status and backstop comments (posted on the foreign issue), verify (Wall tests only from the project test map; `wall_passed` never written anywhere, so every Wall receipt carried the not-recorded line), wall guessRunner (any non-py/go file mapped to the JS runner), output Reason (mid-word slice) and NOT PROVEN (newlines kept), intent card (stderr write over the supervisor's live line).
1128
- - Mechanism: engine10-push.sh fork mode (gh `permissions.push` false: push to the verified fork through `_loki_trusted_push`'s new destination arg, PR upstream from user:branch; a draft run stops at the fork, exit 4; status and backstop comments skipped, the receipt comment on the user's own PR kept; `LOKI_PR_FORK=0` off). testmap `harnessRunners`: no detected runner and no Python/Go test suite means Loki's bun runs the Wall file in the Wall and in verify (`LOKI_WALL_HARNESS_RUNNER=0` off). verify writes `wall_passed`. term_width `clipWords` and `pushErrLine` for one-line output.
1129
- - Fixture: tests/test-engine10-push.sh fork-mode block (6 cases, 5 red before), loki-ts/tests/engine10/verify.test.ts "no project runner", testmap.test.ts harnessRunners, output.test.ts clip and one-line NOT PROVEN, pr.test.ts fork-only and pushErrLine. Live: the same task on a clone of anthropics/claude-code went from verify 0s with no checks to the Wall check executed and failing for the stated reason (BLOCKED, the request lives in the Slack backend, not that repo).
1130
-
1131
- ## FC-NO-DIFF-PUBLISH A run with nothing to publish still pushed a branch, and its stop reason read as glued prose (NO-DIFF-PUBLISH)
1132
- - User-visible vs raw: user-visible. A rerun of `loki start anthropics/claude-code#21527` after FC-ISSUE-FOREIGN-REPO ended BLOCKED (SPEC_CONFLICT, 0 files changed) yet pushed an empty branch to the operator's fork (NOT PROVEN: "PR not opened: fork-only: pushed ..."), logged the skipped status comment as ok, spent two more fix rounds (one on opus) after the implement session had already declared the conflict, and printed a Reason with two model messages joined mid-word ("has shippedI still").
1133
- - Law broken: a run publishes only what it changed, and its result reads as one honest line; spend stops once the outcome is decided.
1134
- - Siblings: the supervisor PR gate (net diff computed only for FAILED, so SPEC_CONFLICT, PARTIAL and VERIFIED skipped it), the status projector and backstop comment (a skip read as a post), session marker parsing (LOKI_SPEC_CONFLICT and LOKI_ALREADY_DONE both line-anchored on glued text), the fix loop (E-98b keeps verifying a single conflict claim, but a fix session confirming it with an unchanged tree was not treated as confirmation).
1135
- - Mechanism: one net-diff check for every verdict gates every push; the push script reports a skipped comment on stdout and callers record it; session text blocks are newline-separated before marker parsing; a fix round that also exits spec_conflict on an unchanged tree ended the loop (superseded by FC-FIX-OVER-CONFLICT: no fix round runs at all after implement declares the conflict).
1136
- - Fixture: supervisor no-diff SPEC_CONFLICT case (no push), tests/test-engine10-push.sh skipped-comment line, session two-block marker case, machine confirmed-conflict case.
1137
-
1138
- ## FC-LOKI-DIR-DIRT A repo that committed Loki's own .loki/ state could never start a run once that state was removed (LOKI-DIR-DIRT)
1139
- - User-visible vs raw: user-visible. `loki start asklokesh/FireLater#20` ended FAILED in 1s with "dirty tracked tree: D .loki/CONTINUITY.md, D .loki/STATUS.txt, ...": the repo tracks four legacy-engine state files, the operator had cleared .loki/ by hand (shell history), and every later run refused to start (the same failure hit #17 on 2026-10-04).
1140
- - Law broken: Loki's own state is never the operator's change; a run is blocked only by the operator's files.
1141
- - Siblings: intake's dirty check (the only consumer of tracked-tree status before the run branch); every later stage already excludes .loki (the diff pathspec ':(exclude).loki', info/exclude, commit staging), so only intake disagreed.
1142
- - Mechanism: intake's dirtyTrackedFiles drops any status line whose paths are all under .loki/, rename sides included; the commit stage keeps excluding them, so the run never commits or reverts those deletions.
1143
- - Fixture: loki-ts/tests/engine10/intake.test.ts FC-LOKI-DIR-DIRT (red: the exact reported reason; green: intake completes and the run's commit names only README.md).
1144
-
1145
- ## FC-VACUOUS-WALL-GREEN A Wall test that already passed on base was sealed as ALREADY_SATISFIED for a feature the repo does not have (VACUOUS-WALL-GREEN)
1146
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#21527` (GitLab support for a hosted Slack integration) on 11.17.61 ended ALREADY_SATISFIED for $0.09 and posted "already satisfied" on the upstream issue. The Wall test it sealed asserted only things the base tree already had (5/5 pass on base, run a97b events wall base_run pass 1 fail 0), so a green-on-base result became the claim itself.
1147
- - Law broken: ALREADY_SATISFIED needs evidence the requested behavior exists; a test that passes before any change proves only that the test is weak.
1148
- - Siblings: seal verdictOf (green-on-base alone returned ALREADY_SATISFIED), discard alreadySatisfied (same three-way OR), the Wall installer (set already_satisfied on intake output, short-circuiting implement), runWall reauthor (kept: a green-on-base test is still reauthored once).
1149
- - Mechanism: green on base is recorded as `green_on_base` proof only; the claim comes from exactly two sources, intake's evidence-cited already-done check or implement's LOKI_ALREADY_DONE, and seal still requires proof for either. One rule in seal and discard.
1150
- - Fixture: loki-ts/tests/engine10/wall.test.ts green-on-base cases (red: ALREADY_SATISFIED with no claim; green: implement runs) and check_result.test.ts "verdict gate" FC-VACUOUS-WALL-GREEN case (a changed tree on a Wall green on base caps at PARTIAL with NOT PROVEN "Wall green on base: no Wall test discriminates the change"; the Wall proves only an intake or implement claim).
1151
-
1152
- ## FC-FIX-OVER-CONFLICT A fix round papered over a declared spec conflict with documentation for a behavior the code does not have (FIX-OVER-CONFLICT)
1153
- - User-visible vs raw: user-visible. On 11.17.62 the same #21527 run had implement declare LOKI_SPEC_CONFLICT twice (the feature lives in a hosted service, not that repo); verify then failed, fix round 1 wrote a CHANGELOG line and a README section announcing GitLab support, verify passed, and the commit was pushed to the operator's fork (run fdfc, commit 980a166) under a SPEC_CONFLICT seal.
1154
- - Law broken: once implement declares the task conflicts with the repository, spend stops and nothing may be written to satisfy the check; docs never substitute for missing code.
1155
- - Siblings: the machine fix loop (ran on any failing verify regardless of implement.exit), the 11.17.61 confirmation rule in the loop (required a second conflict claim from fix; removed), fix stage output spec_conflict (only consumer was that rule; removed), the implement/fix briefs (context.ts FIXED_RULES) and the shared stage prefix (lean_prefix.ts) that plan, wall and implement read.
1156
- - Mechanism: the fix loop condition stops when implement.exit is spec_conflict (verify still runs once, E-98b); FIXED_RULES and the stage prefix forbid docs, changelog or README text announcing behavior the code lacks and direct the agent to LOKI_SPEC_CONFLICT. The A-110/E-67 draft PR for a conflict with real implement work is kept.
1157
- - Fixture: loki-ts/tests/engine10/machine.test.ts E-98b case (one verify, no fix round; red on the old loop), implement brief and spec-first goldens re-captured.
1158
-
1159
- ## FC-FIX-CONFLICT-STALLED A spec conflict declared by a fix round was ignored and the run ended STALLED (FIX-CONFLICT-STALLED)
1160
- - User-visible vs raw: user-visible. `loki start asklokesh/FireLater#20` (run e10-20261019T020704Z-103b): fix round 2 ended with LOKI_SPEC_CONFLICT and no tree change; the fix stage dropped the marker, the anti-loop fired (loop.repeat) and the run ended STALLED "same failure 3 times" instead of BLOCKED with the agent's question and the `loki answer` next step.
1161
- - Law broken: an agent's explicit, evidenced report that the task conflicts with the repository ends the run as a spec conflict wherever it is made; a stall verdict never hides the question.
1162
- - Siblings: implement (the only stage that consumed the marker), the fix stage (dropped it), seal verdictOf and the receipt reason (read implement only), reasonOf and the supervisor blocked comment (read the implement event only). plan, wall and deep sessions are not told the marker exists and never consume it; swept, no change. A claim from a fix round that changed the tree is not believed (the agent did work; verify decides), so a conflict cannot escape a real failure.
1163
- - Mechanism: one rule shared with implement (util/conflict_resume.ts): a fix session that signals the conflict gets the same single correction resume (resumeAfterConflict) and, if it still signals it with the tree unchanged, the fix stage reports exit spec_conflict with the reason; the machine records it on the implement output so seal, output and the supervisor use their existing SPEC_CONFLICT path, and the fix loop stops before the anti-loop.
1164
- - Readers: conflictReasonOf (engine10/events.ts) is the one event reader (last fix reason, else implement's); output.ts, supervisor.ts, run_outputs.ts workItemBlockers, blocked_answer.ts readBlocked and slack_inbound.ts all use it. In deep mode implementLeftS may skip the correction resume; the unchanged-tree check still applies.
1165
- - Fixture: loki-ts/tests/engine10/machine.test.ts (red: stopped "stalled" and verdict not SPEC_CONFLICT; green: BLOCKED path with reason; tree-changed claim is not believed).
1166
-
1167
- ## FC-LINT-OOM Tests shard 0 died with "runner has received a shutdown signal" (exit 143) during ShellCheck Linting (LINT-OOM)
1168
- - User-visible vs raw: user-visible (red Tests on main for 11.17.61 and 11.17.63). Raw: no suite failed; the hosted runner was killed mid-lint.
1169
- - Law broken: a gate must fit its machine; resource exhaustion read as a red verdict hides real results.
1170
- - Siblings: the ratchet pass (autonomy/loki, started in the background first) and the per-file pool in tests/run-shellcheck.sh; both could hold a multi-GB shellcheck at once. Measured RSS: autonomy/loki 8.4 GB, autonomy/run.sh 9.8 GB, 16 GB runner.
1171
- - Mechanism: one scheduling rule in tests/run-shellcheck.sh: a file of at least LOKI_SHELLCHECK_HEAVY_LINES (default 5000) lines runs alone (pool drained first, nothing starts until it ends); the ratchet runs serially. Verdict logic unchanged (598 pass, 0 fail locally, 1m45s).
1172
- - Fixture: tests/test-shellcheck-heavy-serial.sh (fake shellcheck records overlap; red on the previous script, green now).
1173
-
1174
- ## FC-TEST-LEAKS-INTO-REPO Running the engine10 tests left an untracked loki-ts/calc.ts in the checkout (TEST-LEAKS-INTO-REPO)
1175
- - User-visible vs raw: raw (a dirty tree after any full `bun test tests/engine10`; seen in main and in engineers' worktrees). No user run is affected.
1176
- - Law broken: a test must not write outside its temp repo; a probe must not run in the caller checkout.
1177
- - Writer: preflight's codex login probe (providerLoginIssue, `codex login status`) spawned the stub codex CLI in the process cwd (loki-ts); the stub ignored its args and appended multiply() to calc.ts. Only tests/engine10/providers.test.ts reaches it. Not a session write: sessions already run in ctx.repoDir.
1178
- - Siblings: the cline and aider stubs have no probe today (they only warn on login); the e2e claude stub answers `--help` and `auth status` before touching files.
1179
- - Mechanism: the codex stub exits 0 for `login ...` before any edit, and providerLoginIssue runs the probe in tmpdir() so no probe ever executes in the caller checkout.
1180
- - Fixture: loki-ts/tests/engine10/providers.test.ts snapshots untracked files under loki-ts before and after each provider run and expects none new (red on the old stub, green now).
1181
-
1182
- ## FC-STACKED-ON-LOKI-RUN A new run started on an earlier unaccepted Loki run branch inherits that run's output (FC-15 family)
1183
- - User saw: `loki start anthropics/claude-code#21527` in a checkout still on loki/e10-20261018T205304Z-fdfc, the output of an earlier run sealed BLOCKED (spec conflict). The start line said `PR target: origin/main, base: loki/e10-20261018T205304Z-fdfc`; the new run branched from that HEAD, so its branch, its HANDOFF Accept line and any PR carried the earlier run's rejected commit (980a166, fabricated CHANGELOG and README lines). The acceptance "CHANGELOG.md stays unmodified" was vacuously true because the fabrication sat in the base. Raw: the raw arm starts from the user's tree and has no such commit.
1184
- - Law: L4 (the base is a contract), L2 (trust fails closed: unreviewed, rejected output is not a base), L5 (an honest line names what was skipped).
1185
- - Cause: intake.ts took base_sha, tree and the new branch from HEAD unconditionally. FC-15 only voided an ALREADY_SATISFIED claim resting on such commits; it never stopped the run building on them.
1186
- - Rule: when every commit in <PR target>..HEAD carries the Loki-Run trailer, the run branches from the PR target tip instead (the target tip rather than the merge-base, so a squash-merged run still finds its work on the target). The old branch is untouched. One line names the skipped branch and commit count; it is recorded as intake data stacked_loki_skip and shown through noteOf in the summary and PR body. Any commit without the trailer (the operator built on it deliberately), an empty range or an unresolved target keeps base = HEAD.
1187
- - Siblings swept (known follow-up: group runs, where the group baseRef stays HEAD while its units skip; the predicate never fires off a loki/e10-* run branch, so not on main after a fast-forward accept, not on loki/epic-*, loki/group-*, loki/unit-* branches, and not when LOKI_PR_BASE is set by a stacked slice of `loki issues run`): unmergedEvidence/lokiOwn (still reports the mixed Loki plus operator range); the supervisor start line (baseLine now calls the same predicate, so it agrees with intake); ctx.baseSha (set before intake from HEAD; intake now updates it, which the Wall base run, verify, seal and the PR stage read); base_sha, the tree cache key and the contract snapshot (all taken after the checkout).
1188
- - Mechanism: one predicate `stackedOnLokiRun` in loki-ts/src/util/base_guard.ts, used by intake.ts and baseLine. No new knob.
1189
- - Fixture: loki-ts/tests/engine10/intake.test.ts describe "FC-STACKED-ON-LOKI-RUN" (all-Loki range branches from origin/main with the old branch intact and the note recorded; an operator commit in the range keeps HEAD; an unresolved target keeps HEAD) plus the FC-15 case D updated to the skip.
1190
-
1191
- ## FC-LAZY-CONTROL-ENV Lazy-fetch control silenced by ambient git env or a racily clean index (GIT-ENV-LEAK, sibling of FC-25c-ci)
1192
- - User-visible vs raw: Tests run 39174561642 (main 8cc81a1b8) failed shard 6/8 on tests/test-untracked-status-fsmonitor.sh with "control: plain git status did not run the lazy-fetch hook MARKER=no"; the same file passed an hour earlier and 5 of 5 locally.
1193
- - Law broken: a test must be hermetic; a control that depends on inherited process env is not a control.
1194
- - Cause (reproduced 2026-10-18, git 2.55.0): GIT_SSH_COMMAND (autonomy/run.sh:5868 exports GIT_SSH_COMMAND=false) or GIT_NO_LAZY_FETCH in the inherited env makes the plain `git status` skip core.sshCommand, so the hook marker is never written. `env GIT_SSH_COMMAND=false bash tests/test-untracked-status-fsmonitor.sh` and `env GIT_NO_LAZY_FETCH=1 ...` each fail the control exactly as in CI. A second cause remained in a clean env: 1 of 20 runs failed the control because `git status` re-hashes f.txt (and so lazy-fetches the attributes blob) only when the index entry is racily clean; otherwise stat matches and no fetch happens.
1195
- - Siblings: both lazy-fetch fixtures in the file (run_lazy_case and the filter-case repo); any test asserting a repo-local ssh, fsmonitor or filter command runs.
1196
- - Mechanism: both fixtures unset GIT_SSH, GIT_SSH_COMMAND, GIT_NO_LAZY_FETCH, GIT_CONFIG_COUNT, GIT_CONFIG_PARAMETERS, GIT_DIR, GIT_WORK_TREE and set GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null, and run_lazy_case sets f.txt mtime to 2030 (as run_eq_case already does) so status always re-hashes. The control runs first; a dead control is a FAIL that marks the real leg NOT PROVEN, never a skip or pass. The real assertion is unchanged.
1197
- - Fixture: the same file passes 21/0 on 40 consecutive clean runs (was 19/1 once in 20) and with GIT_SSH_COMMAND=false GIT_NO_LAZY_FETCH=1 set.
1198
-
1199
- ## FC-BASELINE-PER-PACKAGE A full suite already red on base was blocked as a regression because the baseline ran a different command (BASELINE-PER-PACKAGE)
1200
- - User-visible vs raw: user-visible. `loki start asklokesh/FireLater#20` (run e10-20261018T205157Z-678e) ended FAILED with "full suite: frontend failed" after $2.60: verify ran one full suite per Project Model package, `(cd frontend && npm test)`, while the intake baseline had run only the repo's first test command at the root, so attributeFullSuite found no baseline for that command and excused nothing; vitest's per-test cross line also carried its duration ("5ms"), so even a matching command would give different ids on base and head.
1201
- - Law broken: a failure the change did not cause is never charged to the change; base and head are measured with the same command and the same id parser.
1202
- - Siblings: the legacy per-runner full suite (FULL_SUITE_CMD, `npm test --silent` vs the profile's `npm test`), the multi-root package suites (runPackageSuites), every runner whose failure line carries a timing suffix (failIds), and the REPOMEM reuse path (a new HEAD drops the per-command entries, so they are re-recorded).
1203
- - Mechanism: package_suite.ts fullSuiteSpecs is the one list of full-suite commands; verify runs it and intake's baselineSuites records a baseline per command in its own cwd (shared budget, a suite past it is recorded not run); attributeFullSuite matches each failing check to its own command's baseline and stays fail-closed (no baseline for a command, an unattributable one, or no known ids: nothing excused); failIds strips a trailing duration ("5ms", "1.2s") from FAIL and cross lines.
1204
- - Fixture: loki-ts/tests/runner/harness_intelligence.test.ts "baselineSuites records one entry per full-suite command" and "attributeFullSuite matches each package check to its own suite baseline"; loki-ts/tests/engine10/verify_prered.test.ts FC-BASELINE-PER-PACKAGE vitest duration case.
1205
-
1206
- ## FC-DEPS-NOT-INSTALLED A fresh clone ran its tests with no dependencies installed, and the runner's "missing packages" error read as the code's failure (DEPS-NOT-INSTALLED)
1207
- - User-visible vs raw: raw. In the same FireLater#20 run every package declared an install command (`npm ci`, citing its package-lock.json) but nothing was installed, so the Wall base run and the baseline ran against an empty dependency tree and npx printed "npm error npx canceled due to missing packages and no YES option"; that line matched no load pattern, so it was a test failure owned by the code and spent fix rounds.
1208
- - Law broken: the harness prepares the environment the repo declares before it measures anything; an environment failure is the harness's, never a fix round.
1209
- - Siblings: the FC-22b install library (prepareDeps) existed but was never wired; the runner load classifier (npm and npx missing packages, POSIX sh "N: tool: not found" next to the bash "command not found" already covered); every ecosystem whose install command the Project Model cites (decided by git, not by a list of dependency directory names).
1210
- - Mechanism: install.ts absentDepRoots names a model package whose install command cites an existing manifest or lockfile in its root while git shows no untracked or ignored directory there (Loki's own .loki/ excluded); intakeDeps runs prepareDeps for those roots once per run, after the profile's first main baseline (measured on the bare clone) but before the Wall base run; after any successful install the main baseline and every per-command suite baseline are re-run, so none is left measured without dependencies; whatever the install created untracked (node_modules without a gitignore, lockfiles, .venv) is recorded in intake as install_created (a content hash per file, created directories) and never written to git config or exclude files, so an untouched install artifact stays out of verify changedFiles and the commit while an agent-edited lockfile still counts as a change; it emits deps.install per package, and a failed install becomes deps_not_proven, which verify carries into not_proven while the run goes on; the intake stage limit grows by the install and suite-baseline caps; runner_errors.ts LOAD_PATTERNS classify both missing-package forms as a harness-owned load error, anchored at the start of a line so test output quoting them stays a test failure.
1211
- - Residual risk: source shipped inside an install-created directory (for example a new build/ or vendor/) is not committed, and a symlink to a directory is not recorded.
1212
- - Fixture: loki-ts/tests/engine10/fc22_install.test.ts "absentDepRoots and intakeDeps (FC-DEPS-NOT-INSTALLED)"; loki-ts/tests/engine10/runner_errors.test.ts FC-DEPS-NOT-INSTALLED case.
1213
- - Budget change: the intake stage limit is now STAGE_BUDGETS.intake.limitS + INSTALL_CAP_MS/1000 + SUITE_BASELINE_BUDGET_S (120s), so a repo that needs an install or a per-command baseline gets that extra time at intake; a repo with nothing absent and no suites spends none of it.
1214
- - Hardening (D106, founder 2026-10-19): the intake install is the lockfile-frozen form when a lockfile exists (install.ts lockedInstall, one table for npm, pnpm, yarn, bun, uv, poetry, pipenv, cargo, bundler, composer), fails closed as harness-owned NOT PROVEN with no mutating fallback, and records `locked`, `lock_note` and `model_cmd` on the check. Fixture: fc22_install.test.ts "lockedInstall (locked, reproducible install)".
1215
-
1216
-
1217
- ## FC-ZERO-ON-BASE An impacted test that is a deliberate skip on base too was listed as NOT PROVEN for the change (ZERO-ON-BASE)
1218
- - User-visible vs raw: user-visible. In FireLater#20 run e10-20261019T020704Z-103b verify selected `backend/tests/integration/request-approval-race.test.ts`, a deliberate `describe.skip` (needs a real DB), so it executed zero tests and the run carried "not run: vitest:backend/tests/integration/request-approval-race.test.ts" in NOT PROVEN although the change never touched it and the pristine base executes zero tests there too.
1219
- - Law broken: a result the change did not cause is never charged to the change (the same base-comparison contract as A-112); zero-executed stays fail-closed whenever it could be about the change.
1220
- - Siblings: any runner whose file-level skip, todo or empty file yields "no tests executed" (vitest, jest, pytest, node:test, go); Wall, task-named (relevant), holdout and edited test files, and a run that edited test configuration, are never exempt; the package and full-suite not_run lines (a suite is a command, not a file) keep their own handling.
1221
- - Mechanism: verify.ts subtractBase (the A-112 pristine detached base worktree) reruns an untouched, non-Wall, non-relevant zero-executed check once and classifies it with the same classifyCheck; zero on base too puts the name in `skipped_on_base` (verify data, plus a `test.skipped_on_base` event "skipped on base: <check>"), and seal omits only those names from its "not run:" NOT PROVEN lines. Any other base outcome (tests ran, failed, missing, timed out) keeps NOT PROVEN. The rerun happens only in this case, and the check's installed node_modules or .venv are linked into the base worktree, never copied.
1222
- - Fixture: loki-ts/tests/engine10/verify_prered.test.ts describe "FC-ZERO-ON-BASE" (untouched skip listed; edited file, Wall file and tests-ran-on-base all stay fail-closed) and loki-ts/tests/engine10/seal.test.ts "FC-ZERO-ON-BASE".
1223
- - Hardening (HIGH review of 4e449d487): linking the head checkout's node_modules into the base worktree lets a workspace link (node_modules/shared -> ../shared, pnpm/npm/yarn workspaces) or an editable install (.venv with pip -e) resolve to HEAD sources, so the base rerun could execute the run's own edits and report a false zero-on-base. Choice: the excusal applies only when the run changed nothing but test files (`changed.every(isTestFile)`) and no test configuration, so no head source, manifest or lockfile exists for any link to expose; deps stay linked because base has none of its own. A run that edits any source keeps zero-executed as NOT PROVEN. Residual: edits made inside a gitignored node_modules or .venv are not in `changed`. Seal also writes one informational line "skipped on base (excused): <check>" next to the retained pre red lines (never downgrades). Regression fixture: verify_prered.test.ts "workspace link" (red before the rule).
1224
- - Hardening 2 (HIGH re-review of 9a4267335, D99): a changed TEST file can also be reached through a workspace link (node_modules/shared/helper.test.js), so test-files-only is not enough. depsSeeHead (util/base_guard.ts) withholds the excusal whenever the dependency dirs the base rerun would link can see head sources: any top-level or @scope/* node_modules entry that is a symlink whose realpath is inside the repo, or a .venv holding a .pth, *.egg-link or direct_url.json that names the repo; an unreadable dependency dir counts as seeing head. This gate is in addition to the test-files-only gate. Fixture: verify_prered.test.ts "reviewer repro 2" (red before the guard).
1225
- - Hardening 3 (round 3 HIGH review, D99): depsSeeHead now walks node_modules and .venv recursively without following symlinks (so node_modules/foo/node_modules/shared -> ../../../shared is seen), inspects every symlink target, and withholds when a target is inside the repo but outside the dependency dir it lives in (an npm .bin link or a pnpm store link stays self-contained). The repo is compared as given and as realpath (/var vs /private/var, a symlinked repo dir). Editable-install files (.pth, .egg-link, direct_url.json with its file:// URL decoded by fileURLToPath so %20 matches, and __editable___*_finder.py) are checked for the repo path, each named path also by realpath. The walk is bounded at 50000 entries; exceeding it or any read error other than a missing dir withholds. The helper logic lives in util/base_guard.ts (zeroOnBase, depsSeeHead), outside the engine core cap. A pnpm workspace package links to a path inside the repo but outside node_modules, so workspace pnpm repos are withheld; that is fail-safe, and the excusal is effectively inert wherever workspace links or editable installs exist.
1226
- - Stated plainly: the motivating incident (FireLater#20, a run that edits source) stays NOT PROVEN by design. The excusal only fires for runs that changed test files alone with dependencies that cannot see head; Seal accuracy outranks delivered accuracy.
1227
- - Superseded 2026-10-19 (founder principle: a PARTIAL caused by the harness, not by missing proof, is a bug) by FC-REPO-SKIPPED-TEST-DOWNGRADES: source-editing runs are now eligible; the dependency-visibility gate (depsSeeHead) and the no-test-config-edit gate remain.
1228
-
1229
- ## FC-REPO-SKIPPED-TEST-DOWNGRADES A relevant test the repo itself skips (zero tests on base and head, file untouched) downgraded the verdict to PARTIAL
1230
- - User-visible vs raw: user-visible. FireLater#20 run e10-20261019T171919Z-cc1c sealed PARTIAL with every executed check green: verify selected `backend/tests/integration/request-approval-race.test.ts` (wholly `describe.skip`, needs a real DB, unchanged by the run), it executed zero tests (events.jsonl seq 60, result not_run, reason "no tests executed (ran 0 tests, empty or all skipped)"), and verdictOf returns PARTIAL for any check whose result is not pass. FC-ZERO-ON-BASE had already removed the "not run:" note for such a check but (a) never excused it from the verdict, (b) never fired for a task-named (relevant) test, and (c) never fired when the run edited any non-test file.
1231
- - Law broken: a result the change did not cause is never charged to the change (A-112 base-comparison contract); and a harness-caused gap is a bug, not a missing proof (founder principle 2026-10-19). Zero-executed stays fail-closed whenever it could be about the change.
1232
- - Siblings swept: every runner that maps "ran 0 tests" to not_run goes through classifyCheck (util/check_result.ts) and so through the one zeroOnBase base rerun: vitest, jest, pytest, node:test, bun, go test, cargo. Runners whose zero is not measurable on base (no runner output parsed, base cut or missing) never excuse. Package and full-suite not_run lines (a suite is a command, not a file) are unchanged and still downgrade.
1233
- - Mechanism (one, extended): verify.ts subtractBase candidate filter drops its `!rel.has` exclusion (a relevant, non-Wall, non-holdout, untouched not_run with the NO_TESTS_REASON is measured on the pristine base like any other), and `zeroOk` no longer requires the run to have changed test files only (it keeps: no test configuration edit; zeroOnBase still withholds whenever the linked dependency dirs can see head, depsSeeHead). Zero on base too puts the name in `skipped_on_base`. seal.ts then (1) leaves exactly those not_run checks out of the verdictOf input (as pre_red fails already are), (2) records each in the receipt with `repo_skipped: true` and result not_run (never dropped), (3) writes the non-blocking note "skipped by the repo on base and head: <check>". Guards kept in verdictOf: the run still needs an executed passing n>0 check (FC-16 proof) and, after a limit kill, targetProofOf (n>0 pass of a Wall or relevant test): a skipped target is never a proof. Wall and holdout checks are never candidates in verify (they are excluded from the zero-candidate filter); seal additionally refuses a skipped_on_base name that matches a Wall file path (equal to the path after the runner prefix, or ending with / plus it). It does not cross-check target_checks: a relevant (task-named) test is eligible by design, and a skipped one is never a proof because target_checks lists only passing checks.
1234
- - Intake zero baseline (replaces the manifest-text gate and the environment fingerprint, both bypassed by content-level edits: remove optdep and uninstall it, move the line between dependency blocks, delete node_modules/optdep/index.js keeping its package.json; the base rerun links HEAD's node_modules and .venv, so any environment edit the run makes is invisible to it): a zero check is excused ONLY when the same check was also zero at INTAKE, on the base commit in an environment the agent had not touched (REPOMEM pattern). Measured on the real FireLater#20 artifact: the intake baseline recorded only `npm test` failing ids (unattributable) and per-package full suites, never per-file results, so the check was not covered; intake now calls probeZeroChecks (verify.ts, one run of each detected test file, bounded by 300 files and 120 s, never throws) and records `zero_baseline: {measured, zero}`. verify excuses a name only if it is in `zero`; a check not measured there (over the bound, cut, missing, absent field) is withheld. The base rerun (zeroOnBase) stays as a second, stricter requirement. depsChangedBeyondAdditions, depsFingerprint and depsEnvKept were deleted: under the baseline rule they add no protection (a pre-change measurement is immune to later edits). Residual: a repo with more than 300 test files only excuses the probed ones. Fixtures: verify_prered.test.ts removed dependency, BYPASS1, BYPASS2, BYPASS3, check absent from the baseline, missing baseline (negatives), FireLater#20-shaped additions-only run, probeZeroChecks.
1235
- - Residual: a dependency that changes how an unchanged file registers tests would need a symlink into head (withheld) or a config edit (withheld). Base not measured (no base worktree, cut, missing) keeps today's behavior.
1236
- - Fixture: loki-ts/tests/engine10/verify_prered.test.ts describe "FC-REPO-SKIPPED-TEST-DOWNGRADES" (relevant skip with source edit is skipped_on_base; diff-touched file, Wall file, zero-on-head-but-ran-on-base, test-config edit stay fail-closed) and loki-ts/tests/engine10/seal.test.ts "FC-REPO-SKIPPED-TEST-DOWNGRADES" (verdict VERIFIED with receipt repo_skipped and note; unlisted not_run, no executed proof, Wall name and base-not-measured all still PARTIAL).
1237
- - Separate finding (no code change): bare "council" and "security scan" in the NOT PROVEN of that run are the high-tier deep items (check_result.ts deepGaps). They never enter verdictOf (seal adds them to notes after the verdict is computed). They did not run because the fast engine defers them to the detached post-PR deep verify (supervisor.ts, spawned only after pr.opened); this run had pr_url null (no pr.opened or deep.started event), so they could not run. Not a verdict cause here; tracked as an observation that a PARTIAL run never gets its deep verify.
1238
- ## FC-FULLSUITE-ANSI-IDS A repo whose full suite was already red on base ended FAILED because the head full-suite run parsed no failure ids from colored output (FULLSUITE-ANSI-IDS)
1239
- - User-visible vs raw: user-visible. FireLater#20 on loki 11.17.74: a frontend vitest suite with 307 known failing ids in `.loki/state/repo-profile.json` ended FAILED "full suite: frontend failed". Raw: the head run's output was colored, `failIds` returned [] (stripped, the same output yields 296 ids, all inside the base set), and attributeFullSuite treats ids=[] as "introduced (blocking)". The fix brief also carried only the command, so the fixer saw no failure text.
1240
- - Law: a parser reads what the tool said, not how it was painted (FC-69); a user's environment must not change a verdict; unknown never excuses, but the reason it is unknown must be named.
1241
- - Cause (measured): src/project_model/package_suite.ts runPackageSuites and the legacy loop in engine10/stages/deep.ts runFullSuite ran the suite with plain process.env (no plainTestEnv) and passed raw output to failIds without stripAnsi. The baseline writer (runner/repo_profile.ts shellRunner) already used both, so base had ids and head had none. Unswept sibling of FC-69.
1242
- - Siblings swept (every `failIds(` and test-output-parsing `run(` under loki-ts/src, all checked): package_suite.ts runPackageSuites (fixed), engine10/stages/deep.ts runFullSuite (fixed), runner/repo_profile.ts shellRunner (already plainTestEnv + stripAnsi), engine10/stages/verify.ts runOnce (plainTestEnv + stripAnsi, FC-69) and its base rerun failIds at subtractBase (consumes runOnce output), runner/load_owner.ts runOnBase (plainTestEnv + stripAnsi), engine10/stages/wall.ts (plainTestEnv + stripAnsi), runner/quality_gates.ts npm test run (summaryFailedCount strips ANSI itself; no ids parsed). The remaining `run(` sites (python, doctor, proof, status, provider, supply, speed) do not parse test output.
1243
- - Mechanism: one `suiteEnv(path)` (plainTestEnv plus the caller's PATH override) for both full-suite runners; ANSI stripped at capture before harnessLoadReason, classifyCheck and failIds; `failIds` strips ANSI itself (idempotent) so no caller can regress; attributeFullSuite says "head failure ids unparseable" instead of "none of the failing checks match them" when a failing check has no parsed ids against an attributable baseline (still blocking, fail-closed); a full-suite failure carries a redacted bounded `failureExcerpt` into the existing verify `excerpts` map, so the fix brief shows failure text.
1244
- - Fixture: loki-ts/tests/engine10/verify_prered.test.ts "FC-FULLSUITE-ANSI-IDS" (colored vitest transcript), loki-ts/tests/engine10/monorepo_cwd.test.ts (colored failing package run keeps ids and a redacted excerpt; child runs under FORCE_COLOR=0 NO_COLOR=1), loki-ts/tests/runner/harness_intelligence.test.ts (colored head excused against a plain superset baseline, colored new ids still block, unparseable ids named, excerpt in the brief). Red before the fix, green after.
1245
-
1246
- ## FC-WALL-OUT-OF-REPO-TARGET A Wall check resolved the code under test from an installed copy outside the repo, so the implementer edited runner config and the run ended PARTIAL (WALL-OUT-OF-REPO-TARGET)
1247
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#81826` on loki 11.17.76 ended PARTIAL with a runner-config tampering flag. Raw: the Wall test's target was `process.env.X ?? join(homedir(), '.claude/plugins/.../name')`, an installed copy, while the change lives in the repo; the implementer added bunfig.toml and a preload to redirect the env var.
1248
- - Law: a Wall check exercises the repo's own code on the tree under change; it never needs runner config to reach it.
1249
- - Cause (measured): the Wall writer prompt never said to use repo-relative paths and no validation looked at where a Wall file resolves its target.
1250
- - Siblings swept: Wall author (fixed); implement brief repro-test line (implementer-written tests are not statically checked; with an in-repo Wall the implementer has no reason to touch runner config); fix and plan stages write no tests.
1251
- - Mechanism: one `outOfRepoTarget` detector in project_model/conventions.ts (homedir, Path.home, $HOME, ~/; comments ignored; a literal absolute path with no home-derived value does not flag, TEN-QUALITY-41; the HOME-assignment exemption covers only lines from the assignment on, TEN-QUALITY-42); wallAuthor re-asks once with the precise reason, a file still failing is discarded (sealed copy only, listed in `discarded`); one sentence in the cache-stable Wall brief.
1252
- - Fixture: loki-ts/tests/engine10/fc23_wall_conventions.test.ts describe "FC-WALL-OUT-OF-REPO-TARGET" (rejected, re-asked, discarded; repo-relative and tmpdir pass). Red before the fix, green after. The detector also flags a quoted absolute home literal (/Users, /home, /root, C:\Users) that is the direct target of an import, require or source line (the installed-copy import shape), while plain string literals stay unflagged; fixture "literal import target flagged".
1253
- - Sibling (TEN-QUALITY-44, fixed): the implement-stage repro test (LOKI_E10_REPRO=1, lean path). After the session, changed test-like files go through the same `outOfRepoTarget` (`outOfRepoTests` in project_model/conventions.ts) and one re-ask built by the shared `outOfRepoReask` text the Wall also uses. Fixture: loki-ts/tests/engine10/implement.test.ts `-t "repro test out of repo"`.
1254
- ## FC-OUT-OF-REPO A request whose change has no target in the repository ran every stage before ending BLOCKED (OUT-OF-REPO-INTAKE)
1255
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#21527` (a feature whose code lives in a hosted Slack/web service) on 11.17.60 to 11.17.74 spent plan, wall, implement and fix before ending BLOCKED. Raw: the plan session had nothing to name, but plan-scope.json with zero files was read as "no scope", so the flow continued into implement.
1256
- - Law: cheap checks run before expensive stages; a run that cannot succeed says so at the first stage that knows; BLOCKED names the question, never a bare failure.
1257
- - Siblings swept: intake already_satisfied (short-circuits to the tail, unchanged), intent_declined (returns before wall/implement), a missing or malformed plan-scope.json (unknown, deliberately NOT out-of-repo), a skipped plan on small tasks (no signal, implementer still decides).
1258
- - Mechanism: one reader `planOutOfRepo` (util/run_cap.ts) fires only when plan-scope.json parses and names zero files, taking the planner's `out_of_repo` sentence as the reason; plan.ts records `out_of_repo` plus `spec_conflict_reason: "out-of-repo: <reason>"`; machine.ts seeds implement.exit = spec_conflict and jumps to the tail, so seal, outcomeOf, EXIT (4), the BLOCKED question comment and the receipt reuse the existing spec-conflict path (no new outcome). conflictReasonOf also reads the plan stage.
1259
- - Fixture: loki-ts/tests/engine10/plan.test.ts "FC-OUT-OF-REPO" (named files never fire; missing or malformed scope never fires) and loki-ts/tests/engine10/machine.test.ts "FC-OUT-OF-REPO machine" (implement, verify and fix do not run; positive control runs them). Red before the fix, green after.
1260
- ## FC-NO-PUSH-UNVERIFIED A run that did not end verified pushed its loki/e10-* branch to the user's remote (NO-PUSH-UNVERIFIED)
1261
- - User-visible vs raw: user-visible. BLOCKED runs of anthropics/claude-code#21527 pushed loki/e10-* branches and opened draft PRs on the user's fork asklokesh/claude-code. Raw: supervisor.ts gated the pr step on `verdict !== "ALREADY_SATISFIED" && hasDiff`, so FAILED, PARTIAL and SPEC_CONFLICT with any diff left the machine.
1262
- - Law: only a verified success leaves the machine; unverified work stays local as evidence.
1263
- - Siblings swept: the supervisor pr step and modernize/ship.ts shipWave (which pushed draft PRs for unverified waves; now refuses unless the wave verified and every unit is PROVEN) are the callers of the push scripts (engine10-push.sh, engine10-push-gitlab.sh); ALREADY_SATISFIED already opened none; the E-67 backstop commit still lands locally on a killed worker; cancelled and tampered runs already skip it.
1264
- - Mechanism: one gate in supervisor.ts, `verdict === "VERIFIED"`; every other verdict with a diff prints "branch loki/<runId> stays local (not pushed)" with the repo path. Supersedes the A-110 and E-67 draft-PR-on-unverified behaviour (tests updated in the same change).
1265
- - Fixture: loki-ts/tests/engine10/supervisor_backstop.test.ts "FC-NO-PUSH-UNVERIFIED" (SPEC_CONFLICT, PARTIAL, FAILED call pr 0 times; VERIFIED once) and loki-ts/tests/engine10/modernize/ship.test.ts "FC-NO-PUSH-UNVERIFIED" (unverified wave pushes 0 times). Red before the fix, green after.
1266
- - Moat fixture: tests/moat/p9-rule-of-two.sh seeds a real pytest test and a project .venv (python and ruff shims) so its default, auto-pr and opt-out legs seal VERIFIED and Loki's own push and PR fire; its "unverified" leg removes the ruff shim, seals PARTIAL, and asserts "stays local (not pushed)" with no Loki push or PR.
1267
-
1268
- ## FC-LINT-PRE-RED: a lint check excused as failing on base still named the run's failure
1269
- - User-visible vs raw: user-visible. A run whose only failing check was a lint check also failing on base (cc#81826, shellcheck SC2181 on both) printed `Reason: lint:shellcheck failed` and listed it under Failures in HANDOFF.md, so the founder read a base-relative pass as a blocker.
1270
- - Law: a check verify excused as pre-existing (`pre_red_checks`, TEN-QUALITY-03) is never the stated reason, never a Failure; it is shown separately as already failing on base.
1271
- - Cause (measured from .loki/runs events): verify judged lint base-relative correctly in all three rounds (`pre_red_checks: ["lint:shellcheck"]`, fix briefs excluded it, seal skipped it); only `reasonOf` (engine10/output.ts) and the handoff renderer (util/run_summary.ts) re-read raw `checks[].result` and ignored `pre_red_checks`.
1272
- - Siblings swept: seal verdict (already honours pre_red_checks), failures_grouped and fix brief (already exclude), reasonOf and handoff Failures (fixed); commands/export_sarif.ts receiptFindings (reads pre_red_checks from the run events), util/failure_memory.ts failedCheckNames (engine10/supervisor.ts now passes verify's pre_red_checks), e10ext/select.ts lintFails via AttemptCandidate.preRedChecks (all fixed through the one `isBlockingFail` in util/failure_memory.ts). features/attempts.ts now fills `preRedChecks` through the optional `AttemptOps.preRed(i)`, but stays inert until `score()`'s caller supplies preRed (the real treeswapOps runs no lint). Open: supervisor.ts must pass verify `pre_red_checks` as failedCheckNames' second argument. Other linters share the same path (ruff, eslint via excuseLint; unparseable output stays fail-closed with the existing reason).
1273
- - Mechanism: `pre_red_checks` read in `reasonOf`; `composeHandoff` tags failing checks in it `pre_red`, `renderHandoff` moves them to "Already failing on base".
1274
- - Fixture: tests/engine10/output.test.ts and tests/util/run_summary.test.ts, "FC-LINT-PRE-RED". Red before the fix, green after.
1275
-
1276
- ## FC-SEAL-NA-AS-UNPROVEN A package with no test command, a repo with no app, and deep checks the risk tier never asked for were listed NOT PROVEN, so a fully proven run ended PARTIAL or carried phantom gaps (SEAL-NA-AS-UNPROVEN)
1277
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#81826` on loki 11.17.78 (run e10-20261019T064138Z-a7ca) ended PARTIAL solely because verify.not_proven held "full suite: package claude-code-repo defines no test command", while the full-suite runs for the other packages and the Wall test passed. Raw: a package without a test command has nothing to run; it is not applicable, not unproven. Every receipt also listed "full suite", "app boot", "council" and "security scan" as NOT PROVEN even when the risk tier never selected them or verify had run the full suite.
1278
- - Law: NOT PROVEN lists what was needed and not shown; what does not apply is disclosed separately and never hidden; unknown stays unproven (fail-safe); a real gap never becomes VERIFIED.
1279
- - Cause (measured): project_model/package_suite.ts added the no-test-command note unconditionally; util/app_probe.ts returned not_run when app-runner reported "no application detected"; seal.ts spread the static DEEP_NOT_PROVEN constant into every receipt without reading risk.checks_deferred. The only N/A filter was a prefix match on "full suite (no test runner" in verify.ts.
1280
- - Siblings swept: package_suite.ts runPackageSuites (fixed); deep.ts runFullSuite legacy "full suite (no test runner detected)" (now N/A, listed); verify.ts not_proven filter (one naNote helper replaces the prefix match); app_probe.ts "app boot" and "http probe" (n/a only when app-runner itself said no application detected; a Docker project with the daemon down, an unsafe command or a throw stay not_run); deep.ts runAppBoot and the addendum not_proven; seal.ts DEEP_NOT_PROVEN (now deepGaps from risk.checks_selected and checks_deferred, fail-safe to the full list when risk is absent or malformed); supply, review, contract and wall not_proven are real gaps and untouched.
1281
- - Mechanism: one `naNote`/`naText` pair in package_suite.ts (the "n/a: " prefix travels in the existing notProven Set) and one `deepGaps` in util/check_result.ts. A package without tests is N/A only when a real suite ran AND (the diff is not in that package OR a passing Wall or holdout check whose test file lies inside THAT package proves it: the package root owns the file, root "." owns any file inside no other package, and a Wall that failed anywhere voids the proof; round 2 closed the cross-package leak); an unknown diff, or changed files it owns with no Wall proof, stay NOT PROVEN. N/A notes reach the receipt as the additive `not_applicable` field and a "### N/A" section; they never enter verdictOf. Tier-skipped deep checks read "not selected by risk tier: X" there; a selected-but-unrun check stays NOT PROVEN; an executed full suite is dropped.
1282
- - Fixture: loki-ts/tests/engine10/monorepo_cwd.test.ts (package without tests: N/A, owned-by-diff stays NOT PROVEN with and without proofFiles, an in-package proof makes it N/A, a proof in another package does not, root package with plugins/** diff stays N/A, unknown diff fail-safe; verify wiring describe mutates proofFiles, wallProofOf and the not_proven filter), seal.test.ts describe "FC-SEAL-NA-AS-UNPROVEN" (N/A section, low and high tier, executed full suite dropped, no-risk fail-safe, real gap still downgrades), tests/util/check_result.test.ts deepGaps, tests/util/app_probe.test.ts (empty repo n/a, missing Docker runtime stays not_run), deep.test.ts (n/a boot, no runner). Red before the fix, green after.
1283
-
1284
- ## FC-WALL-PROOF-PACKAGE-BLIND A passing Wall at the repo root that drives a test-less package's changed files by path never credited that package, so a multi-root run sealed PARTIAL instead of VERIFIED
1285
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#81826` on loki 11.17.97 sealed PARTIAL: the diff changed plugins/ralph-wiggum/hooks/stop-hook.sh and scripts/setup-ralph-loop.sh, the Project Model (LLM generated, nondeterministic) declared a package `plugins` with no test command, and the Wall (always at the repo root since 11.17.95, red on base, green on head, driving both scripts by path) never matched it. The note "full suite: package plugins defines no test command" stayed NOT PROVEN, the verdict went PARTIAL before crossReview ran (xreview.ts only reviews VERIFIED), and "one reviewer" was also reported NOT PROVEN. The previous run, whose model had no `plugins` package, sealed VERIFIED. Raw: proof credit was decided by where the proof file lives, not by what it exercises.
1286
- - Law: a passing proof proves what it exercises; credit follows the code under test, never the proof's location or the model's nondeterministic package split. Unknown stays unproven (fail-safe).
1287
- - Cause (measured): runPackageSuites credited a test-less package only when a proof file's packageOf(...).root equalled the package root.
1288
- - Siblings swept: package_suite.ts runPackageSuites is the only consumer of proofFiles for package credit (grep proofFiles, packageOf over loki-ts/src: deep.ts and verify.ts only forward the list; verify.ts wallProofOf only decides whether the Wall passed; seal.ts and check_result.ts deepGaps consume the resulting note prefix, not the proof). No other place credits proof by package root or location, so there is nothing else to fix.
1289
- - Mechanism: execution evidence, never proof text (a text or path match is gameable, and the real Wall locates scripts by basename search, never naming a path). One `execProves` plus `wallRedOnBaseOf` in package_suite.ts, fed by verify.ts through the existing runFullSuite opts (`wallBaseRed` from wall.base_run). A test-less package is N/A when (a) the Wall was red on base (base_run.fail > 0; missing, all not_run or green never credits), (b) it passed on head (proofFiles non-empty from wallProofOf, which already needs every Wall and holdout check to pass), and (c) the diff is known, non-empty after dropping this run's own Wall files, has no quoted path, and every remaining file is owned by that package. A red-on-base, green-on-head Wall over a diff confined to one package necessarily exercised that package's change. A diff spanning packages gets no credit from this rule; the in-package proof rule is kept.
1290
- - Fixture: loki-ts/tests/engine10/monorepo_cwd.test.ts describe "FC-WALL-PROOF-PACKAGE-BLIND" (credited with base red and a single-package diff; NOT PROVEN with base green, base_run missing, a two-package diff, unknown, empty or quoted diff, Wall failed on head; wallRedOnBaseOf cases). Red before the fix, green after.
1291
-
1292
- ## FC-WALL-FILE-OWNS-EVERY-PACKAGE The run's own passing Wall file at the repo root has no owning package, so every test-less package read as owning the diff and stayed NOT PROVEN
1293
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#81826` on loki 11.17.99 sealed PARTIAL with "full suite: package scripts defines no test command" although the whole diff was under plugins/ (package `plugins`, credited by FC-WALL-PROOF-PACKAGE-BLIND) and `scripts` was untouched. Run e10-20261019T165943Z-a552: wall base_run fail=1, Wall green on head.
1294
- - Law: a proof is not product. Ownership of a diff is decided by the product files it changes; an unknown, quoted or unowned product path still fails safe to owned.
1295
- - Cause (measured): runPackageSuites `owns` counted changed files with no owning package as owned by every package; tests/loki_wall_*.test.ts (the root Wall) is such a file. execProves already excluded the Wall files; `owns` did not.
1296
- - Siblings swept: execProves (already excludes Wall files), the proofFiles credit path (unchanged), and the quoted-path and unresolved-path rows of FC-CHANGED-PATH-QUOTED (still owned: only this run's passing Wall files, from proofFiles, are excluded).
1297
- - Mechanism: one `product` list in runPackageSuites, the changed files minus this run's passing Wall files (the same set execProves excludes). Unknown diff or a Wall-only diff stays owned (fail-safe). A failed Wall has no proofFiles, so nothing is excluded.
1298
- - Fixture: loki-ts/tests/engine10/monorepo_cwd.test.ts test "FC-WALL-FILE-OWNS-EVERY-PACKAGE" (N/A with a passing root Wall and an untouched test-less package; NOT PROVEN when the Wall failed or an unowned product path is present). Red on HEAD 2739c8e26 (1 fail), green after (38/0).
1299
-
1300
- ## FC-VERIFIED-PR-WITHOUT-ITS-TEST A VERIFIED run's pushed branch never contained the acceptance test that made it VERIFIED
1301
- - User-visible vs raw: user-visible. anthropics/claude-code#81826, run e10-20261019T171807Z-7698, sealed VERIFIED; its commit changed 2 product files and tests/loki_wall_ralph_max_iterations.test.ts stayed untracked, so the PR reviewer saw "Missing tests: the diff has no test changes" for the very test that ran green.
1302
- - Law: a proof a reviewer is asked to trust must be reproducible from the pushed branch (evidence or it did not happen). Not product, but publishable once the verdict is sealed.
1303
- - Cause (measured): commit_filter.ts dropSet drops every WALL_FILE (loki_wall_*) in the commit stage and in backstopCommit, and nothing publishes the Wall afterwards, even on VERIFIED.
1304
- - Siblings swept: backstopCommit (killed or crashed worker: stays dropped), discardIfSatisfied, runPackageSuites proofFiles (FC-WALL-FILE-OWNS-EVERY-PACKAGE), flagOutsideScope, formatChanged, receipt diff_sha256 and head_sha, seal_drift head check, reviewer_body changed-files list: none sees a Wall file as product, because the Wall is published in a SEPARATE child commit made at seal after the verdict.
1305
- - Mechanism: publishableWall (util/check_result.ts) selects the Wall files verify ran and passed (target_checks + executed proof), reads the SEALED copy under runDir/wall and requires it to hash to wall.files[].sha256 (mismatch: not published, NOT PROVEN note). The seal stage, only when the verdict is VERIFIED, writes the sealed bytes and makes one child commit (trailer Loki-Wall-Publish). The receipt binds the PRODUCT head (head_sha, tree, diff_sha256 computed before the child commit); the pushed head is its child whose only change is Wall files, so sealDrift reads it as benign head movement (no overlap with the sealed diff). The PR body gains an "Acceptance tests" section (paths and the exact verify command). Opt-out LOKI_COMMIT_WALL=0.
1306
- - Fixture: loki-ts/tests/engine10/seal.test.ts describe "FC-VERIFIED-PR-WITHOUT-ITS-TEST" and loki-ts/tests/engine10/pr_body_golden.test.ts (publish on VERIFIED, sealed bytes not agent bytes, failed/not-run/PARTIAL/opt-out/backstop keep dropping, receipt head and diff unchanged, PR body section). Red on base 4ade2c9a6, green after.
1307
-
1308
- ## FC-CHANGED-PATH-QUOTED A non-ASCII changed path came back from git as a quoted octal escape, so no consumer of changedFiles could match it to a package, Wall file or test
1309
- - User-visible vs raw: user-visible. A diff in `frontend/src/café.ts` made the test-less frontend package read as not owned by the diff, its note became N/A, not_proven was empty and the run could seal VERIFIED with the frontend edit unproven (HIGH re-review of FC-SEAL-NA-AS-UNPROVEN r2).
1310
- - Law broken: a git path must be the real path; an unresolvable path must fail safe, never read as "not in the package".
1311
- - Siblings swept: every consumer of changedFiles/diffFiles (package ownership, uncoveredChanged, risk tier, impacted tests, lint selection); diffFiles numstat and name-status keyed by the same path (fixed together); secure_scan.ts already used quotePath=false.
1312
- - Mechanism: `changedFiles` uses `-c core.quotePath=false` and `-z` (NUL split) for both git calls, diffFiles numstat and name-status use `-z` with `--no-renames` (simple NUL records, so a name holding a quote, backslash, tab or newline is never escaped); `--no-renames` on the changedFiles diff too, because a rename listed only its new path and hid the source package; the package ownership check treats a changed path packageOf cannot resolve, or one that starts with a quote, as owned (stays NOT PROVEN).
1313
- - Fixture: loki-ts/tests/util/check_result.test.ts and tests/engine10/monorepo_cwd.test.ts, "FC-CHANGED-PATH-QUOTED" (non-ASCII path in changedFiles; git mv across packages keeps the source path; diffFiles on a name with a double quote; verify with a non-ASCII frontend file and a backend Wall stays NOT PROVEN; unresolvable path fail-safe). Red before the fix, green after.
1314
-
1315
- ## FC-WALL-PHANTOM-PATH A Wall check located the code under test at a repo path that does not exist, and the implementer added a symlink to make it resolve (PR carried a link instead of the change)
1316
- - User-visible vs raw: user-visible. `loki start anthropics/claude-code#81826` on loki 11.17.77: the issue names the plugin "ralph-loop", the repo directory is plugins/ralph-wiggum. Raw: the Wall wrote `join(ROOT, 'plugins', 'ralph-loop')` (absent) and the implementer committed `plugins/ralph-loop -> ralph-wiggum` to satisfy it.
1317
- - Law: a Wall check names code that exists in the base tree or that the plan creates; a change never satisfies a check by adding an alias for a path.
1318
- - Cause (measured): FC-WALL-OUT-OF-REPO-TARGET validated only home-derived targets; nothing resolved repo-relative literals against the base tree, and seal looked at no added-symlink signal (E-55 covers only base-side typechanges of test files).
1319
- - Siblings swept: Wall author (fixed); implement stage (nothing to forbid in-brief; the diff is judged at seal); fix and plan stages write no Wall tests; seal diff checks (weakened test, runner config) did not look at mode 120000 on the head side (fixed).
1320
- - Mechanism: `phantomRepoPaths` in project_model/conventions.ts evaluates join/resolve chains rooted at the test dir or cwd (const assignments followed) and reports missing paths the plan does not name; wallAuthor folds it into the existing single re-ask (reason plus same-named files from the repo map) and discards a file still failing. The detector resolves process.cwd() against both the test's nearest package root and the repo root, and skips paths the test creates, git ignores, or the plan names WITH its parent (plugins/ralph-loop; never a bare segment or basename). `process.env.X ?? Y` and `|| Y` evaluate as Y (the real #81826 file used `process.env.REPO_ROOT ?? process.cwd()`); the two cwd runs pair calls by source line and text, not array index. `addedSymlinkNotes` (util/base_guard.ts, folded into seal's rcNotes) makes an added symlink (status A, or T from a regular file; never M) or added submodule pointer a NOT PROVEN item that blocks VERIFIED unless the plan asks for one by the word symlink or submodule, un-negated; alias is not an escape.
1321
- - Fixture: loki-ts/tests/engine10/fc23_wall_conventions.test.ts describe "FC-WALL-PHANTOM-PATH" (reported, existing passes, plan-created passes, re-ask, discard); loki-ts/tests/engine10/seal.test.ts "FC-WALL-PHANTOM-PATH" (added symlink, plan-requested, deletion, modified link, negated plan, submodule). Round 2 adds monorepo cwd, created-path, ignored-path, plan-parent, split-call and same-name-directory cases, and a fixture built from the real #81826 Wall file. Red before the fix, green after.
1322
- - Sibling note (in-test-dir phantom, v11.17.83, #81826 rerun): the Wall wrote `resolve(import.meta.dir, 'plugins/ralph-loop')`, which is tests/plugins/ralph-loop, absent from the base tree. Raw: phantomRepoPaths reported only paths escaping the test dir, so it passed; the implementer symlinked tests/plugins/ralph-loop -> ../../plugins/ralph-wiggum and seal ended PARTIAL.
1323
- - Law: same as above; a repo path the Wall invents is corrected up front, whether or not it escapes the test dir.
1324
- - Siblings swept: the escape-only assumption lived in one place (the report condition in `ev`); outOfRepoTarget, seal's addedSymlinkNotes and the implement repro-test path make no such assumption.
1325
- - Mechanism: `strayInTestDir` in phantomRepoPaths additionally reports a path that stays inside the test dir when its first segment is absent from the test dir but present at the package or repo root (the author meant a repo path). Same exemptions (plan with parent, created, ignored by the VCS, write/expect/tmp lines); fixtures the test dir lacks (fixtures/a.json) and __self__ stay unreported.
1326
- - Fixture: fc23_wall_conventions.test.ts "in-testdir phantom" (incident lines report tests/plugins/ralph-loop; plan-named, fixtures, created and __self__ paths pass). Red before the fix, green after.
1327
- - Siblings swept (review of 11.17.85): the package-root fallback trusts a known project model fully (its package cwd, else the repo root) and otherwise walks up to the nearest manifest-shaped JSON (string name plus another key); config files (tsconfig, linter, test-runner, xml, cfg, toml) are never roots, so they cannot hide a real phantom (fixture "fallback no false root"). Known limit: a nameless manifest or a non-JSON-manifest package resolves only through the model; without one the repo root is the cwd. The vendor-dir exemption is the ignore check alone, with no vendor-dir name or `*_modules` pattern, so src/my_modules/x and src/feature_modules/x are reported (fixture "modules dir phantom"). The release-gate pytest guard strips only the pip install segment before checking (tests/test-release-gate-dedup.sh M10).
1328
- - Sibling note (walk-up base, run e10-20261019T121849Z-848d, #81826 rerun): the Wall found the plugin with `for (const start of [process.cwd(), import.meta.dir]) { let dir = resolve(start); while (true) { const candidate = join(dir, 'plugins', 'ralph-loop'); ... dir = dirname(dir) } }` and threw 'plugins/ralph-loop not found'; the repo has plugins/ralph-wiggum. Raw: the loop variable is unresolved, so phantomRepoPaths evaluated nothing, no re-ask ran, the implementer could not touch the Wall test and the run sealed SPEC_CONFLICT.
1329
- - Law: same as above; a repo path the Wall invents is corrected up front, however the base directory is reached.
1330
- - Siblings swept: the only place that dropped unresolved bases is `ev` in phantomRepoPaths; outOfRepoTarget and seal's addedSymlinkNotes build no paths. Error-message literals like 'plugins/ralph-loop' are not path construction and are never evidence.
1331
- - Mechanism: in phantomRepoPaths a `for (const X of [items])` makes X, and `let dir = resolve(X)`, a walk-up base only when every item evaluates to the cwd or the test directory (process.cwd(), import.meta.dir, __dirname or a const bound to them); repo-relative strings and join(...) items leave X unresolved, as before. A join/resolve on a walk-up base with literal segments is reported when its first literal segment exists at the package or repo root and the full path is missing at every directory the walk passes through: the test dir, the package root and the repo root, and each parent between them and the repo root. A path found at any of those directories is real and is not reported. A first segment absent everywhere stays silent as before; paths built on a walk-up base never use the escape rule, and nested joins on a candidate are not double reported. Temp and mkdtemp bases never resolve and stay unflagged; plan, created and ignored exemptions are unchanged.
1332
- - Fixture: fc23_wall_conventions.test.ts "walk-up phantom" (the real Wall shape reports plugins/ralph-loop; ralph-wiggum, plan-named, absent first segment, mkdtemp and tmpdir bases give []; HIGH review block fixtures FP-A repo-relative string loop, FP-B join on cwd loop, FP-C several joins loop, FP-D2 walk-up whose file exists on the way up all give []). Red before the fix, green after.
1333
-
1334
- ## FC-MEDIUM-REVIEWER-UNCREDITED The medium tier selects "one reviewer", nothing on the default path runs or credits it, so every medium run lists it NOT PROVEN and seals PARTIAL
1335
- - User-visible vs raw: user-visible. A real run on anthropics/claude-code#81826 (v11.17.83, risk tier medium) listed "one reviewer" under NOT PROVEN.
1336
- - Law broken: a check the tier selected is credited when, and only when, it really ran (evidence or it did not happen); a check that did not run stays NOT PROVEN and is never hidden or dropped from the plan.
1337
- - Cause (measured): check_result.ts PLAN.medium selects "one reviewer"; deferredOf keeps it in checks_deferred unless markExecuted names it; markExecuted has one caller (verify.ts, naming only "full test command" and "full suite"); no code names "one reviewer". The only reviewers are seal's crossReview (xreview.ts, opt-in via loki.yaml review, LOKI_REVIEW_PROVIDER or LOKI_XVENDOR_DEFAULT) and deep's council session (a detached worker that runs after seal). deepGaps (11.17.82, FC-SEAL-NA-AS-UNPROVEN) began pushing every deferred non-deep item into notProven; before it the receipt listed only the four static deep items, so this is a visibility regression of an old unkept promise, not a new bug in the reviewer.
1338
- - Siblings swept (every PLAN.selected item): "targeted tests" and "lint of changed files" run always (RAN_ALWAYS); "full test command" and "full suite" are credited by verify.ts markExecuted and deepGaps fullRan; "council" and "security scan" (high) are credited by deep checks via executed(); "one reviewer" had no credit path (fixed here). Low tier names are disclosed as not selected.
1339
- - Mechanism: one `REVIEWER_ITEM` credit rule inside `deepGaps`: credited when seal's cross-review returned a verdict (level not "not_run") or a deep council check executed. No review configured or a not_run review keeps it NOT PROVEN. The default path still has no reviewer, so default medium runs stay PARTIAL by design until a reviewer is configured; running a default same-vendor reviewer on every medium run is a separate cost decision.
1340
- - Fixture: loki-ts/tests/util/check_result.test.ts "medium reviewer" (tier alone, not_run review, verdict review, council pass and not_run, high council not credited by a cross-review, low unaffected). Red with the credit removed, green after.
1341
- - Sibling note (medium default review): the credit above was inert on a default run because the cross-review itself was opt-in (loki.yaml `review:`, LOKI_REVIEW_PROVIDER or LOKI_XVENDOR_DEFAULT). `crossReview` in engine10/stages/xreview.ts now defaults to the run's own provider and CLI (same vendor) when the risk record says medium and nothing is configured; a configured reviewer or council agent keeps precedence, and LOKI_REVIEW_PROVIDER=off or none opts out (one reviewer then stays NOT PROVEN). The receipt `review` block carries `same_vendor: true, defaulted: true`. A BLOCK still caps the verdict at PARTIAL through `minVerdict` (never upgrades). Low and high tiers do not default. Fixture: loki-ts/tests/engine10/xreview.test.ts describe "medium default review" (red with the default removed). Slow-reviewer follow-up: seal (limit 60s) used to kill the stage, and the receipt, when a default review ran long; seal now gets `extraLimitS` (the review box, LOKI_E10_REVIEW_BOX_S, default 300s, plus 30s) only when a review is planned, the stage abort signal reaches the reviewer child, a review past its box is not_run, and the receipt review block carries `level`. Fixtures: seal.test.ts "slow reviewer keeps receipt" and "seal passes reviewRan through".
1342
-
1343
- ## FC-OUTCOME-FLOOD A per-item NOT PROVEN list (pre-red tests) had no cap, so one run printed about 300 "pre red:" entries and filled the terminal, PR body and receipt.md
1344
- - User-visible vs raw: user-visible. Run e10-20261019T130749Z-f11c on asklokesh/FireLater#20 (11.17.93): every already-failing frontend test appeared twice on the NOT PROVEN line, once by short name and once by full path name, next to "baseline: 1 pre-existing failing check(s) (not blocking), 0 introduced".
1345
- - Law broken: a disclosure must stay readable; an informational count is one line, the detail lives in the machine record.
1346
- - Cause (measured): seal.ts adds one `pre red: <id>` per verify.pre_red id (verify records both name forms); output.ts, reviewer_body (via pr.ts), renderReceiptMd and the handoff each printed every receipt not_proven line unbounded.
1347
- - Siblings swept (every per-item list in seal's notProven): pre red, not run, flaky test (also the separate flaky clause in formatSummary), skipped on base (excused), changed untested after limit, reverted test edit, wall test discarded, Wall test not executed, weakened test. Single-fact lines (baseline, verdicts) are not lists and stay.
1348
- - Mechanism: one display-only `collapseNotProven` in util/term_width.ts, applied at the four surfaces. Pre-red names are deduplicated (a short name is the tail of its " > " full path twin) and collapse to "pre red: N tests already failing on base (listed in receipt.json)" past 3; other lists collapse to "<prefix>N entries (listed in receipt.json)" past 8. Idempotent. receipt.json not_proven and the receipt digest are unchanged.
1349
- - Fixture: loki-ts/tests/engine10/output.test.ts "collapseNotProven" (150 short plus full-name entries), route_receipt_golden.test.ts "FC-OUTCOME-FLOOD receipt.md", tests/util/run_summary.test.ts handoff case. Red before the helper existed, green after.
1350
-
1351
- ## FC-WALL-LOCATION-BLIND The Wall author was never told where its file would live, so a root-relative __dirname built phantom paths; and a stale untracked Wall file from an earlier run was credited as an exogenous pass
1352
- - User-visible vs raw: user-visible. `loki start asklokesh/FireLater#20` on loki 11.17.93 (run e10-20261019T130749Z-f11c) sealed PARTIAL with no Wall proof. Raw: the author wrote "this file lives at the repo root" with `const root = __dirname` and joined backend/src and frontend/src; Loki installs Wall files beside the first detected test (backend/src/lib), so the detector correctly flagged backend/src/lib/backend/src. The re-ask said "locate the real code by searching", which misdiagnosed a location error. Separately the receipt listed "wall test discarded: loki_wall_websocket.test.ts" and also an exogenous pass for vitest:backend/src/lib/loki_wall_websocket.test.ts.
1353
- - Law: an author that writes a file is told the repo-relative directory the file will run from and what __dirname resolves to there; a re-ask names the real cause; a discarded Wall file is never also credited as a check.
1354
- - Cause (measured): wallAuthor passed no destination to buildWallBrief (the brief says the author cannot see the repository); phantomReason had no notion of the test directory. The pass came from an untracked loki_wall_websocket.test.ts (mtime 00:55, an earlier run's installed Wall file, a different body than this run's discarded copy) still in the working tree: verify's changedFiles lists untracked files, so it ran as a changed test and lint target.
1355
- - Siblings swept: Wall author (fixed: brief names the destination, re-ask names the cause); implement bug-repro test and fix stage (an in-repo session whose cwd is the repo, so the file location is the author's own choice; nothing blind); verify changed set (fixed: a loki_wall_* path not installed by this run's Wall is dropped; the commit stage already excludes the prefix); holdout files are staged later by stageHoldout and unaffected.
1356
- - Mechanism: buildWallBrief takes the destination from wallTargetDir (the value installCore copies to); phantomReason(miss, repoFiles, testDir, pkgRoot) names "your test is copied to X/, __dirname is that directory, not the repo root; Y exists relative to the repo root" when the phantom is testDir joined with a path real at the repo or package root; util/check_result.ts withoutStaleWall filters verify's changed list.
1357
- - Fixture: loki-ts/tests/engine10/fc23_wall_conventions.test.ts describe "FC-WALL-LOCATION-BLIND" (brief, reason, end-to-end re-ask with the run's file shape, stale file excluded, own file kept). Red before the fix (4 of 6), green after.
1358
-
1359
- ## FC-XREVIEW-BLIND-TOOLS The seal cross-reviewer is given no shell and a prompt that asks it to check behaviour, so it reports "my tool output came back empty" as a review flag and reviews blind
1360
- - User-visible vs raw: user-visible. Run e10-20261019T124856Z-0c36 (anthropics/claude-code#81826, 11.17.93) sealed with two NOT PROVEN cross-review flags: "My tool output came back empty, so I could not confirm..." and "Acceptance lines, traced by hand because the shell output was not visible".
1361
- - Law broken: a reviewer is told what it can and cannot do; a limit of its own sandbox is never reported to the user as a defect of the work (evidence or it did not happen).
1362
- - Cause (REAL ROOT CAUSE, corrected after HIGH review of 54ffc9f0c): `leanReviewerFlags()` emitted `--tools=`, which disables ALL tools in Claude CLI 2.1.303 (stream-json init shows `"tools":[]` for the old and the first-fix argv alike), so allowlist and denylist were moot and the Bash deny was a red herring. Original observation: reviewArgv for claude passed `--allowedTools Read,Grep,Glob --disallowedTools Write,Edit,NotebookEdit,Bash`, so the reviewer had no shell at all; the prompt (acceptance block: "Check the diff against each line") invites running commands. Repro with the exact flags (haiku, `a.sh` containing `echo hi`): "This session has no shell tool ... there is no bash output", permission_denials empty. The process-group spawn (11.17.92) and cwd/env are not involved; the reviewer ran and returned a verdict.
1363
- - Siblings swept: council voters (council/voter_agents.ts) and quality_gates reviewers already use the shared REVIEW_ALLOWLIST_TOKEN (read-only git/cat/ls/grep/rg/find/head/tail/wc) plus REVIEW_GUARD_DENYLIST (mutations only), so they can inspect; seal xreview was the one reviewer with a blanket Bash deny. Codex xreview runs in its read-only sandbox with a shell, unaffected.
1364
- - Mechanism: xreview claude argv passes `leanReviewerFlags({ tools: "Read,Grep,Glob,Bash" })` = `--tools=Read,Grep,Glob,Bash` (one argument, so the variadic option cannot swallow the prompt), Bash narrowed by the shared allowlist; the prompt (REVIEW_TOOLS_NOTE) lists only the tools the reviewer has and tells it to say so when it could not inspect something (it is never told to hide an inspection failure). Write escapes under the allowlist (reproduced: `rg --pre rm`, `git diff --output=f`) are closed in REVIEW_GUARD_DENYLIST (TS and autonomy/lib/claude-flags.sh, byte-identical): `rg --pre/--pre-glob`, `--output`/`--ext-diff`/`--no-index` on git diff/log/show/blame, `git --exec-path`, `find -delete/-exec/-execdir/-ok/-fprint/-fls`, each as a prefix form and a mid-argument glob form (`Bash(git * --output*)`; live probe confirmed `git diff HEAD~1 --output=pwned.txt` and `rg one --pre rm a.txt` are denied). This also hardens council voters and quality_gates, which share the list. Sibling sweep: `leanReviewerFlags` default (`--tools=`) is unchanged for its other caller, quality_gates claudeReviewer (haiku-routed text-only reviewer, no allowlist, diff in prompt; blind by design, not the bug); council voters do not use it. Live init line (haiku, real argv, stream-json): `"tools":["Bash","Glob","Grep","Read"]`. Round 2 (HIGH review of e4e690dce): allowed `git status/diff/show/log` execute programs named by the repo `.git/config` and attributes (core.fsmonitor, diff drivers, textconv) and so can rewrite the worktree before seal hashes it; xreview therefore uses `REVIEW_ALLOWLIST_NO_GIT` (shared allowlist minus every `Bash(git` entry; the diff is already in the prompt) and the note says no git commands are available. Repo settings: with `--setting-sources project` a repo `.claude/settings.json` can carry `permissions.allow` or `defaultMode`, and the machine user `defaultMode: auto` made `touch marker.txt` run in a live haiku probe (settings-source-dependent, not proven for a trusted repo offline), so xreview now passes `--setting-sources user` plus `--permission-mode dontAsk`; probe with those flags: `cat f.txt` ran, `touch marker.txt` was denied and no marker was created even with a repo settings file allowing touch and bypassPermissions. Round 3 correction: `--setting-sources user` was itself a hole (user `permissions.allow` such as Bash(python3:*) pre-approves under dontAsk, user hooks fire in every review child, and the user CLAUDE.md added about 4,331 cache-creation tokens per review). xreview now passes `--setting-sources ""` (no source loads; the CLI accepts the empty value, "none" is rejected): live probe with the real argv, `python3 -c "print(6*7)"` denied, `cat f.txt` ran, 0 hook events (vs 12 with `user`), init tools `["Bash","Glob","Grep","Read"]`. Also closed: `rg --hostname-bin` (runs a program). A leading `rg --pre=CMD` form slipped past both `Bash(rg --pre=:*)` (prefix rules need a word boundary) and `Bash(rg * --pre*)` (needs an argument before it); live probe confirmed it ran until `Bash(rg --pre*)` and `Bash(rg --hostname-bin*)` were added, then denied. The new test records the argv the stub reviewer CLI really receives. Sibling note: council voters and quality_gates share the git allowlist and are exposed to repo-config driver execution; follow-up slice FC-REVIEWER-GIT-CONFIG-EXEC (not fixed here). Fixture: loki-ts/tests/engine10/xreview.test.ts describe "FC-XREVIEW-BLIND-TOOLS" (red before, green after).
1365
-
1366
- ## FC-PR-BODY-PUBLIC-HYGIENE The PR body, written for a public upstream repo, carried template boilerplate, a local absolute path, a file the commit never included, unlabeled base-red checks and flags cut mid-sentence
1367
- - User-visible vs raw: user-visible. Run e10-20261019T124856Z-0c36 on anthropics/claude-code#81826 sealed VERIFIED, and its PR body said: "What the issue asked" listed the issue template's checklist ("I have searched existing issues..."); "What changed" listed tests/loki_wall_ralph_max_iterations.test.ts, a Wall file the commit stage drops; Receipt printed `File: /private/tmp/.../receipt.json`; "How it was tested" showed `shellcheck ... -> fail` with no context while the handoff section called the same check "Already failing on base"; NOT PROVEN bullets were cut at 300 characters ("The \"no state") and one bullet was a dangling header ("Acceptance lines, traced by hand ...:") whose sub-lines fell past the 5-note cap.
1368
- - Law: every line of a body that leaves the machine is true of the PR (only files in the commit, only what was asked), contains no local path, labels a check the same way everywhere it appears, and never shows half a sentence.
1369
- - Siblings swept: status-comment (receipt_surface.ts renderReceiptComment: prints digest and `loki verify <id>` only, no path, no file list, no check results; clean); HANDOFF.md (run_summary.ts renderHandoff already splits base-red checks under its own heading; its notProven clip ends with an ellipsis; clean); reviewer brief (`checks run: 4 (3 pass, 1 fail, ...)` hid base-red, and oneLine cut NOT PROVEN with no ellipsis: fixed); receipt.md renderer (`- fail: lint:shellcheck` hid base-red: fixed).
1370
- - Mechanism: one label helper in util/check_result.ts (`checkResultLabel`, `ALREADY_FAILING_ON_BASE`) used by the body builder, the brief and receipt.md; the receipt check gains an optional additive `pre_red: true` (absent on older receipts, digest computation untouched) and older runs fall back to verify.pre_red_checks. `renderReviewerBody` drops Wall files from the file list, strips issue-template checklist lines (preferring the intent card's "What I think you want" line), and prints the receipt path repo-relative. `parseReview` keeps each reviewer flag whole (header lines absorb their sub-lines, a header with nothing under it is dropped, cap 600 characters with an ellipsis).
1371
- - Fixture: loki-ts/tests/engine10/reviewer_body.test.ts "FC-PR-BODY-PUBLIC-HYGIENE" (real #81826 shape: template body, excluded Wall file, base-failing shellcheck, absolute receipt path); xreview.test.ts "parseReview keeps flags whole"; reviewer_brief.test.ts and seal.test.ts base-red labels. Red before the fix, green after.
1372
-
1373
- ## FC-CP-DENYLIST-DRIFT The control-plane Ask policy kept a hand-copied literal of REVIEW_GUARD_DENYLIST, so growing the loki-ts list (11.17.96 xreview rg --pre, git --exec-path, find -exec forms) turned main CI Tests red
1374
- - User-visible vs raw: raw (CI only). packages/control-plane/test/server/ask_policy.test.ts "the deny list covers every token of the loki-ts REVIEW denylist" failed with `Expected to contain: "Bash(git --exec-path:*)"` on main from v11.17.96. No user impact: the Ask job already denies the blanket `Bash`, so the missing tokens were redundant there; the guard exists so the mirror never silently lags.
1375
- - Law broken: a mirrored security list is either derived from its source or guarded by a parity test that fails in the same slice that edits the source (a release that adds tokens must update every mirror before it ships).
1376
- - Siblings swept: DENIED_TOOLS in packages/control-plane/src/ask/policy.ts (the only copy that was stale; fixed); autonomy/lib/claude-flags.sh (byte-identical mirror, already updated in 11.17.96); loki-ts consumers (stages/xreview.ts, stages/deep.ts, runner/quality_gates.ts, council) import the exported constant, no copy; no other mirror of REVIEW_GUARD_DENYLIST or REVIEW_ALLOWLIST_NO_GIT exists under packages/ or tests/ (grep of the whole tree excluding node_modules and dist). tests/test-control-plane.sh (CP-04) runs the control-plane tests as one bun process, so it was red from this same drift and no other cause.
1377
- - Mechanism: the control-plane package ships alone (Docker image has no loki-ts), so a cross-package import is not allowed; DENIED_TOOLS keeps a literal copy of every REVIEW_GUARD_DENYLIST token and the parity test now asserts `missing` is empty, so a failure lists every absent token at once instead of the first.
1378
- - Fixture: packages/control-plane/test/server/ask_policy.test.ts "the deny list covers every token of the loki-ts REVIEW denylist". Red before (missing "Bash(git --exec-path:*)" and the later tokens), green after.
1379
-
1380
- ## FC-REVIEWER-GIT-CONFIG-EXEC A reviewer child with the shared Bash allowlist (git status, diff, show, log) runs programs named in the target repo's own git config
1381
- - User-visible vs raw: user-visible as integrity, not output. A HIGH reviewer reproduced it: a plain `git status` in a reviewer child executed the repo's core.fsmonitor script and rewrote `.loki/runs/<id>/events.jsonl`; `git diff` did the same through diff.external and diff.<driver>.command / textconv (selected by .gitattributes or .git/info/attributes). Raw claude has no allowlist, so no new exposure there; the allowlist narrowed the tools but left a repo-controlled exec path inside Loki's own trust gate.
1382
- - Law broken: the repo under review is untrusted input (safe_git.ts FC-25); the evidence a reviewer reads must not be producible or mutable by the code under review.
1383
- - Cause (measured): safe_git hardens only Loki's own git calls (argv `-c` flags, blanked driver keys). A reviewer model builds its own git argv, and REVIEW_ALLOWLIST_TOKEN grants `Bash(git ...:*)` with no flag restriction, so nothing hardened the child's git. The only lever that reaches a model-built command is the child's env.
1384
- - Siblings swept (every spawn of claude or another provider CLI that can run git):
1385
- - council voters and devil's advocate, loki-ts/src/council/voter_agents.ts defaultClaudeRunner: the only TS consumer of reviewAllowlistArgv. Fixed.
1386
- - quality_gates code review, loki-ts/src/runner/quality_gates.ts (structured and text claude spawns): denylist only, no allowlist, but Bash is otherwise open so git status/diff is reachable. Fixed with the same env.
1387
- - bash reviewer dispatch, autonomy/run.sh _dispatch_reviewer claude arm (the other consumer of loki_review_allowlist, three spawn sites): fixed through `loki internal reviewer-git-env`, fail-closed (no env, no allowlisted review).
1388
- - run.sh codex, cline, aider and opencode reviewer arms: no shared git allowlist (that token is a claude CLI flag); their reviewers get a diff in the prompt. Not changed, recorded here as swept.
1389
- - engine10 stages xreview.ts and deep.ts council session (providers.ts sessions): different mechanism (read-only disallowedTools on a session that already has exec); owned by the FC-XREVIEW-BLIND-TOOLS row and another engineer, not touched.
1390
- - autonomy/grill.sh and engine10 worker sessions: no review allowlist; worker sessions already have full exec by design.
1391
- - Mechanism: one helper, `reviewerGitEnv` in loki-ts/src/util/safe_git.ts, reusing driverKeys (the DRIVER_KEY_RE enumeration). It sets GIT_CONFIG_NOSYSTEM and command-scope GIT_CONFIG_COUNT entries (core.fsmonitor blank, core.hooksPath and core.attributesFile /dev/null, core.pager cat, protocol.ext.allow never, every repo filter and textconv key blanked, every diff.<driver>.command pointed at a plain `diff -u` shim) and replaces GIT_EXTERNAL_DIFF with the same shim. Route chosen over dropping git from the allowlist: council prompts and reviewers rely on git diff and git show to read the change, and removing them would blind the reviewers (the FC-XREVIEW-BLIND-TOOLS class); env hardening keeps the capability and closes the exec path. A blank diff.external or driver command makes git die ("external diff died"), hence the shim instead of a blank.
1392
- - Fixture: loki-ts/tests/util/safe_git_extdiff.test.ts describe "FC-REVIEWER-GIT-CONFIG-EXEC reviewerGitEnv" (control: plain git fires fsmonitor, diff.external and the driver command; with the real builder none fire and status, diff, show -p and log -p still return real patches; wiring check on both TS spawns). Mutation-checked: dropping the driver-key enumeration, the fsmonitor entry or the GIT_EXTERNAL_DIFF override each turns the fixture red.
1393
- - Round 2 (HIGH review block): the same mechanism now also covers gpg exec (log.showSignature=false and gpg.program, gpg.openpgp.program, gpg.ssh.program, gpg.x509.program set to false), partial-clone lazy fetch firing core.sshCommand (GIT_NO_LAZY_FETCH=1 plus repo network command keys enumerated with NET_CMD_KEY_RE and blanked), a shim that ends options with `--` so a file named `-n` is a path, textconv blanked to `cat` (a blank one dies on log -p), and GIT_PAGER=cat. reviewerGitEnv takes a required base, and the two TS reviewer spawns pass tokenFreeEnv(process.env); full_env_spawn_guard.test.ts flags a reviewerGitEnv call with no base or a non-token-free base in the spawning reviewers. Fixtures in safe_git_extdiff.test.ts (gpg, lazy fetch, `-n`, textconv) and the guard test; each mutation (drop gpg entries, drop GIT_NO_LAZY_FETCH, drop `--`, textconv to blank, non-token-free base) turns its fixture red.
1394
- - Round 3: remote.<n>.uploadpack/receivepack are first-value-wins, so a repo-local value beats an env blank. reviewerGitEnv therefore throws when the repo sets either key, and throws on a partial clone (extensions.partialClone) when git is older than 2.44 (GIT_NO_LAZY_FETCH is ignored there); both fixtures are mutation-checked. The spawn guard now requires tokenFreeEnv( on every reviewerGitEnv call (per call, not per file); the only raw process.env base is the cli.ts reviewer-git-env print, an explicit file-and-line row that fails the guard when stale.
1395
-
1396
- ## FC-TRUSTED-PUSH-PARTIAL-CLONE The trusted push fetches the run branch out of a partial (--filter=blob:none) agent clone into a temp repo, and upload-pack dies serving objects the clone never downloaded, so a VERIFIED run ends with no PR
1397
- - User-visible vs raw: user-visible. A VERIFIED loki 11.17.100 run on anthropics/claude-code#81826 (clone made with --filter=blob:none) ended `engine10-push: push to fork asklokesh/claude-code refused or failed (rc=1)` and opened no PR.
1398
- - Law broken: a verified result must reach the user on every supported checkout shape (full, shallow, partial); a push helper must not depend on the agent repo being able to serve every reachable object.
1399
- - Cause (measured): `_loki_trusted_push` fetches from the agent repo path into a fresh template-less repo. On a partial clone upload-pack runs pack-objects over history whose blobs were never downloaded and modern git sets no lazy fetch there: `git-pack-objects died with error ... possible repository corruption on the remote side`.
1400
- - Siblings swept: autonomy/lib/engine10-push.sh `_e10_local_push` (same fetch-from-path into a temp repo, left unchanged on purpose: its pinned origin is a local path, so a lazy blob fetch would read the operator's own local repo, which holds every object, and there is no remote network credential to seed; the partial-clone failure is a remote-origin shape); autonomy/lib/engine10-push-gitlab.sh (same fetch from "$dir"; fixed with the same seeding, kept logically equivalent in the child because it is a standalone script that cannot source run.sh, and tests/test-engine10-push-gitlab.sh runs it that way); loki-ts/src/runner/attempts.ts only calls the shell push and holds no copy. No other fetch or clone from the agent repo path exists in autonomy/ or loki-ts/src.
1401
- - Mechanism: in `_loki_trusted_push`, when the agent repo is partial (extensions.partialclone non-empty, or any remote.<n>.promisor true/yes/on/1, both read as data), the temp repo is seeded with the validated origin URL (captured before a fork destination overwrites it) as promisor remote `loki-base`, and its refs are fetched with --filter=blob:none under the credential runner. The agent pack is thin (deltas against blobs the temp repo knows only by id), so the client lazy-fetches those delta bases from loki-base during the agent fetch; that fetch therefore also runs under the runner, with the agent-dir upload-pack (the only process that reads agent config) started through `--upload-pack '/usr/bin/env -i PATH=/usr/bin:/bin GIT_NO_LAZY_FETCH=1 git upload-pack'`, so no token, helper config, HOME or lazy fetch reaches it. Option chosen over prefetching delta-base blobs (git may pick any blob in the edge trees as a base, so an enumeration is a heuristic that fails closed only on private origins and costs a tree walk on large repos) and over an unfiltered seed (downloads full history). GIT_TERMINAL_PROMPT=0 on both fetches. Non-partial repos take the old path with no extra network or runner. A failed seeding or agent fetch fails closed.
1402
- - Fixture: tests/test-engine10-push-gitlab.sh (partial-clone block, red before with "could not stage branch", green after) and tests/test-trusted-push-agent-config.sh "FC-TRUSTED-PUSH-PARTIAL-CLONE" block (a real blob:none clone of a local origin with a history-only blob; pushes the agent branch; asserts the non-partial path never writes remote.loki-base config). Red before (rc=1, no push), green after. Review round 1 (HIGH, reproduced): the agent fetch ran token-less, so a private origin refused the lazy delta-base fetch; both fixtures now add a 20000-line file edited by the agent, an origin that refuses token-less reads through a packObjectsHook in the test global config, and a spy on the agent-dir upload-pack environment. Red on 95bf13b9a for the GitHub path (rc=1, nothing pushed); the GitLab child holds its token process-wide, so only its env-scrub assertion was red there.
1403
-
1404
- ## FC-FULL-SUITE-EXIT-CODED-SCRIPT A project-model full-suite command that is a plain CI checker script (not a test framework) prints no runner summary, so verify recorded it not_run UNMEASURED and a proven change sealed PARTIAL
1405
- - User-visible vs raw: user-visible. claude-code#81826 on v11.17.101 sealed PARTIAL although the Wall passed with n=7; Not-proven listed "one reviewer", "not run: full suite: repo-root" and "executed count unmeasured".
1406
- - Law broken: a run seals VERIFIED unless it genuinely cannot be proven; a harness-side parse gap on a command that has no summary to parse must not hold back a proven change.
1407
- - Cause (measured): the model gave package repo-root the test command `python3 .github/scripts/check_workflow_hardening.py`; it printed "OK: checked 3 job(s) ..." and exited 0; classifyCheck (kind test) needs a parsed count, ran() returned null, result not_run. crossReview returns null for any verdict other than VERIFIED or ALREADY_SATISFIED, so "one reviewer" stayed uncredited only because the run was already PARTIAL.
1408
- - Siblings swept: runFullSuite legacy per-runner path (commands come from FULL_SUITE_CMD, always frameworks, unchanged); base_guard and wall use real runners (unchanged).
1409
- - Mechanism: check_result.ts isExitCodedScript (every segment a plain interpreter-plus-script run, nothing naming a test framework or containing "test"; anything ambiguous is a framework) feeds classifyCheck exitCoded: pass on rc 0, fail on rc != 0, count "n/a (exit-coded check, not a test runner)", never an n, so it is never executed proof. A framework with no summary stays UNMEASURED. FC-16 on the target is unchanged.
1410
- - Fixture: tests/util/check_result.test.ts and tests/engine10/monorepo_cwd.test.ts (red before: missing export), tests/engine10/seal.test.ts (Wall n>0 plus exit-coded full suite is VERIFIED with one reviewer credited by a real review).
1411
-
1412
- ## FC-UNOWNED-FILE-OWNED-BY-TESTLESS-PACKAGE A changed file no package owns made every unrelated test-less package own the diff, so a proven run sealed PARTIAL
1413
- - User-visible vs raw: user-visible. claude-code#81826 on 11.17.102 sealed PARTIAL with "full suite: package scripts defines no test command" although the diff touched only plugins/ralph-wiggum/{hooks/stop-hook.sh,scripts/setup-ralph-loop.sh} and the passing Wall (n=4) executed both scripts. Run e10-20261019T191337Z-08db.
1414
- - Law: ownership is positive evidence, never absence of an owner; an unowned file is proven or named, never silently passed or silently blamed on an unrelated package.
1415
- - Cause (measured): runPackageSuites `owns` returned true for any test-less package whenever any product file had no owning package.
1416
- - Siblings swept: execProves (reused as the proof), the proofFiles credit, quoted and unresolved paths (quoted still owned by all, fail-safe), FC-WALL-FILE-OWNS-EVERY-PACKAGE.
1417
- - Mechanism: a test-less package owns an unowned file only if it is the repo-root package. Unowned product files are proven by execProves (Wall red on base, green on head, whole product diff unowned); otherwise one NOT PROVEN note "changed file <f> belongs to no package and no passing check exercised it".
1418
- - Fixture: loki-ts/tests/engine10/monorepo_cwd.test.ts tests FC-UNOWNED-FILE-OWNED-BY-TESTLESS-PACKAGE. Red on 00bc53d72 (5 fail), green after.
1419
-
1420
- ## FC-UNIT-BASE-IS-NOT-THE-JOURNAL-BASE A group unit started at origin/HEAD while the slice journal and resume key named local HEAD, so a receipt could be adopted for a base it never ran on
1421
- - User-visible vs raw: user-visible. A fresh group run skipped a unit as "reused from the slice journal" although the unit's work was built on a different commit than the group's base; or a valid receipt was refused. Raw: backlog.py:199 gave a non-fix unit base "", worktree_prep.py _start_point then ran git fetch and started it at origin/HEAD, while run_group keyed the journal on baseRef (local HEAD).
1422
- - Law: a resume or adoption key names the base the work actually ran on; one base per unit, derived once.
1423
- - Cause (measured): only fix units carried an explicit base (fix_units.ts); non-fix units inherited an implicit one.
1424
- - Siblings swept: fix_units.ts (already explicit, group tip), backlog.py ref-to-SHA resolution (reused), worktree_prep._start_point (explicit base returned untouched, no fetch), integrate (starts the group branch at baseRef), engine10 intake baseSha (single run, rev-parse HEAD), autonomy/loki origin/HEAD readers (not unit bases).
1425
- - Mechanism: run_group sets base = baseRef on every non-fix unit before writing dag.json, so dispatch and the journal key use the same value. The adoption is further bound to the signed task sha256, the unit branch tip and the verified receipt. No time bound is needed: a key that names the base and card never matches work built on another base, and a moved base misses.
1426
- - Fixture: loki-ts/tests/engine10/seal_group.test.ts "#317: local HEAD ahead of origin/HEAD, every unit starts at the journal-key base". Red before the fix (unit base undefined), green after.
1427
-
1428
- ## FC-TRUSTED-CACHE-VERDICT fast_verify trusts an agent-writable content-hash cache as a fresh scan, so a planted entry (an empty list suppressing a high finding, or injected finding text) decides the verdict
1429
- - User-visible vs raw: user-visible. A planted `.loki/cache/fast-verify.json` entry keyed by a file's content hash is returned as that file's findings without rescanning. `[]` makes a file with a high finding PASS; a crafted message reaches the MCP client as trusted tool output. Closed by #319.
1430
- - Law: a verdict is derived from the content being judged, never from a file the judged agent can write (EL exogenous gates).
1431
- - Cause (measured): autonomy/lib/fast_verify.py Cache reads entries by blake2b(content) and returns them unvalidated; mcp/server.py listed loki_verify_fast in _TRUSTED_TOOLS so its finding text skipped the untrusted envelope.
1432
- - Siblings swept (grep for cache files under .loki and ~/.loki in loki-ts/src, autonomy, mcp): engine10/cache.ts repomap/testmap cache lives under ~/.loki/cache/v10, outside the repo, keyed by tree hash, no MAC (same-user write is out of this row's threat model; it selects tests, it does not return findings); run.sh `.loki/cache/install` is in-repo and agent-writable but stores node_modules, not a verdict; router history and update_check feed estimates and notices, not a verdict. No other in-repo cache returns findings. Every MCP tool and resource is covered by the runtime-derived guard in tests/mcp/test_phase1_tools.py.
1433
- - Mechanism: two layers. (1) loki_verify_fast findings sit under untrusted_content via _UNTRUSTED_FIELDS. (2) fast_verify.Cache lives at ~/.loki/cache/fast-verify/<realpath hash>.json (LOKI_FAST_VERIFY_CACHE_DIR overrides), and each entry carries an HMAC under a per-user 0600 key beside it. The cache key and the MAC body are derived from everything the verdict depends on: a producer fingerprint (blake2b of fast_verify.py's own bytes, computed at import, so any rule edit orphans every old entry and there is no hand-bumped schema constant), the repo-relative path (is_test decides which rules run, so a file moved from a test path to a product path is rescanned), and the content. An entry whose MAC fails is a miss and the file is rescanned; with no readable key, no readable source or an unusable cache dir the cache is off. The key is created through a 0600 temp file and os.link (never clobbered, never partial), the cache dir is forced to 0700 even when it pre-exists loose, a key that is group or other accessible or not owned by the user is refused, and the cache json is written at 0600 through a temp file and os.replace. The old repo-local `.loki/cache/fast-verify.json` is never read. Closes #319.
1434
- - Trust boundary: closed for repo content and for any write the judged agent makes inside the repo. NOT closed against a same-user process that can read ~/.loki/cache/fast-verify/key: it can forge a valid MAC. A same-user agent that is allowed to read the home directory therefore defeats this cache; the only complete answer for that threat is --no-cache (the fast scan is milliseconds), which the exogenous gates should use.
1435
- - Sibling (engine10 cache): closed by #331, see FC-ENGINE10-CACHE-KEY-NOT-PRODUCER-BOUND.
1436
- - Fixture: tests/mcp/test_phase1_tools.py test_stored_text_appears_only_inside_untrusted_content (red with loki_verify_fast trusted: leaks ['loki_verify_fast']; green after) and tests/test-fast-verify.sh CACHE BINDING block (planted repo-local and forged-MAC empty-list entries; red when the MAC check is replaced by `if False:`, green with it), plus the CACHE KEY and CACHE FILES blocks there: a test-path file moved to a product path must FAIL (red when the path is dropped from cache_key), a copy of fast_verify.py with disabled rules must not poison the real rules' cache (red when PRODUCER is a fixed string), the key and cache json are 0600 and the dir 0700 (red when the key is written 0666), and no verify cache file appears in a repo (red when cache_path points at root/.loki/cache/fast-verify.json). Tests that run fast_verify set LOKI_FAST_VERIFY_CACHE_DIR to a run tmp or pass --no-cache, never the real ~/.loki.
1437
-
1438
- - Recurrence 2026-10-23 (RQ-102, founder-reported): all 12 release runs from v11.66.1 to v11.67.9 failed publish-npm on the 8x15s settle loop of scripts/npm-dist-tags.sh ("dist-tags did not settle on the highest version after 8 tries", run 39938698648), and verify-npm-registry has `needs: publish-npm`, so a failed publish job SKIPPED the only converge step. At 12:58Z latest=11.67.8 next=11.67.9 (npm dist-tag ls and the uncached packument agree; packument modified 10:29:12Z, a late 11.67.8 latest write with no re-assert for 2.5 h). Cause: the settle window (about 2 min) is shorter than the measured registry lag (4 to 11 min), and the converge job was gated on the very step that fails. Same shape for publish-ts-sdk and verify-sdk-registry.
1439
- - Mechanism (one owner, D86): publish-npm owns the upload and one best-effort tag write (LOKI_DISTTAG_TRIES=2, failure is a ::warning, an upload failure stays fatal). verify-npm-registry owns convergence and the red signal: `if: !cancelled() && needs.publish-npm.result != 'skipped'` (a cancelled run stops it; a skipped publish uploaded nothing), it first proves the registry serves the version, then converges with LOKI_DISTTAG_TRIES=50 and LOKI_DISTTAG_SLEEP=15 (at least 12.5 min). Red only if the tags never settle. The SDK pair has the same shape. npm-dist-tags.sh and its never-below-max logic are unchanged.
1440
- - Fixture: tests/test-npm-dist-tags.sh structural cases on .github/workflows/release.yml (verify jobs gate on !cancelled() and not skipped, with always() absent; publish tag step is non-fatal; converge window at least 720 s and timeout at least 40). Mutation: each of these goes red alone: `needs`-only gating, always() gating, a fatal tag call, a dropped TRIES, a short timeout, on both the npm and SDK pairs.
1441
- - Recurrence 2026-10-23 (RQ-103, rev-b N2): npm-dist-tags.sh exits at the first settle (two reads 15 s apart), so a late lower write the registry applies after that exit stays until the next release (the 2.5 h stuck latest). Mechanism (same one, scheduled): .github/workflows/converge-dist-tags.yml runs every 15 min for loki-mode and loki-mode-sdk and only calls scripts/npm-dist-tags.sh (no tag logic of its own); it passes the script the version the registry serves under latest (never the VERSION file, so a release in flight is neither the target nor a reason to skip) and fails red when that read is empty. A separate file because release.yml triggers every publish job and has no scheduled entry point; no other workflow owns a periodic converge. Fixture: the RQ-103 block in tests/test-npm-dist-tags.sh (schedule, both packages, and a behavioral run of the extracted converge step against the stub registry with next below latest and an unpublished VERSION); mutations that each go red: schedule removed, a package dropped, the script call commented out, the read guard inverted or forced, the old VERSION-arg skip form.
1442
-
1443
- ## FC-CATALOG-CAPABILITY-DRIFT-UNCHECKED A catalog entry whose capabilities drifted from the API failed only at request time
1444
- - User-visible: a catalog entry whose thinking types or effort levels drifted from what the model accepts fails only at request time, as an API error mid-run. Raw: the catalog (providers/model_catalog.json) is static and never compared with models.retrieve.
1445
- - Law broken: a capability the harness relies on is claimed without being checked against its source of truth.
1446
- - Siblings swept: effortForTier clamp (bash and Bun, parity-checked), claudeEffortLevels, the catalog freshness row (age only, offline).
1447
- - Mechanism: loki doctor "Model capabilities" row. With ANTHROPIC_API_KEY it calls models.retrieve per claude catalog id and prints each mismatch (effort levels, API-expressible thinking types); without a key or offline it warns and never changes the tally or exit code. The bench runner takes --effort sweep, expanding to the catalog levels for --model.
1448
- - Fixture: loki-ts/tests/commands/doctor.test.ts "model capability row", tests/test_bench_runner.py sweep tests, tests/test-bash-bun-parity.sh effort clamp cells. Red when the comparison, the sweep catalog read or the clamp model argument is removed.
1449
-
1450
- - Mechanism: run_group sets base = baseRef on every non-fix unit before writing dag.json, so dispatch and the journal key use the same value. The adoption is further bound to the signed task sha256, the unit branch tip and the verified receipt. #317b: task.sha256 covers items only, so the receipt also signs unit_spec.sha256 (unitSpecSha256 in unit_mode.ts over id, items, acceptance, writeSet, base), written by the seal stage and recomputed by adoptJournaled from the same function; an absent, empty or mismatched value rejects the adoption. No time bound is needed: a key that names the base and card never matches work built on another base, and a moved base misses.
1451
- - Fixture: loki-ts/tests/engine10/seal_group.test.ts "#317: local HEAD ahead of origin/HEAD, every unit starts at the journal-key base". Red before the fix (unit base undefined), green after. #317b fixtures: "a colliding key whose signed unit spec has a different acceptance is not adopted" and "a receipt with no signed unit_spec is not adopted", red with the spec check removed or acceptance left out of the hash.
1452
-
1453
- ## FC-TEST-TIMEOUT-OVERRIDES-CLI A file-level setDefaultTimeout(30_000) overrides the bun test --timeout flag and ignores machine load, so a slow-but-correct test fails on a busy host
1454
- - User-visible vs raw: user-visible to every engineer and release gate. `runAutonomous > repo profile cache hit` failed at 30.1s on origin/main at load average 28 on 14 cores, both with `--timeout 30000` and with `--timeout 120000`. The same test passed with the literal raised to 240_000 (87s wall, including startup).
1455
- - Law: a test's time budget scales with the load of the machine it runs on, and the operator has an env knob that always works; a flake is a missing scale, not a reason to retry. bun test does not expose `--timeout` to the process (argv, Bun.argv and execArgv were probed under `bun test --timeout 777777`: argv=[bun, file]), so the knob is an env var, not the CLI flag.
1456
- - Cause (measured): 14 test files call `setDefaultTimeout(N)` with a fixed literal (10 at 30_000, 3 at 130_000, 1 at 60_000; count: `git grep -n 'setLoadAwareTimeout(' -- loki-ts/tests` after the change, `git grep -n 'setDefaultTimeout(' fff00b1da^ -- loki-ts/tests` before), and a file-level call wins over the CLI flag.
1457
- - Siblings swept: all 14 literals in loki-ts/tests, replaced in one change; per-test `, N)` timeouts are explicit single-test budgets and are not in this class.
1458
- - Mechanism: loki-ts/tests/preload.ts exports `setLoadAwareTimeout(baseMs)`: baseMs times clamp(ceil(2 * loadavg1 / cpus), 1, 10). `LOKI_TEST_TIMEOUT_SCALE` (positive number, capped at 10) replaces the factor; `LOKI_TEST_TIMEOUT_MS` is a floor that wins when larger. A guard in loki-ts/tests/util fails on any direct `setDefaultTimeout(` call in tests.
1459
- - Fixture: loki-ts/tests/util/test_timeout_guard.test.ts tests FC-TEST-TIMEOUT-OVERRIDES-CLI, including a subprocess run through the real setLoadAwareTimeout path (env knob lifts the budget, a real hang still times out).
1460
-
1461
- - Siblings swept: every registered MCP tool and resource, via the runtime-derived guard in tests/mcp/test_phase1_tools.py (seeds this cache with the marker; the observer now also sees os.scandir, os.stat, Path.iterdir and glob); the JS mirror declares its fields in src/protocols and counts them in tests/protocols/mcp-server.test.js.
1462
- - Mechanism: part 1 (this change) moves loki_verify_fast findings under untrusted_content via _UNTRUSTED_FIELDS. The verdict-suppression half is open in #319 (cache entries must be authenticated or re-derived, then the row is closed).
1463
- - Fixture: tests/mcp/test_phase1_tools.py test_stored_text_appears_only_inside_untrusted_content (red with loki_verify_fast trusted: leaks ['loki_verify_fast']; green after).
1464
-
1465
- ## FC-STATE-MANAGER-ROOT-IGNORED A process-wide state manager singleton ignored its lokiDir after first use, and a per-root cache would grow without bound
1466
- - User-visible vs raw: user-visible. getStateManager({ lokiDir }) for a second .loki directory returned the first directory's manager, so reads and writes for run B landed in run A's state; a long-lived process serving many roots would have leaked one open watcher per root once keyed naively.
1467
- - Law: a keyed accessor honours its key on every call; a cache that holds OS resources is bounded and releases what it evicts.
1468
- - Cause (measured): state/manager.ts kept one module-level defaultManager and created it from the first call's options only.
1469
- - Siblings swept: state/manager.py get_state_manager has the same single-default shape and is to follow the same keying rule (separate branch); no other TS caller passes arguments to getStateManager.
1470
- - Mechanism: one per-root cache in state/manager.ts keyed by the (st_dev, st_ino) of the .loki directory (symlinks and case spellings share a manager), capped by LOKI_STATE_MANAGER_MAX (default 8), least recently used evicted and stop()ped.
1471
- - Fixture: loki-ts/tests/state_manager.test.ts (under loki-ts so bun and CI discover it). Red on origin/main (2 fail), green after (5 pass or 4 pass and 1 skip on a case-sensitive volume); mutations: no eviction, eviction without stop, path-string key, constant key, no LRU touch each go red.
1472
- - Python half (RQ-35): state/manager.py get_state_manager keyed per realpath but with no bound. Now an OrderedDict LRU capped by LOKI_STATE_MANAGER_MAX (default 8), evicted managers stop()ped outside the lock. Fixture: state/test_manager.py TestDefaultManagerCap (red before: 3 fail; mutant skipping stop() on eviction: 2 fail).
1473
-
1474
- ## FC-ENGINE10-CACHE-KEY-NOT-PRODUCER-BOUND engine10 repomap and testmap cache keyed by tree hash plus a hand-bumped prefix, so a builder change kept serving old maps, and cache files were written with default modes
1475
- - User-visible vs raw: user-visible. After a repomap or testmap builder change, test selection and repo context kept using the previous builder's output until someone bumped `repomap2-` by hand. Closed by #331.
1476
- - Law: a cached result is derived from everything it depends on (producer, path, content), never from a name a person must remember to bump (EL exogenous gates).
1477
- - Cause (measured, loki-ts/src/engine10/cache.ts): the file name was `repomap2-<tree>.json` / `testmap-<tree>.json`; `writeJson` used writeFileSync with default modes and did not set the dir mode.
1478
- - Siblings swept: fast_verify.Cache (closed in #319, FC-TRUSTED-CACHE-VERDICT), run.sh `.loki/cache/install` (node_modules, not a verdict), router history and update_check (estimates and notices). The #283 slice journal shares writeJson and so is now private and atomic too.
1479
- - Mechanism: one producer fingerprint, `producerFingerprint()` in cache.ts: sha256 over the path and bytes of every .ts file under loki-ts/src. scripts/build.ts generates it into the bundle (`globalThis.__LOKI_PRODUCER_FP__`, since the bundle has no source path to hash); a source run hashes src/ itself; if neither works it is a per-process random value so nothing stored can match (a cache that cannot name its producer misses). It sits in the file name next to the tree hash and the `repomap2-` prefix is gone. `writeJson` forces the dir to 0700 (also when it pre-existed loose), writes a 0600 temp file and renames it into place, removing the temp file on failure. Whole-tree hashing is deliberate: the builders import broadly, and an unrelated source edit costing one rebuild is cheaper than a hand-picked list that drifts. Old `repomap2-` and unfingerprinted files are never read and age out only if the same prefix is written; they are small and private.
1480
- - Trust boundary: this cache selects tests and context; it does not return findings. A same-user process that can write ~/.loki/cache/v10 can still plant a map, and nothing here authenticates entries (no MAC). Out of scope; the only complete answer is not using a cache for a gate.
1481
- - Fixture: loki-ts/tests/engine10/cache.test.ts describe "#331 producer-bound, private, atomic cache": one changed byte of a copied builder changes the fingerprint; a second build under a different fingerprint misses and the same one hits; the key carries the fingerprint with no hand-bumped prefix; dir 0700 and files 0600 including a pre-existing 0777 dir; temp file plus rename (inode replaced, no .tmp left after success or failure). Mutations each go red: key without the fingerprint (3 fail), files written 0666 (1 fail), dir chmod removed (1 fail), in-place write instead of rename (1 fail). Tests use an injected tmp cache root; tests/engine10/intake.test.ts runs under a fake HOME.
1482
- - Review round 2 (rev-c M5, M6): `computeProducerFingerprint(root, injected)` is the testable core of `producerFingerprint()`. Tests in cache.test.ts "unbound fallback and injected value": separate processes get different 64-hex keys when the source cannot be hashed and a map written under one is a miss under the other (red when the fallback is a constant, M5); a malformed injected value ("", "x", wrong length, non-string) is ignored for the source hash (red when the 64-hex check is dropped, M6). Orphans: the old `repomap2-*.json` files are removed on the first repomap write (red when that prune is removed, M7); an old `repomap-<tree>.json` name from before binding is ignored and ages out under the 5-file prune.
1483
-
1484
- - Fixture: tests/mcp/test_phase1_tools.py test_stored_text_appears_only_inside_untrusted_content (red with loki_verify_fast trusted: leaks ['loki_verify_fast']; green after).
1485
-
1486
-
1487
- ## FC-RELEASE-GREEN-WITHOUT-REGISTRY-PROOF A green publish-npm job proved only that npm accepted the upload, not that the registry served the version
1488
- - User-visible vs raw: user-visible. Run 39852427194 printed "+ loki-mode@11.50.0" at 20:52:55Z and "+latest" at 20:52:57Z, but the registry did not serve 11.50.0 until about 21:04Z. A consumer installing in that window got the old version from a release reported green.
1489
- - Law: a release is green on registry proof, not on a client's upload log; the proof is a read of the registry.
1490
- - Cause (measured): the registry applies a publish asynchronously. The registry's own time field for 11.53.0, 11.53.1 and 11.54.0 is 4m10s, 5m13s and 6m12s after the workflow's "+ loki-mode@" line.
1491
- - Siblings swept: publish-ts-sdk and publish-python-sdk (same upload-log-only pattern; not changed here, listed as follow-up), npm-dist-tags.sh (re-reads tags after writing).
1492
- - Mechanism: job verify-npm-registry (needs publish-npm only, no concurrency group, nothing needs it) polls `npm view loki-mode@VERSION version --prefer-online` for about 15 minutes and fails red if it never resolves. It cannot delay docker, homebrew, tag or release, nor hold the release-npm lock.
1493
- - Fixture: tests/test_release_next_tag.py test_registry_proof_is_its_own_job_and_nothing_waits_on_it. Red when the job is renamed or removed (mutant run), green after.
1494
-
1495
- ## FC-RELEASE-NOTES-FORMAT-UNCHECKED-LOCALLY release.sh --ship wrote a CHANGELOG section CI's release-notes.sh rejects, so the failure surfaced only after npm had published and the tag and GitHub release were skipped
1496
- - User-visible vs raw: user-visible. v11.53.0, v11.53.1 and v11.54.0 were on npm with no tag and no GitHub release: "section ... is not fully written (no '### ' subsection)".
1497
- - Law: a local release gate runs the same validators CI runs before the commit and push; no validator that can block a downstream channel runs only after an irreversible publish.
1498
- - Cause (measured): ship_write_changelog copied the --notes file verbatim and only compared the header line; scripts/release-notes.sh ran first in CI's release job.
1499
- - Siblings swept: the global guard rows (scripts/global-guards.tsv) already run locally; the pre-push hook's release-notes gate checks the tree, not the --notes file before the write.
1500
- - Mechanism: run_ship calls ship_check_notes_format (scripts/release-notes.sh on the notes file) in preflight, before any write, and again on the written CHANGELOG.md; both refuse with SHIP_NOTES_FORMAT (exit 4). LOKI_RELEASE_SKIP_GUARDS does not reach it.
1501
- - Fixture: tests/test-release-sh.sh T17d (flat notes with LOKI_RELEASE_SKIP_GUARDS=1). Red on origin/main's release.sh (exit 0, VERSION bumped, pushed), green after.
1502
-
1503
- ## FC-DIST-TAG-MOVED-BACKWARDS Concurrent back-to-back releases left npm latest below the newest version
1504
- - User-visible vs raw: user-visible. After 11.53.0, 11.53.1 and 11.54.0 shipped within 8 minutes, dist-tags read latest=11.53.1 and next=11.54.0, so `npm i loki-mode` installed an older release than the newest.
1505
- - Law: a dist-tag only ever moves to the highest published version, and the final state is read back, not assumed from the write.
1506
- - Cause (measured, partly inferred): publish-npm logs show the writes in order (11.53.1 "+latest" 23:31:01Z, 11.54.0 "+latest" 23:34:42Z), so the runs did not run out of order and the lagging CUR read could only cause a skip, not a backwards move. The registry's time field shows 11.53.1 became served at 23:36:13Z, after 11.54.0's tag write; the observed latest=11.53.1 is consistent with the registry applying 11.53.1's tag write late. That mechanism is inferred from timestamps, not directly observed.
1507
- - Siblings swept: publish-ts-sdk had the same CUR-vs-VERSION compare for loki-mode-sdk; fixed in slice pd/sdktags, it now calls scripts/npm-dist-tags.sh with LOKI_DISTTAG_PKG=loki-mode-sdk (no second copy of the logic). publish-python-sdk has no dist-tag step and needs none: PyPI has no tags and orders releases by version, so a late upload of a lower version cannot become the latest the installer resolves (twine --skip-existing keeps reruns idempotent). The CoS-dispatched force_republish run 39898474372 re-asserted 11.54.0 at 04:06:43Z.
1508
- - Mechanism: scripts/npm-dist-tags.sh targets max(VERSION, latest, next, every published stable version), never lower; it re-reads both tags after writing and again after a pause, re-asserting the target until both settle or the tries run out (red). Every read (latest, next, published versions) must succeed and be non-empty before any write; an error or empty answer refuses with "refusing to move latest (fail closed)" and zero writes (review rev-a: an outage otherwise made VERSION the only candidate).
1509
- - Fixture: tests/test-npm-dist-tags.sh (7 cases incl. late overtaking write restored and view outage error/empty with zero writes). Mutants (ignore tags, no re-read, target=VERSION, outage guard removed) each fail a case.
1510
-
1511
- - Mechanism: scripts/npm-dist-tags.sh targets max(VERSION, latest, next, every published stable version), never lower than its own reads; a per-tag read above the target (another run moved it) or an empty one re-reads and writes nothing, and npm has no compare-and-set so a window of milliseconds between the last read and the write remains; it re-reads both tags after writing and again after a pause, re-asserting the target until both settle or the tries run out (red). Every read (latest, next, published versions) must succeed and be non-empty before any write; an error or empty answer refuses with "refusing to move latest (fail closed)" and zero writes (review rev-a: an outage otherwise made VERSION the only candidate).
1512
- - Fixture: tests/test-npm-dist-tags.sh (8 cases incl. a fresh-higher read never written backwards; incl. late overtaking write restored and view outage error/empty with zero writes). Mutants (ignore tags, no re-read, target=VERSION, outage guard removed) each fail a case.
1513
-
1514
- - Late materialization (recurrence 2026-10-23): the registry applies a publish 3 to 6 minutes after the client prints "+ loki-mode@X" (registry time field: 11.53.0 +4m10s, 11.53.1 +5m13s, 11.54.0 +6m12s). 11.54.1 uploaded 04:34:24Z, served 04:39:34Z; 11.54.2 uploaded 04:38:07Z, served 04:41:17Z, leaving latest=11.54.1 next=11.54.2. The 15s settle in the script is far shorter than the lag, so a write made in publish-npm can be overtaken after it settles (mechanism inferred, not directly observed). Fix: verify-npm-registry (and verify-sdk-registry for loki-mode-sdk) run scripts/npm-dist-tags.sh again after the version resolves; the script is convergent (max stable, never lower), so it is safe outside the release-npm lock.
1515
- - Fixture: tests/test-npm-dist-tags.sh (9 cases incl. latest dragged down after the first settle restored by a second run, a settled converge run writing nothing, late overtaking write restored and view outage error/empty with zero writes). Mutants (ignore tags, no re-read, target=VERSION, outage guard removed) each fail a case.
1516
-
1517
- ## FC-CATALOG-CAPABILITY-DRIFT-UNCHECKED A catalog entry whose capabilities drifted from the API failed only at request time
1518
- - User-visible: a catalog entry whose thinking types or effort levels drifted from what the model accepts fails only at request time, as an API error mid-run. Raw: the catalog (providers/model_catalog.json) is static and never compared with models.retrieve.
1519
- - Law broken: a capability the harness relies on is claimed without being checked against its source of truth.
1520
- - Siblings swept: effortForTier clamp (bash and Bun, parity-checked), claudeEffortLevels, the catalog freshness row (age only, offline).
1521
- - Mechanism: loki doctor "Model capabilities" row. With ANTHROPIC_API_KEY it calls models.retrieve per claude catalog id and prints each mismatch (effort levels, API-expressible thinking types); without a key or offline it warns and never changes the tally or exit code. The bench runner takes --effort sweep, expanding to the catalog levels for --model.
1522
- - Fixture: loki-ts/tests/commands/doctor.test.ts "model capability row", tests/test_bench_runner.py sweep tests, tests/test-bash-bun-parity.sh effort clamp cells. Red when the comparison, the sweep catalog read or the clamp model argument is removed.
1523
-
1524
- - Mechanism: run_group sets base = baseRef on every non-fix unit before writing dag.json, so dispatch and the journal key use the same value. The adoption is further bound to the signed task sha256, the unit branch tip and the verified receipt. #317b: task.sha256 covers items only, so the receipt also signs unit_spec.sha256 (unitSpecSha256 in unit_mode.ts over id, items, acceptance, writeSet, base), written by the seal stage and recomputed by adoptJournaled from the same function; an absent, empty or mismatched value rejects the adoption. No time bound is needed: a key that names the base and card never matches work built on another base, and a moved base misses.
1525
- - Fixture: loki-ts/tests/engine10/seal_group.test.ts "#317: local HEAD ahead of origin/HEAD, every unit starts at the journal-key base". Red before the fix (unit base undefined), green after. #317b fixtures: "a colliding key whose signed unit spec has a different acceptance is not adopted" and "a receipt with no signed unit_spec is not adopted", red with the spec check removed or acceptance left out of the hash.
1526
-
1527
- ## FC-TEST-TIMEOUT-OVERRIDES-CLI A file-level setDefaultTimeout(30_000) overrides the bun test --timeout flag and ignores machine load, so a slow-but-correct test fails on a busy host
1528
- - User-visible vs raw: user-visible to every engineer and release gate. `runAutonomous > repo profile cache hit` failed at 30.1s on origin/main at load average 28 on 14 cores, both with `--timeout 30000` and with `--timeout 120000`. The same test passed with the literal raised to 240_000 (87s wall, including startup).
1529
- - Law: a test's time budget scales with the load of the machine it runs on, and the operator has an env knob that always works; a flake is a missing scale, not a reason to retry. bun test does not expose `--timeout` to the process (argv, Bun.argv and execArgv were probed under `bun test --timeout 777777`: argv=[bun, file]), so the knob is an env var, not the CLI flag.
1530
- - Cause (measured): 14 test files call `setDefaultTimeout(N)` with a fixed literal (10 at 30_000, 3 at 130_000, 1 at 60_000; count: `git grep -n 'setLoadAwareTimeout(' -- loki-ts/tests` after the change, `git grep -n 'setDefaultTimeout(' fff00b1da^ -- loki-ts/tests` before), and a file-level call wins over the CLI flag.
1531
- - Siblings swept: all 14 literals in loki-ts/tests, replaced in one change; per-test `, N)` timeouts are explicit single-test budgets and are not in this class.
1532
- - Mechanism: loki-ts/tests/preload.ts exports `setLoadAwareTimeout(baseMs)`: baseMs times clamp(ceil(2 * loadavg1 / cpus), 1, 10). `LOKI_TEST_TIMEOUT_SCALE` (positive number, capped at 10) replaces the factor; `LOKI_TEST_TIMEOUT_MS` is a floor that wins when larger. A guard in loki-ts/tests/util fails on any direct `setDefaultTimeout(` call in tests.
1533
- - Fixture: loki-ts/tests/util/test_timeout_guard.test.ts tests FC-TEST-TIMEOUT-OVERRIDES-CLI, including a subprocess run through the real setLoadAwareTimeout path (env knob lifts the budget, a real hang still times out).
1534
-
1535
- - Late materialization (recurrence 2026-10-23): the registry applies a publish 3 to 6 minutes after the client prints "+ loki-mode@X" (registry time field: 11.53.0 +4m10s, 11.53.1 +5m13s, 11.54.0 +6m12s). 11.54.1 uploaded 04:34:24Z, served 04:39:34Z; 11.54.2 uploaded 04:38:07Z, served 04:41:17Z, leaving latest=11.54.1 next=11.54.2. The 15s settle in the script is far shorter than the lag, so a write made in publish-npm can be overtaken after it settles (mechanism inferred, not directly observed). Fix: verify-npm-registry runs scripts/npm-dist-tags.sh again after loki-mode@VERSION resolves; the script is convergent (max stable, never lower), so it is safe outside the release-npm lock.
1536
- - Fixture: tests/test-npm-dist-tags.sh (9 cases incl. latest dragged down after the first settle restored by a second run, a settled converge run writing nothing, late overtaking write restored and view outage error/empty with zero writes). Mutants (ignore tags, no re-read, target=VERSION, outage guard removed) each fail a case.
1537
-
1538
- ## FC-THINKING-TYPE-ACCEPTED-BY-CLI-REJECTED-BY-API A thinking type the CLI accepted was a 400 for the model, and the usage error stated a rule the model did not have
1539
- - User-visible vs raw: user-visible. `loki internal sdk-judge|sdk-text --model claude-sonnet-5-5 --thinking disabled` at effort high or below was sent as `thinking: {type: "disabled"}`, which the API rejects with a 400 at every effort for that model (claude-api skill, Thinking & Effort table: Sonnet 5.5 "disabled returns 400; to turn thinking off send between_tools"). The judge failed closed and the bash caller fell back. Separately, the usage error "disabled cannot combine with --effort xhigh or max" was printed for claude-opus-5 and claude-haiku-5-5, which #314 clamps instead of refusing.
1540
- - Law: a request parameter is derived from the model catalog, never hand-written per model; a message never states a rule the catalog does not record.
1541
- - Cause (measured): #314 encoded only the effort ceiling (thinking.max_effort) and kept one model-blind sentence in two callers; the catalog had no way to say that a type is never valid for a model and what replaces it.
1542
- - Siblings swept: claude-opus-5-5, claude-fable-5-1 and claude-fable-5 list only adaptive in thinking.types, so disabled is a 400 at every effort for them (review of 37d4eb08d): claudeThinkingAccepted(model, type) is false for a type absent from types with no substitute, thinkingConflictsEffort refuses it at every effort, thinkingUsageError says "disabled is not accepted by <model>", and clampThinkingForEffort sends adaptive with a ledger record; claude-haiku-4-5 (no disabled substitute is invented), the max_effort clamp (still applied to the substituted type), thinkingConflictsEffort (a substituted model no longer conflicts), both callers of the usage error.
1543
- - Mechanism: catalog thinking.substitute maps a requested type to the type sent (claude-sonnet-5-5: disabled to between_tools). clampThinkingForEffort applies the substitute, then the max_effort clamp to the substituted type, and the ledger thinking_clamp records `disabled->between_tools@<effort> (max high)` or `disabled->adaptive@<effort> (max high)`. thinkingUsageError builds the sentence from the substitute and max_effort of the resolved model.
1544
- - Fixture: loki-ts/tests/providers/claude_flags.test.ts "thinking substitute and usage message (#314 follow-up)", loki-ts/tests/runner/sdk_invoker.test.ts "thinking substitute (#314)", tests/test-model-catalog-single-source.sh pins thinking.substitute. Three mutants (drop the catalog substitute, hand-write the message, clamp on the requested type) each turn them red.
1545
-
1546
- ## FC-PRINTED-COMMAND-REACHES-WRONG-ENGINE The two commands the product prints for a user (`loki start --resume`, `loki verify <receipt path>`) were routed to an engine that could not do the job, and reported success or a wrong verdict
1547
- - User-visible vs raw: user-visible. After a crash in implement, `loki start --resume` printed "phase implement is not auto-resumable ... start a new run", resumed nothing and exited 0 in 0.46s (the stage resume resumed the same crash in 20.8s through `loki "<task>" --resume`). `loki verify .loki/runs/<id>/receipt.json` on a VERIFIED run printed `VERDICT: CONCERNS` with the receipt path used as a git base ref ("Diff: 0 files"), where bare `loki verify` said VERIFIED.
1548
- - Law: a command the product tells the user to run reaches the engine that owns the artifact it names, and its exit code is the engine's, never a success the engine did not report.
1549
- - Cause (measured): the legacy start branch delegated `engine10 resume` (resumeMain, the reporter) instead of `engine10 --resume` (resumeFlag, the stage resume) and mapped its exit 4 to 0; cmd_verify routed only composite, group and deploy receipts to engine10, so a run receipt fell through to the diff verifier. tests/test-next-resume-agree.sh pinned `STUB-ARGV:engine10 resume` with a stub that exits 0, so it passed while the user path resumed nothing (a guard that passed its own mutation).
1550
- - Siblings swept: `loki resume` (the reporter, keeps its 0/4/5 map), `loki "<task>" --resume` (already resumeFlag), bare `loki verify` and `loki verify e10-*` (already v10 in bin/loki), `--pubkey` verify (already v10), a git ref and a non-receipt .json (stay on the diff verifier, pinned as controls).
1551
- - Mechanism: `start --resume` delegates `engine10 --resume [run-id]` and returns the code unchanged; cmd_verify sends `.loki/runs/e10-*/receipt.json` paths and any existing file whose schema starts `loki.v10.receipt` to `engine10 verify`.
1552
- - Exit 4 is shared: `start --resume` returns the engine code unchanged, and 4 is both "cannot resume" (for example a run whose base commit is gone) and EXIT.BLOCKED (engine10/output.ts), so a resumed run that ends BLOCKED also exits 4. A script cannot tell the two apart from the code alone; the printed line does. No doc may claim 4 means only "cannot resume". The codes are unchanged.
1553
- - Fixture: tests/test-next-resume-agree.sh drives the real Bun entry on a seeded interrupted run (asserts `engine10: resume from stage implement`, exit 2 not 0) and a stub that exits 4 (asserts 4); tests/test-verify-receipt-route.sh covers the path forms, a tampered receipt (TAMPERED, never CONCERNS) and the diff-verifier controls. Red on origin/main (17 and 4 failures), green after; mutants (delegate back to `resume`, schema route removed) turn them red.
1554
-
1555
-
1556
-
1557
- ## FC-EMPTY-RUN-BRANCH-LEFT-CHECKED-OUT A run that committed nothing left HEAD on its empty loki/ branch, the next run stacked on it, and the handoff offered to merge a 0-file change
1558
- - User-visible vs raw: user-visible. After two FAILED runs with 0 commits, two empty `loki/e10-*` branches remained, HEAD sat on the last one (the next run printed `base: loki/e10-...`), and HANDOFF.md read `Accept: git merge --no-ff loki/e10-...` for 0 files.
1559
- - Law: a run never leaves the user's checkout worse than it found it (Claude Code never moves your branch); only work that exists is offered for acceptance.
1560
- - Cause (measured): stages/intake.ts ensureBranch checks out the run branch; supervisor.ts restored origBranch only on a stop signal, and run_summary.ts always printed the Accept/Reject pair.
1561
- - Siblings swept: stop (SIGINT/SIGTERM) already restored via restoreBranch and keeps the branch (a stopped run is resumable); crashed runs never reach the terminal code, so resume keeps its branch. A run branch with any commit, a branch the user moved off, and a non-`loki/` name are never touched.
1562
- - Legacy sibling (run.sh): the session branch `loki/session-*` had the same defect. `_loki_restore_base_branch` (autonomy/run.sh) applies the same rule at session end: only for a non-zero result other than a stop signal (130, 143), only when the branch tip equals `session-start-sha`, never a branch with a commit, a dirty tracked tree keeps the branch and prints the way back, and the delete is `git branch -d`. A successful (VERIFIED) session and a stopped one are left alone, as in engine10. Fixture: tests/test-branch-lifecycle.sh section BR (empty failed session, failed with commit, successful, signal, dirty tree, and a mutant that drops the tip check). The engine10 CLI path was checked on main with an invalid-API-key stub run through fatal_e2e: HEAD returned to main and no loki/ branch remained, so no engine10 gap exists.
1563
- - Mechanism: stop_restore.ts dropEmptyRunBranch, called once from the supervisor after the backstop commit, deletes the branch (update-ref -d with the expected tip) only when the tip equals intake's base_sha, after restoreBranch's plain no-force checkout; run_summary.ts prints `Nothing to accept` for 0 files.
1564
- - Fixture: tests/engine10/supervisor_signal.test.ts "supervisor: a run that committed nothing" (empty, committed positive control, user-switched, dirty, non-loki name) and tests/util/run_summary.test.ts (0-file Decide block).
1565
-
1566
- ## FC-TEST-STAGE-CREATED-DIR-TURNS-NULL-GIT-INTO-SPAWNS The route matrix guard exceeded its 120s budget on clean main because its stages created the fake repo directory, so every safeGit call spawned real git instead of failing in 2ms
1567
- - User-visible vs raw: raw (a guard test red or hung at a fixed budget; no user-visible symptom, but the L1 routing guard and its 34-case mutation proof could not finish, so the moat check was effectively off).
1568
- - Law broken: a guard must finish in seconds and its cost must not depend on machine load.
1569
- - Cause (measured): on clean origin/main 04dfb3074 the test timed out at 120000ms (load 21). Section timing showed the machine cases at about 0.5s each (64 per matrix, 4 more matrices in the override snapshots). Per stage: fix spent 64ms in currentDiff and 62-113ms in treeFingerprint. safeGit on a missing cwd measured 2.2ms per call and on an existing non-git directory 86ms per call. Stages create `<runDir>/repo/.loki`, which turned the missing directory into an existing one after the first stage.
1570
- - Siblings swept: other makeCtx users in tests/engine10/route_matrix_lib.ts all go through the same helper; the mutation proof (route_matrix_mutation.test.ts) runs the same matrix once per mutation, so it had the same cost.
1571
- - Mechanism: makeCtx removes `<runDir>/repo` at creation, on every stage.started emit and before each fake session, so git on the fake repo path fails fast with the same null result. The matrix judges models, not git.
1572
- - Fixture: tests/engine10/route_matrix.test.ts (15s total at load 34 against more than 120s before).
1573
- ## FC-GROUP-REFUSES-LOCAL-TASK-WITHOUT-ORIGIN A multi-part prompt in a repo with no owner/repo origin exits NOT PROVEN with no unit started, so a task that needs no remote cannot run at all
1574
- - User-visible vs raw: user-visible. `loki start` on a two-file prompt in a fresh local repo printed "NOT PROVEN: group run needs an origin remote of the form owner/repo; no unit was started" and exited 1 in 0.2 s. The bench (fd-2-two-module-untested, loki arm, 2026-10-23) scored it a failure although Loki never attempted the task.
1575
- - Law broken: a path that needs a remote only for its launcher must be chosen only when the remote exists; a local task never fails because of a capability it did not need (verified-unless-it-cannot-be).
1576
- - Cause (measured): route.ts chose the group runner on decomposability and provider slots alone; run_group.ts then refused at its own origin check (the unit launcher, autonomy/lib/backlog.py, takes the owner/repo slug).
1577
- - Siblings swept (grep for remote get-url origin across loki-ts/src, autonomy, bin): run_group.ts (three checks, now one originSlug); backlog.py origin_slug (the launcher itself, github.com only); run_outputs.ts resume (already degrades with a message); base_guard.ts (already skips without an origin); issue-parser.sh and run.sh issue-mode slug detection (issue mode needs a repo by definition). No other local-task path requires an origin.
1578
- - Mechanism: route.ts asks run_group.originSlug before choosing the real group runner (task or mission) and otherwise takes the existing sequential fallback with the reason named. run_group.ts keeps its own refusal for a direct call.
1579
- - Sibling not closed: originSlug accepts any path-shaped origin with two segments (a local bare path reads as a slug), while backlog.py accepts github.com only; tightening it would change GitHub Enterprise hosts, so it is left and noted here.
1580
- - Fixture: loki-ts/tests/engine10/route_group.test.ts (no origin: sequential with reason, not NOT PROVEN, task and mission; control: an owner/repo origin still reaches the group runner). Red with the two gates in route.ts removed (3 failures).
1581
-
1582
- ## FC-SKILL-LINK-HIJACK A plain run rewrites the user's global skill link to itself
1583
- - User saw: running any loki binary repointed ~/.claude/skills/loki-mode, once into a loki-run temp dir and later, on `loki --version` from global 11.17.103, back to that older install.
1584
- - Mechanism (measured): bin/loki calls `loki_skill_link_heal` on every run (FC-30). The heal repointed a LIVE link to a different Loki install whenever the running root looked like an install path, with no check on temp roots, on which install was newer, or on whether the caller was an install step. Raw cause is a repair that treated "any running install" as the owner of the link.
1585
- - Law: L0 (repair only what is provably the harness's own, never take over a user-visible global). L2 (the verdict about the host must not depend on which binary was run last).
1586
- - Siblings: swept every writer of provider skill links. `loki_skill_link_heal` (all four provider dirs, one mechanism, fixed here); `cmd_setup_skill` (autonomy/loki ~14954) is an explicit command and refuses an existing target, so it stays; `cmd_remote` (autonomy/loki ~29612) only creates when SKILL.md is absent, never replaces a valid link; doctor.ts is read-only; shell completions do not touch skill dirs.
1587
- - Mechanism of the fix: one shared function. A plain run (mode run) repairs only dangling links; only `bin/loki-postinstall.js` (mode postinstall) repoints a live link to another install. A running root under TMPDIR, /tmp, /var/tmp or /var/folders is refused. A live link is never repointed to a lower VERSION than its current target. Opt-out LOKI_NO_SKILL_LINK_HEAL=1 unchanged.
1588
- - Fixture: tests/test-skill-link-heal.sh cases 15-18 (temp-rooted scratch prefix refused, plain run leaves a valid link, downgrade refused, upgrade by postinstall repoints as positive control), fake HOME only. Red on the old lib (3 of 4 fail, the control passes). Mutants: drop temp check, drop mode gate, drop version guard each turn exactly one case red.
1589
-
1590
- ## FC-FALSE-DONE-HAS-NO-BASELINE The bench had no task on which either tool claims done while a hidden check fails, so the false-done rate (M5) could not be measured as a rate
1591
- - User-visible vs raw: user-visible. The claim "Loki stops on agent-independent checks, Claude Code stops when the model says done" had no number behind it: the only discriminator, multifail-1-two-modules, went 2 of 2 for both tools.
1592
- - Law broken: a comparative claim ships with a measured baseline and an interval, never with a rate inferred from tasks that do not separate the tools.
1593
- - Siblings swept: attribution.py (false_done, false_completion_rate) and runner.py grade already compute the metric and are unchanged; the existing tasks all pass on both tools in the recorded trials, so none separates them.
1594
- - Mechanism: four pre-registered tasks (benchmarks/bench/tasks/fd-1 to fd-4), each with a held-out overlay that checks only prompt-stated requirements. References and near-miss artifacts stay outside the repo and are hash-attested through run.sh oracles (private_probe.py). The task and overlay bytes are pinned, so a change after a paid trial needs a new task id.
1595
- - Fixture: tests/test_bench_taskspec.py FalseDonePreRegisteredTasks (pins, task load, overlay red on the untouched fixture). Red when an overlay or task byte changes.
1596
-
1597
- ## FC-FALSE-DONE-OVERLAY-MISSES-A-STATED-RULE A pre-registered false-done overlay returned rc=0 for a solution that broke a rule its own prompt states, so the task could not tell done from false-done
1598
-
1599
- - User-visible vs raw: user-visible. v11.58.0 shipped four overlays that graded "done" for solutions violating stated rules; a false-done rate measured on them would have credited the broken solutions as passes (fd-1: duplicate keys differing only by whitespace; fd-1 also needed keys differing only by case accepted).
1600
- - Law broken: a held-out overlay is the claim's ground truth, so for every rule the prompt states, one near-miss solution that breaks only that rule must return rc=1. tl-324 reproduced two holes in fd-1; probing fd-2, fd-3 and fd-4 the same way (one near-miss per stated rule, overlay rc recorded) found rc=0 holes in all three: fd-2 shout()/bracket() edited ("do not change the existing functions"); fd-3 bad tax rate raised ValueError, or a negative quantity checked on the first item only ("raise the same errors for every input"); fd-4 files sorted or deduplicated, a total in place of a per-file word count, --min-words 0 rejected, a fractional N accepted, usage errors after output had started, a directory operand crashing.
1601
- - Siblings swept: fd-1 through fd-4 (about 50 near-misses probed). The only remaining rc=0 near-miss is --min-words +5, which is not a violation (int("+5") is a valid non-negative integer); it is left unasserted.
1602
- - Mechanism: pins forbid editing a graded overlay, so the defect is fixed by a new id (fd-1b to fd-4b) and the old id is retired in benchmarks/bench/retired_tasks.json. bench_schema.without_retired filters cost_claim and equivalence_report rows, and runner.run_task refuses a retired task, so a retired task never runs and never feeds a claim. An unreadable manifest raises instead of keeping every row.
1603
- - Fixture: tests/test_bench_taskspec.py FalseDonePreRegisteredTasks: test_b_overlays_pass_the_reference_and_reject_every_near_miss (12 near-misses, red on the retired overlays per test_retired_overlays_still_accept_the_near_misses_that_retired_them), the retirement tests, and the b pins. Private positive and adversarial artifacts for each b id are hash-attested in private_attestation.json.
1604
-
1605
- ## FC-CITATION-UNDER-SUBMODULE-ALWAYS-HIGH A fabricated blocking finding citing a path under a bumped submodule stayed HIGH, checked out or not, and said nothing
1606
- - User-visible vs raw: user-visible. A council reviewer citing `vendor/lib/ghost.ts:2` for a file that exists nowhere kept a blocking HIGH finding and blocked the run, with no hint the citation could not be checked. Raw: loki-ts/src/council/finding_schema.ts citationVerdict returned "ok" for any path under a diff-listed gitlink, or whose directory component equalled a gitlink's final segment (#332, follow-up to #302).
1607
- - Law: a downgrade needs positive evidence, and an unchecked citation must say it was unchecked; "ok" is never the silent answer to "could not look".
1608
- - Siblings swept: touched() prefix rule, the loop resolving a path under a gitlink against cwd and root, rule (i) bare basename, rule (ii) final-segment component; all four now share one gitlink check (checkedOut, deleted-in-diff).
1609
- - Mechanism: citationVerdict returns "refuted" | "unproven" | "ok". A gitlink directory with content on disk and no deleted-in-diff record (CitationContext.gitlinkDeleted, filled by voter_agents from the submodule old..new diff) refutes the citation (MEDIUM, [citation not found]); an absent or empty gitlink directory keeps HIGH and prefixes the description with [NOT PROVEN: ...], which travels in the council issue text.
1610
- - Fixture: loki-ts/tests/council/finding_schema.test.ts "submodule citation: checked out", "not checked out" and "deleted in the diff"; red when the return in the gitlink branch of citationVerdict is mutated back to "ok".
1611
-
1612
- ## FC-PYTEST-IMPORT-SYMLINK-UNDER-TESTS A committed change carried a symlink under tests/ (tests/kvparse.py, tests/wcl.py in 2 of 3 cw20 pilots), flagged only after the fact as NOT PROVEN by base_guard.ts (FC-WALL-PHANTOM-PATH)
1613
- - User-visible vs raw: user-visible; the PR holds a link the task never asked for and the verdict degrades to NOT PROVEN.
1614
- - Law broken: the change contains only what the task needs.
1615
- - Cause (SUSPECTED, evidence status: code reading only, no transcript yet): no engine call creates a link under tests/ (engine links are node_modules, .venv, venv in throwaway worktrees: base_guard.ts, mutation_proof.ts, before_after.ts; treeswap.ts restores an attempt's own link). The implement agent most likely links `tests/<mod>.py` to a root module so a bare `pytest` import works; the commit stage's `git add -A` then stages it. The Wall brief already carried `python -m pytest <file>` (wall_hints.ts), the implement brief did not.
1616
- - Siblings swept: fix.ts and other stages build on FIXED_RULES or a brief without a runner line; the Wall brief already states the invocation. Only the implement lead lacked it.
1617
- - Mechanism: one line in FIXED_RULES (cache-stable lead of the implement brief) naming the exact invocation and forbidding files or links under tests/ to satisfy imports.
1618
- - Fixture: tests/e10ext/full_job_brief.test.ts "FC-PYTEST-IMPORT-SYMLINK-UNDER-TESTS" and the implement-brief golden.
1619
- - Open: confirm by grepping the loki implement transcripts of the fd-1 and fd-4 grid trials for `ln -s` and bare `pytest` (eng-303c); update this row with the result. Not covered: an agent that links anyway is still only flagged, not blocked.
1620
-
1621
- ## FC-SHARD-WATCHLIST-STALE The shard-coverage guard hardcoded which five suites are the heaviest; the table moved on, and one new registration shifted LPT tie rotation and turned main red
1622
- - User-visible vs raw: raw (CI guard). tests/test-shard-coverage.sh T5 failed on main: "expected 5 watched suites in 5 distinct shards", with trust-core tests and E2e Features both in shard 1. It is also a misleading signal, because no shard was unbalanced.
1623
- - Law: a guard asserts the property that holds by construction, over inputs derived from the live data, never over a name list that was true on one date.
1624
- - Cause (measured): bisected across the four commits that touched the runner or the table. 54700a30b: 0 failures; 71b292af4 (registers npm-dist-tags.sh with no duration row): 1 failure; 4b880492b and 77f1196c5: 1 failure. The hardcoded watch list held "E2e Features" (20s in the table) and "Review deadline, ..." (45s), which stopped being among the five heaviest long ago (ShellCheck 140, trust-core 130, reachability 95, Quick receipt order 90, v10 ops 70). They only separated by tie rotation, and the new registration moved the rotation. LPT guarantees the heaviest n suites land in n distinct shards; light suites carry no such guarantee.
1625
- - Siblings swept: test-shard-durations-drift.sh failed on the same commit for the same missing row (npm-dist-tags.sh); fixed with a measured row (8s). T1 to T4 do not name suites.
1626
- - Mechanism: T5 derives its five suites from tests/shard-durations.tsv, restricted to registered names, sorted by seconds descending.
1627
- - Fixture: tests/test-shard-coverage.sh T5. Executed mutation: replacing the runner's `shardof[idx] = pick` with `idx % 2` turns T5 red (and the empty-shard checks).
1628
-
1629
- ## FC-HELP-DRIFT-PLAIN-PROSE-CHANGELOG The help-drift guard went red on main because the release notes stopped using backticks, so it read nothing and called its own extraction broken
1630
- - User-visible vs raw: raw (a red gate on a clean main). tests/test-help-changelog-drift.sh failed with "no backticked 'loki ...' invocations in the newest 5 CHANGELOG sections; extraction is broken" on origin/main 9e4812a14 and on 749d5bb46.
1631
- - Law: a guard must read the format the writer actually emits; a drift check that reads nothing proves nothing, and it must not be silenced by relaxing the assertion or by adding text to the CHANGELOG.
1632
- - Cause (measured): the extractor was correct for its stated contract, but the CHANGELOG sections written since v11.53.0 are plain prose with no backtick at all (`awk '/^## v/{c++} c>=1 && c<=30' CHANGELOG.md | grep -c '`'` gave 0 for the newest 30 sections; the last section with a backtick is v11.52.0). The newest sections do name invocations unbackticked (for example loki doctor --json, loki start --resume), so the writer changed and the reader did not follow.
1633
- - Siblings swept: the other tests that read CHANGELOG (test-release-notes.sh, test-p0-verification-sweep.sh, test-enforcement-doc-honesty.sh, test-receipt-signing-discoverability.sh, test-mcp-tool-surface-packaged.sh, test-no-unreachable-shipped.sh) have no backtick-dependent read (grep for CHANGELOG lines mentioning a backtick in them: no output). The assertion text and the 5-section window are unchanged.
1634
- - Mechanism: extract_invocations also reads `loki <word> [--flag ...]` from the text left after backticked spans are removed. A plain hit with a --flag is checked exactly like a backticked one. A bare `loki <word>` could be prose, so it is checked only when the word is already a literal command in loki.bash main_commands; measured on the newest 30 sections the plain extractor finds 4 distinct invocations and no prose false positive.
1635
- - Fixture: tests/test-help-changelog-drift.sh self-test: plain-prose real flag passes, plain-prose unknown flag is reported by name, bare prose naming no command passes, and a window with no invocation still reports broken extraction. With the plain branch removed the first and third fixtures and the real run go red.
1636
-
1637
- ## FC-HELP-DRIFT-WINDOW-CAP The help-drift guard went red on main again because five consecutive slice releases named no command, so its fixed 5-section window held nothing and it called its own extraction broken
1638
- - User-visible vs raw: raw (a red gate on a clean main). tests/test-help-changelog-drift.sh exits 1 with "no 'loki ...' invocations (backticked or plain) in the newest 5 CHANGELOG sections; extraction is broken" on origin/main 11ab1df18 (v11.60.6) and on a78f48031 (v11.60.8), after FC-HELP-DRIFT-PLAIN-PROSE-CHANGELOG shipped in v11.60.6.
1639
- - Law: the same as FC-HELP-DRIFT-PLAIN-PROSE-CHANGELOG, a guard that reads nothing proves nothing and is not silenced by relaxing the assertion or by adding text to the CHANGELOG; and a window sized in sections is a rate assumption that slice-per-release cadence (3-10 per hour) breaks.
1640
- - Cause (measured): sections v11.60.4 to v11.60.8 are plain prose naming no `loki <word>`; the plain-prose extractor is correct, the cap was the fault. The nearest section naming an invocation is within 10 sections (LOKI_HELP_DRIFT_SECTIONS=LOKI_HELP_DRIFT_WINDOW=10 gives a green real run), so a cap of 5 is red today and any larger fixed cap is red again after enough command-free releases.
1641
- - Siblings swept: the only other section-count reader of CHANGELOG is the per-version release-notes extraction (scripts/release-notes.sh, tests/test-release-notes.sh), which selects one named version and has no window.
1642
- - Mechanism: LOKI_HELP_DRIFT_WINDOW defaults to 1000 (the whole file). The window still widens one section at a time and stops at the nearest section that names an invocation, so only the newest invocation-bearing sections are checked. "Extraction is broken" now means no invocation anywhere in the CHANGELOG. The assertion text is unchanged.
1643
- - Fixture: tests/test-help-changelog-drift.sh self-test: an invocation six sections down passes (red at the old cap, green now) and a CHANGELOG with no invocation anywhere still reports broken extraction.
1644
-
1645
- ## FC-ZSH-START-RESUME-COMPLETION `loki start --resume` shipped in help and bash completion but not in the zsh completion
1646
- - User-visible vs raw: user-visible (Tab after `loki start --` in zsh never offered --resume).
1647
- - Law: HELP-DRIFT (FC-72): a flag the CHANGELOG names must be in help and in both completions.
1648
- - Cause (measured): `LOKI_HELP_DRIFT_SECTIONS=30 bash tests/test-help-changelog-drift.sh` gave "FAIL: '--resume' (CHANGELOG, loki start) is absent from completions/_loki" (13 passed, 1 failed) while the same flag is in the loki.bash start arm and in `loki start --help`. The default window never reached the note that names it, so the gap went unseen.
1649
- - Siblings swept: completions/ holds only loki.bash and _loki (no fish file); every other flag named in the newest 30 CHANGELOG sections passes all four completion checks (the same run, 14 passed after the fix).
1650
- - Mechanism: completions/_loki start block gains `--resume[...]::run id:` (optional run id, matching `--resume [RUN_ID]` in help).
1651
- - Fixture: tests/test-help-changelog-drift.sh runs the real CHANGELOG over a 30-section window and requires it drift-free; red before the change, green after.
1652
-
1653
- ## FC-COMMIT-STAGES-USER-UNTRACKED A commit publishes the user's untracked files that were present before the run (FC-42 family)
1654
- - Law: only what the run changed is published; a pre-run user file the run left untouched is never staged into a Loki commit (the same rule dropSet already applies to pre-run tracked dirt).
1655
- - Cause (measured, #353 CW-18 test): intake records preexisting_untracked_blobs for every untracked file, but only discardIfSatisfied read it. The commit stage and the supervisor backstop run `git add -A` and dropSet excluded only untouched pre-run tracked dirt, so a normal VERIFIED run committed the user's untracked notes.
1656
- - Siblings swept (every site that stages files): commit stage (seal.ts) and backstopCommit (commit_filter.ts, via supervisor) both go through dropSet and are fixed together; attempts.ts, modernize/scheduler.ts and verify.ts stage inside fresh worktrees that hold no user untracked files; check_result.ts and pr_lessons.ts add explicit run-owned paths only; discard.ts already protects them.
1657
- - Mechanism: untouchedUntracked (e10ext/preexisting_dirty.ts) returns the intake-untracked files whose bytes and mode still match the snapshot; dropSet resets them from the staged set. A file the run edited stays the run's change. Crash-written files are not in the snapshot (see FC-CRASH-WORK-READ-AS-PREEXISTING).
1658
- - Fixture: loki-ts/tests/e10ext/commit_filter_guard.test.ts (commit stage and backstop); red before the change, green after; mutants: ignore the snapshot, treat an edited file as untouched.
1659
-
1660
- ## FC-PROVIDER-VERSION-PROBE-FULL-ENV The cline `--version` probe in providers/mcp_config.ts spawned with the full parent env, handing GH_TOKEN, GITHUB_TOKEN and SSH_AUTH_SOCK to an external binary, and main went red on full_env_spawn_guard from 35411094c (CW-10) on
1661
- - User-visible vs raw: raw (a credential-exposure boundary and a red guard on main, not a user message).
1662
- - Law: no child process receives the ambient GitHub credentials (FC-40); a spawn gets tokenFreeEnv or a listed reason.
1663
- - Cause (measured): `clineVersion()` called `spawnSync("cline", ["--version"], { env: process.env })`. The version probe needs no credential, so no allowlist row is justified. tests/util/full_env_spawn_guard.test.ts reported `unlisted: loki-ts/src/providers/mcp_config.ts` alone, in `bun test tests/util` (524 tests, 1 fail) and in the full tests/providers run.
1664
- - Order sensitivity: not reproduced. The guard is a pure static scan (walk SRC, regex per call window, compare with the allowlist file); it holds no state, mocks nothing and reads only files, so its result cannot depend on test order. "Passes in some batches" is explained by batches that never include the file (diff-selected shards run it through the walk-all-src marker only when selected). No change to the guard was needed; the behavioral test below fails independently of the guard.
1665
- - Siblings swept: every other `process.env` spawn in providers/ is already listed with a reason (claude_flags.ts, runner/providers.ts) or token-free; mcp_config.ts is the only unlisted one (guard output).
1666
- - Mechanism: spawn through `tokenFreeEnv(process.env)` (util/safe_git.ts), the shared scoped env builder.
1667
- - Fixture: tests/providers/mcp_config.test.ts runs a fake `cline` that records GH_TOKEN, GITHUB_TOKEN and SSH_AUTH_SOCK with canary values set in the parent; red with `env: process.env` (restored in the mutant run), green with tokenFreeEnv. The full_env_spawn_guard goes red on the same mutant.
1668
-
1669
-
1670
- ## FC-WALL-FILE-WITHOUT-RUNNER-SILENTLY-DROPPED A sealed Wall file that no runner of the repo can run was dropped from verify, named nowhere, and the receipt still said wall.passed true
1671
- - User-visible vs raw: user-visible. A Wall of `loki_wall_a_test.py` plus `loki_wall_b.rs` (or `.java`, `.rb`) verified with `wall_passed: true` and an empty `not_proven`; the second acceptance test was sealed read-only and never executed.
1672
- - Law: a sealed Wall file is either executed and passing, or named NOT PROVEN; a proof never covers a file it did not run.
1673
- - Cause (measured): wall.ts guessRunner returns null for any extension other than `.py`, `.go` and the repo's JS/TS runner, so the file was sealed but left out of wallTests and out of not_run_files; verify.ts built wallTests from `map.tests` filtered by the sealed paths, so a path with no test entry vanished, and wall_passed was computed only over the checks that ran.
1674
- - Siblings swept: every extension guessRunner refuses (`.rs`, `.java`, `.rb`, `.php`, `.cs`, a JS file with no JS runner detected), a `.py` or JS Wall file the verify test map does not list (same drop in verify), the all-dropped Wall (wall_passed stays unset, never true), publishableWall (requires an executed passing target check, so a dropped file is never published; it is now named in not_proven instead of vanishing), holdout checks (unchanged, they come from a separate store).
1675
- - Mechanism: one wording, `NO_RUNNER_REASON` in util/check_result.ts. wall.ts adds each sealed file without a runner to `not_run_files` (seal.ts already turns those into `Wall test not executed: <file>: <reason>`); verify.ts adds the same line to `not_proven` for every sealed path missing from wallTests and refuses `wall_passed: true` while any is missing (a failing check still gives false). Seal's set dedups the identical text.
1676
- - Fixture: tests/engine10/verify.test.ts "FC-WALL-FILE-WITHOUT-RUNNER-SILENTLY-DROPPED" (mixed runnable plus `.java` is not a bare true, all-dropped is unset, the runnable-only control stays true with no such line) and the extended tests/engine10/wall.test.ts "a sealed file whose runner cannot be guessed" (the `.rb` is named, the `.py` is not). Mutants: dropping the wall_passed guard, dropping the not_proven push, and dropping the not_run_files push each turn the matching test red.
1677
-
1678
- ## FC-E10EXT-LINE-BUDGET-CROSSED-BY-ACCUMULATION Slices that each fit the e10ext budget summed past it, so structural-checks "line budgets" went red on main (1513 >= 1500)
1679
- - User-visible vs raw: raw. No user output changes; the structural-checks gate fails on main and blocks every release behind it.
1680
- - Law: a budget is a ceiling on the directory, not on a slice; the slice that crosses it moves cohesive code to the module that already owns that concern, never raises the cap.
1681
- - Cause (measured): `find loki-ts/src/e10ext -name '*.ts'` counted 1513 lines with scripts/structural-checks.sh's awk rule; e10ext holds 15 files and the cap is 1500.
1682
- - Siblings swept: engine10 core is 4979 of 5000 (no headroom to move code into it), modernize and contrib budgets, the earlier e10ext 1509 of 1500 recorded under the 10.7.1 train row.
1683
- - Mechanism: currentBranch and restoreBranch (a git helper used by supervisor, base_guard and run_outputs) live in util/safe_git.ts, which owns git helpers and has no line budget; e10ext is 1492.
1684
- - Fixture: scripts/structural-checks.sh "line budgets" PASS; loki-ts/tests/engine10/budget.test.ts and tests/util/fc25_safe_git.test.ts keep restoreBranch covered at its new path. Red on origin/main 5fa53fe11 (1513), green after.
1685
-
1686
-
1687
- ## FC-TEST-SLICE-LOADER-DROPS-HELPER The cost-cache test exec'd a slice of dashboard/server.py that began at the calculator, so a helper defined just above it was dropped and 5 of 9 tests died with NameError
1688
- - User-visible vs raw: raw (a red gate on a clean main). `python3 -m pytest -q tests/dashboard/test_api_cost_cache.py` on origin/main 11ab1df18 failed with `NameError: name '_inference_geo_multiplier' is not defined` at the calculator's return line.
1689
- - Law: a test that execs a slice of a module must derive the slice from what the code under test calls; a hand-picked start marker silently goes stale the moment the code gains a helper above it.
1690
- - Cause (measured): dashboard/server.py defines `_inference_geo_multiplier` at line 7833 and `_calculate_model_cost` at 7852; the test sliced from `def _calculate_model_cost` to the `/api/cost` route, so the helper added by bfbf0c24b (LOKI_INFERENCE_GEO multiplier) was outside the exec'd text. The server itself was correct.
1691
- - Siblings swept: tests/test-pricing-parity.sh geo_dash already starts the slice at `def _inference_geo_multiplier` and passes; tests/dashboard/test_cost_timeline_endpoint.py does not slice server.py (23 pass); no other test indexes `def _calculate_model_cost`.
1692
- - Mechanism: `_slice_start` in the test walks the AST, finds module-level helpers the calculator calls that are defined above it, and starts the slice at the earliest. The exec namespace also supplies os, json, _Path and __file__ that the helper reads.
1693
- - Fixture: tests/dashboard/test_api_cost_cache.py `test_slice_includes_every_helper_the_calculator_calls`. Executed mutation: forcing `_slice_start` back to `def _calculate_model_cost` turns 6 tests red (10 pass otherwise).
1694
-
1695
- ## FC-TEST-AMBIENT-INFERENCE-GEO The cost-cache test read the ambient LOKI_INFERENCE_GEO, so a developer shell with the variable set turned 3 tests red
1696
- - User-visible vs raw: raw (a red test on a contributor machine, found by rev-b in the costcache2 review).
1697
- - Law: a test of a function that reads an environment variable pins that variable itself and restores it; it never inherits the shell.
1698
- - Cause (measured): `LOKI_INFERENCE_GEO=us python3 -m pytest -q tests/dashboard/test_api_cost_cache.py` gives "3 failed, 7 passed" on 9dc4242bf; unset or empty gives 10 passed. `_calculate_model_cost` multiplies by `_inference_geo_multiplier`, which reads `LOKI_INFERENCE_GEO` (dashboard/server.py:7837).
1699
- - Siblings swept: the only other test file naming LOKI_INFERENCE_GEO under tests/dashboard is none (grep of tests/dashboard/*.py found no other reader); tests/test-pricing-parity.sh sets it explicitly per case.
1700
- - Mechanism: ApiCostCacheTests.setUp pops the variable and registers a cleanup that restores the prior value (or removes it).
1701
- - Fixture: with LOKI_INFERENCE_GEO=us the file gives 10 passed, 7 subtests passed (was 3 failed, 7 passed); an in-process run with the variable set to us leaves it set to us afterwards.
1702
-
1703
- ## FC-FIXRESUME-LOOP-TEST-DEFAULT-TIMEOUT A test that runs five full resumes in a loop had no explicit timeout, so bun's 5000ms default failed it under load
1704
- - User-visible vs raw: raw. No product behavior changes; a red test blocks the gate it runs in.
1705
- - Law: a test that spawns a subprocess or runs a supervisor more than once carries an explicit timeout sized for load, never bun's 5000ms default (sibling of FC-SUBPROCESS-TEST-DEFAULT-TIMEOUT, not yet on main).
1706
- - Cause (measured): tests/engine10/fix_resume.test.ts "P12-07n: a garbage alive file is no heartbeat and never stops the resume" failed at 5002.38ms (reproduced 1 of 3 reruns at load 24) and at 5001.21ms alone at load 25; with the bound, three runs took 4.51s, 3.71s and 2.97s whole-process.
1707
- - Siblings swept: every other test in fix_resume.test.ts that loops is pure (the P12-07j malformed-event loop at :382, the P12-07m cap loop at :560); the single-supervisor async tests run one resume and the only subprocess test (P12-07m main(), :576) already has 70000.
1708
- - Mechanism: the P12-07n test takes an explicit 60000ms bound; no assertion changed.
1709
- - Fixture: tests/engine10/fix_resume.test.ts P12-07n itself; it fails at 5000ms on origin/main debc7e843 under load and passes with the bound.
1710
-
1711
- ## FC-STAGE-DIFFSTAT-CONCURRENT-WALL-NOT-MET The per-stage diff_stat is journaled for wall, implement and fix through runStage, but the concurrent wall (split) path journals none
1712
- - User-visible vs raw: user-visible in the run replay scrubber (#422): the wall stage of a concurrent run shows its line counts as unmeasured. NOT MET, deliberately not fixed in pd/stage-diffstat.
1713
- - Law: a value the journal cannot measure is absent, never a fabricated zero (the diff_stat key is omitted, not 0).
1714
- - Cause: the split wall author runs beside implement and installs into a base copy, so a before/after tree window around it would report implement's changes as wall's.
1715
- - Siblings swept: the other tree-writing stages (implement, fix, non-split wall) all go through runStage and are covered; plan, verify, commit, seal, pr and deep write no tracked files by design.
1716
- - Mechanism (open): measure the install itself (the diff of the base copy before and after install) inside installWallJob.
1717
- - Fixture: none yet; tests/engine10/machine.test.ts covers the covered stages only.
1718
-
1719
- ## FC-E2E-A130-DEFAULT-TIMEOUT-UNDER-LOAD Two e2e tests fail at bun's 5000ms default at load 30 and above, on origin/main as well
1720
- - User-visible vs raw: raw. A red test blocks the gate it runs in.
1721
- - Law: a test that spawns the engine carries a load-aware timeout, never bun's 5000ms default (sibling of FC-FIXRESUME-LOOP-TEST-DEFAULT-TIMEOUT).
1722
- - Cause (measured): tests/engine10/e2e.test.ts "A-130 round 2: a tampered event log is TAMPERED..." and "A-130 quiet by default..." failed at 5.1 to 6.4s on a clean origin/main worktree (d7ee510e4) at load 29 to 43; the other 9 tests in the file pass.
1723
- - Siblings swept: not yet; every test in tests/engine10 that spawns the CLI without an explicit timeout.
1724
- - Open question: should these use setLoadAwareTimeout from tests/preload.ts instead of a fixed bound? Not fixed in this slice.
1725
- - Fixture: bun test tests/engine10/e2e.test.ts at load 30 or higher.
1726
-
1727
- ## FC-P9-DYNAMIC-IMPORT-FALSE-POSITIVE Moat case P9.allowtoken-only-fetch-callers went red on main for a dynamic import of reviewerGitEnv in cli.ts, which is not an allowToken caller
1728
- - User-visible vs raw: raw (a red moat case on main; no user-facing message).
1729
- - Law: allowToken reaches safeGit only from the two fetch callers (FC-25c); the guard must fail closed on anything that can hide a caller, and must not fail on code that provably cannot reach the opt-in.
1730
- - Cause (measured): the commit 58a365567 (FC-REVIEWER-GIT-CONFIG-EXEC) added `const { reviewerGitEnv } = await import("./util/safe_git.ts")` at loki-ts/src/cli.ts:330. The scan's import rule flags every literal dynamic import or require of safe_git, because a namespace-style result can be reached through a computed key. `bash tests/moat/p9-rule-of-two.sh` on a clean origin/main worktree with deps installed printed `CASE P9.allowtoken-only-fetch-callers FAIL ... safe_git imported by alias, namespace, require or dynamic import ...: loki-ts/src/cli.ts`. tests/moat/run.sh step 0a installs loki-ts deps before the case runs, so this was a real red, not a missing-typescript false red.
1731
- - Siblings swept: cli.ts:330 is the only dynamic import of safe_git outside loki-ts/src/util (git grep). Static named imports of safe_git were already accepted; the dynamic destructured form is the same shape.
1732
- - Mechanism: in the P9 AST scan, a literal `await import("...safe_git...")` is accepted only as the initializer of an object destructuring whose every element is a plain identifier property (no rest, no computed key, no string key, no nested pattern) and none of whose names is an opt-in helper. Any other dynamic or require form still flags. A destructured safeGit with allowToken is still caught by the raw allowToken grep outside the two fetch callers.
1733
- - Fixture: tests/moat/p9-rule-of-two.sh positive controls for a rest destructure, a computed-key destructure, a renamed opt-in helper and a destructured safeGit carrying allowToken (each must be flagged), and a negative control for the real `{ reviewerGitEnv }` form (must not flag). Mutant: accept any destructure shape; the rest and computed controls must go red.
1734
-
1735
- ## FC-MERGED-SLICE-BROKE-REPO-WIDE-ENGINE10-GUARD Three merged slices (35f9a1988, 8b439ee51, 5fc8c5d46) left main red on repo-wide engine10 size and stack-knowledge guards that their selected tests never ran
1736
- - User-visible vs raw: raw (red guards on main; no user-facing message). One consequence is user-visible: the implement lead named a Python runner, which is stack knowledge in a harness that must stay generic.
1737
- - Law: the e10ext tree stays under 1500 lines, the engine10 core stays under 5000 lines, and no file under loki-ts/src names a manifest or runner unless l0_guard.allowlist lists it. Slice gates that run only the touched test files never execute these guards.
1738
- - Cause (measured on clean origin/main 5cae48655): tests/engine10/budget.test.ts e10ext count was 1513 after 35f9a1988 (FC-COMMIT-STAGES-USER-UNTRACKED, +14 lines in preexisting_dirty.ts); l0_guard.test.ts failed on src/e10ext/context.ts naming pytest after 8b439ee51; the core count (the test's split("\n") rule, summed over src/engine10 excluding modernize/) was 4972 at 837abdc40, 5011 at 5fc8c5d46 (FC-ENGINE10-CACHE-KEY-NOT-PRODUCER-BOUND, cache.ts +49 lines since the 9872ddead trim to 4959), 5012 at 8066c0108 and 5025 at c955a15e8 (R100-022 preflight); history is not linear, so 5fc8c5d46 is the first measured crossing and c955a15e8 the final count on main.
1739
- - Siblings swept: every test under tests/engine10 that reads src repo-wide was listed; the three failing here are the only ones red on main. The same weakness exists for any future slice, so the sweep rule is that a slice touching loki-ts/src runs budget.test.ts and l0_guard.test.ts, not only its own files.
1740
- - Mechanism: untouchedUntracked moved to features/untouched_untracked.ts; the producer fingerprint code moved from engine10/cache.ts to util/producer_fingerprint.ts; the implement lead now says to run tests with the exact command the Project Model lists instead of naming a runner. No cap was raised and no allowlist entry was added.
1741
- - Fixture: the existing guards themselves, red before (1513, a pytest literal in context.ts, 5025) and green after. Mutants: five blank lines appended to commit_filter.ts turn the e10ext cap red, five to cache.ts turn the core cap red, and a pytest literal put back into context.ts turns the L0 guard red.
1742
- - Recurrence (pd/mainred): the same slice gate weakness left two MORE engine10 guards red on origin/main. (1) never_below_raw "L1 static guard" (3 tests): 9efb8eca0 changed the seal.ts receipt field to `model: externalRun(o) ? EXTERNAL_MODEL_LABEL : ctx.model`, a record of the run model that configures no session; the guard judged it an unvetted pin. (2) the implement.test.ts brief golden (len 2781): 85979c3af made the FIXED_RULES lead stack-neutral (len 2791, sha 55cb50f6...) without re-capturing it. Product right in both; guard allowlist and golden were stale. Fix: allowlist the seal.ts receipt line in the guard, re-capture the golden. Mutants: reverting the seal.ts allowlist line turns 3 L1 tests red; changing one byte of FIXED_RULES turns the golden red.
1743
-
1744
- ## FC-FULL-SUITE-IGNORES-PROJECT-INTERPRETER The deep-verify full pytest suite ran under bare `python`, not the project's own interpreter, so a repo with a working .venv and no `python` on PATH could never prove its suite
1745
- - User-visible vs raw: user-visible. An uninterrupted run on a Python fixture with a working .venv and ruff ended PARTIAL because the full-suite check could not run (bare `python` missing or without pytest), while per-test runs under the same repo used the venv.
1746
- - Law: every check that executes project code resolves its interpreter through one resolver (project venv first), and no stage hardcodes a bare interpreter name.
1747
- - Cause (measured): `FULL_SUITE_CMD` in util/check_result.ts was a static table (`python -m pytest -q`); the per-test path used `resolveTool` but the full-suite path (engine10/stages/deep.ts, project_model/package_suite.ts) did not.
1748
- - Siblings swept: deep.ts and package_suite.ts (both consumers moved), non-pytest runners (vitest, jest, bun, node, npm, go, cargo: no interpreter, stay static), per-test pytest (already resolveTool).
1749
- - Mechanism: `fullSuiteCmd(runner, repoDir)` resolves pytest through `resolveTool(repoDir, "python", "python3", "python")`; both consumers call it.
1750
- - Fixture: tests/engine10/deep.test.ts "pytest full suite runs under the project .venv python when no bare `python` is on PATH" (red on the static table, green after).
1751
-
1752
- ## FC-SUITE-RUN-LINE-BREAKS-ON-SPACED-PATH The full-suite baseline run line joined an absolute project interpreter path into a bash -c string unquoted, so any repo under a path with a space never ran its base suite
1753
- - User-visible vs raw: user-visible. For a Python repo under a spaced path (for example `~/My Projects/app`) the intake baseline got `bash: .../My: No such file or directory` (rc 127), so a failure already red on base could not be excused and the run ended FAILED or PARTIAL for a harness reason.
1754
- - Law: a command assembled from argv for a shell is quoted word by word; a path the harness resolves is never trusted to be space-free.
1755
- - Cause (measured): FC-FULL-SUITE-IGNORES-PROJECT-INTERPRETER made `fullSuiteCmd` return an absolute `.venv/bin/python`; `fullSuiteSpecs` (project_model/package_suite.ts) joined `[cmd, ...args]` with spaces into `SuiteSpec.run`, which `baselineSuites` runs through `bash -c`. Reproduced by rev-c: rc 127 with a space in the path, rc 0 and "1 passed" without.
1756
- - Siblings swept: `deep.ts` runs the same argv with no shell (unaffected); `packageSuiteSpecs` uses the project model's own command string (already a shell line, unchanged); `SuiteSpec.cmd` stays the plain display join so recorded commands and the root-baseline dedup (`normCmd(run)`) are unchanged for space-free repos.
1757
- - Mechanism: `shellQuote` moves from runner/providers.ts to util/shell.ts, with `shellWord` (quotes a word only when bash would split or expand it). `fullSuiteSpecs` builds `run` with `shellWord` per token.
1758
- - Fixture: tests/engine10/deep.test.ts "FC-SUITE-RUN-LINE-BREAKS-ON-SPACED-PATH" (a repo under `My Projects/app` with a `.venv/bin/python` runs rc 0 via `bash -c spec.run`; a plain go line stays byte-identical). Mutant dropping the per-word quoting gives rc 127 and the test goes red.
1759
-
1760
- ## FC-LINT-TOOL-NEVER-USED-BY-PROJECT verify demanded lint:ruff of a Python repo that neither declares ruff nor has it installed, so the check ended not_run and the run PARTIAL
1761
- - User-visible vs raw: user-visible. The 05:44Z python fixture run (kvparse, bench workdir loki-bench-loki-jxegolv0) had no .venv, no ruff config and no ruff on PATH; verify recorded lint:ruff not_run (reason "ruff not found on PATH") and seal listed "not run: lint:ruff" as NOT PROVEN, so a correct change could not reach VERIFIED.
1762
- - Law: a verifier demands only the checks the project itself uses; a tool the project never names and the machine does not have is not applicable, while a tool the project names and the machine lacks stays not_run.
1763
- - Cause (measured): runLintChecks (engine10/stages/verify.ts) ran lint:ruff for every changed .py with resolveTool's last resort `ruff`, which fails to spawn when nothing resolves.
1764
- - Siblings swept: lint:shellcheck (bash -n always runs; shellcheck is a general tool on the changed set, unchanged), lint:eslint (already gated on an ESLint config in the package), lint:tsc (already gated on tsconfig.json). ruff was the only named-tool check with no project gate.
1765
- - Mechanism: lint:ruff runs when ruff resolves (project .venv, uv, or PATH) or the project declares it (projectDeclaresRuff: ruff.toml, .ruff.toml, or `ruff` in pyproject.toml, setup.cfg, tox.ini, .pre-commit-config.yaml, Pipfile or a requirements file). Otherwise no check is recorded.
1766
- - Fixture: tests/engine10/verify.test.ts "FC-LINT-TOOL-NEVER-USED-BY-PROJECT" (no ruff, nothing declared: no check; declared but absent: not_run). Mutant (gate always true) turns the first test red.
1767
-
1768
- ## FC-VISUAL-EVIDENCE-NA-READ-AS-NOT-PROVEN Seal listed "visual evidence skipped: no package.json" for a repo with no UI and no changed page file, so a CLI or library change was NOT PROVEN for evidence it never had to produce
1769
- - User-visible vs raw: user-visible. The same python fixture run and the first-run gate `scripts/first-run-gate.sh --entry start-empty` (eng-mcpchain, #357 criterion 3) ended PARTIAL with this line among the NOT PROVEN items.
1770
- - Law: an item is NOT PROVEN only when the run could have produced it and did not; a capture that has no subject is not applicable.
1771
- - Cause (measured): sealEvidence (features/visual_evidence.ts) added every capture skip reason to notProven, including the two that mean there is nothing to boot ("no package.json", "no dev, preview or start script") when no changed file is a page file.
1772
- - Siblings swept: the LOKI_VISUAL_GATE evidence_failed branch (same predicate now), "no changed page files" (existing, pinned by the no-web-pages test, left as is), a changed page file with no package.json (still NOT PROVEN and still a gate failure), and LOKI_VISUAL_EVIDENCE=1 (API transcript requested, still reported).
1773
- - Mechanism: one predicate in sealEvidence, noUi = skip reason is one of the two no-boot reasons AND the API transcript was not requested AND no changed file isPageFile; it suppresses both the NOT PROVEN line and evidence_failed.
1774
- - Fixture: tests/features/visual_evidence.test.ts "FC-VISUAL-EVIDENCE-NA-READ-AS-NOT-PROVEN" (changed .py: no line, no failure; changed page file with no package.json: line and failure).
1775
-
1776
- ## FC-RUNS-ENV-LEAKS-ACROSS-PROJECTS `loki runs` applied the caller's LOKI_DIR to every registered project, so one run list was reported once per project (repo "a" and repo "b")
1777
- - User-visible vs raw: user-visible. `loki runs --json` returned the same run twice under two repo labels.
1778
- - Law broken: a row's repo label names the project whose runs directory it was read from; a per-process env knob that points at one directory cannot be applied to N projects.
1779
- - Cause (measured, rev-b probe m309/p008/probe.ts): collectRuns called runsRoot(env, proj.path), and runsRoot honors LOKI_DIR (absolute or relative to the repo dir) for any repo dir it is given.
1780
- - Siblings swept: runsRoot callers in run_status.ts (status, intent, pause/cancel) act on the current repo, where LOKI_DIR is correct; collectRuns is the only multi-project reader.
1781
- - Mechanism: collectRuns reads join(proj.path, ".loki", "runs") for each registered project and ignores LOKI_DIR.
1782
- - Fixture: loki-ts/tests/commands/run_status.test.ts "LOKI_DIR set across 2 projects lists each run once under its own repo"; a mutant restoring runsRoot(env, proj.path) turns it red.
1783
-
1784
- ## FC-RUNSH-LINE-CITE-DRIFT Comments in loki-ts/src cite autonomy/run.sh by line number, and the numbers go stale on every run.sh edit
1785
- - User-visible vs raw: raw (maintainer-facing). A reader following "run.sh:28272" lands on unrelated code and trusts a parity claim that no longer points at its subject. #212 criterion 2 failed twice for this (the PAUSED.md cite was refreshed and went stale again).
1786
- - Law: a cross-file reference names a thing that survives edits (function, variable or literal string), never a position.
1787
- - Cause (measured, eng-npm 2026-10-23, origin/main): `git grep -nE 'run\.sh[: (~]+[0-9]{4,}' loki-ts/src` returned 279 lines in 21 files; the cites were already wrong when written against older run.sh (the PAUSED.md cite ~28272 resolved to a different line on 5746a4550).
1788
- - Siblings swept: the same pattern in loki-ts/src outside runner/ (providers/claude_flags.ts and others, same grep). Cites to other files (manager.ts:1770) and line cites in docs/ are out of scope; they drift the same way and are not guarded here.
1789
- - Mechanism: one structural check, "no script line cites in loki-ts/src", in scripts/structural-checks.sh (so pre-push and CI run it), scanning loki-ts/src for `(run.sh|autonomy/loki|completion-council.sh)` followed by a 3-or-more digit line number (the siblings were folded in under D86).
1790
- - Fixture: tests/test-structural-checks.sh T13 plants a `run.sh:12345` cite in a scratch tree (check goes red) and an anchored "autonomy/run.sh (the _resume_line assignment)" cite (stays green).
1791
- - Remediation: all 279 cite lines were rewritten to name an anchor: `run.sh <function>()` or `run.sh "<unique literal>"`, located from the comment text and the matching TS code via grep on the current run.sh, never from the old numbers (run.sh grew about 2.5x since they were written). 0 cites were unrecoverable. Where the logic is inline in run_autonomous() the anchor is that function. Three header lists (budget.ts, rarv.ts, checkpoint.ts) that cited bare line numbers without the run.sh prefix now list function names only. The 21 sibling cites to autonomy/loki and completion-council.sh (commands/doctor.ts, commands/status.ts, council/voter_agents.ts, runner/council.ts) and 5 three-digit run.sh cites were anchored the same way. Unrecoverable referent: 6 cites in commands/doctor.ts pointed at bash doctor code (_doctor_block, doctor_provider_install_cmd, text-mode cmd_doctor) that no longer exists in autonomy/loki because doctor is Bun-only; they now say so instead of citing a line.
1792
-
1793
- ## FC-PROOF-POINTER-STALE-ON-BUN-ROUTE The Bun route never cleared .loki/state/last-proof-id.txt at run start, so a run that stopped before proof generation left the previous run's id and every reader showed a receipt for different work
1794
-
1795
- - User-visible vs raw: user-visible (receipt and summary sections name the wrong run).
1796
- - Law: absent reads as "no data"; a stale pointer reads as evidence, so a run must never inherit a prior run's pointer.
1797
- - Cause (measured): #211 was fixed on the bash route only (autonomy/run.sh:7665). `git grep -n last-proof-id origin/main -- loki-ts/src` returned only the writer in runner/proof.ts.
1798
- - Siblings swept: loki-ts/src/engine10 neither writes nor reads the pointer (`git grep -n last-proof-id -- loki-ts/src/engine10` returns nothing). The readers (run.sh, loki, run-summary.sh, trigger-server.py) are route-agnostic and unchanged.
1799
- - Mechanism: clearStaleProofPointer in runner/proof.ts, called once from runAutonomous right after makeContext.
1800
- - Fixture: loki-ts/tests/runner/autonomous.test.ts "#211 Bun-route sibling" seeds run-A, stops the run before proof with LOKI_PROOF=0, expects the pointer absent. Mutant: remove the call in runAutonomous; the test goes red.
1801
- ## FC-ENGINE-ROUTE-MOCKED-TOOL-PATH A flag an MCP tool passes to bin/loki was never registered in autonomy/route.sh, and the tool's mocked tests could not see the refusal
1802
- - User-visible vs raw: user-visible. loki_seal_diff runs bin/loki under LOKI_ENGINE=v10, where route.sh refuses any flag it does not list as legacy_flag; the tool returned "loki exited 2 without a receipt" on every real call while its unit tests (a fake loki script) and the bun e2e (bin/loki without LOKI_ENGINE=v10) were green.
1803
- - Law: a tool path is tested through the entry point the user takes (Engine Law: test the embedding path), not only against a stand-in for it.
1804
- - Cause (measured): --seal-existing was added to supervisor.ts but not to _ROUTE_VALUE_FLAGS or _ROUTE_E10_FLAGS in autonomy/route.sh. The same slice carried three siblings of one class (trust boundary crossed by an unvalidated value): the patch path inherited by every child env, an engine-written run_id joined into a path unchecked, and a receipt naming the default model for a run no Loki session wrote.
1805
- - Siblings swept: mcp/v10_tools.py passes bin/loki only a bare ref (v10_run) and the verify subcommand (v10_verify); seal_diff is the only caller that passes a flag.
1806
- - Mechanism: one real-entry test class (RealEngineSealDiffTests in mcp/tests/test_v10_tools.py) runs seal_diff through the real bin/loki under v10_tools._env() with only the claude CLI stubbed; any future tool flag missing from route.sh fails it. The env, run_id and receipt fixes live in the one place each (worker.ts takes and deletes the patch path, v10_tools.RUN_ID_RE, seal.ts externalRun).
1807
- - Fixture: mcp/tests/test_v10_tools.py RealEngineSealDiffTests, SealDiffTests.test_run_id_from_the_clone_is_validated_before_any_path_join, loki-ts/tests/engine10/seal_existing.test.ts (B2, B3). Mutations that go red: route.sh without --seal-existing, worker/session env not stripped, receipt model reverting to ctx.model, run_id unchecked, no fetch of refs/loki/seal.
1808
-
1809
- ## FC-EXTERNAL-SEAL-WITHOUT-WALL-VERIFIED An external diff sealed through the shipped path skips the Wall and is still VERIFIED
1810
- - User-visible vs raw: user-visible. `loki_seal_diff` (v10_tools._env) and plain `loki "<intent>" --seal-existing p` do not set LOKI_E10_PLAN, so the E-64 lean path fired: wall stage.skipped "small task with a relevant test: cascade skips Wall", the receipt's wall files list was empty, and wrong code (`return a + b + 1`) sealed VERIFIED on the one existing add-only test. The CW-12 tests set LOKI_E10_PLAN=1, which forces a plan and hides the skip.
1811
- - Law: an external diff has no implement session whose work the harness watched, so the Wall is its only independent proof; no Wall that ran means no VERIFIED.
1812
- - Test-only plan knob (swept): src never sets LOKI_E10_PLAN, it only reads it (sizing.ts:57); the only setters are tests (sizing, route_record_agreement, route_matrix_lib, plan, seal_existing, runner_errors, router_optout_golden, plan_route, e2e, session_l1_model, scope, issue_ref_e2e, fc69_wall_e2e under loki-ts/tests/engine10, and mcp/tests/test_v10_tools.py). A forced plan is not a path a default user takes; seal_existing.test.ts (its default seal() helper keeps it; the B6 cases remove it) and the MCP real-engine class (it is now removed there) hid this one.
1813
- - Non-external lean skip: see FC-LEAN-VERIFIED-WITHOUT-CHANGED-SYMBOL below, which carries the measured run and the fix.
1814
- - Siblings swept: every Wall skip reason in wall.ts (LOKI_E10_WALL=0, lean path, no runnable test command, pytest-only under the system interpreter) reaches the same verdict, so the rule lives in verdictOf (seal.ts), not in each skip.
1815
- - Mechanism: wallAuthor never takes the lean skip when ctx.externalPatch is set; verdictOf returns PARTIAL for an external run whose wall.files is empty, and seal adds a NOT PROVEN line naming it.
1816
- - Fixture: loki-ts/tests/engine10/seal_existing.test.ts runs a wrong external body with LOKI_E10_PLAN absent and expects not VERIFIED plus the NOT PROVEN line; red on the previous head, and a mutant restoring either half (lean skip, or the verdictOf rule) turns a case red.
1817
-
1818
- ## FC-LEAN-VERIFIED-WITHOUT-CHANGED-SYMBOL A non-external run with no Wall is VERIFIED on a pre-existing test that never calls the changed symbol
1819
- - User-visible vs raw: user-visible. Measured run (throwaway repo, stub claude from loki-ts/tests/engine10/fixtures/e2e, LOKI_E10_PLAN unset, task "add a multiply(a, b) function to calc.ts", implement appends multiply): outcome VERIFIED, stage.skipped wall "small task with a relevant test: cascade skips Wall", the receipt's only check is bun:calc.test.ts pass n=1 with provenance exogenous, and calc.test.ts contains no "multiply" (grep -c multiply = 0). The new function is untested and the receipt says VERIFIED.
1820
- - Law: a VERIFIED verdict needs a passing check that exercises the changed code. With no Wall file that ran, the only candidate is a pre-existing test, so it must reference a symbol the diff declares or modifies; otherwise PARTIAL with a NOT PROVEN line naming the symbols.
1821
- - Siblings swept: every no-Wall path (lean skip, LOKI_E10_WALL=0, no runnable test command, pytest-only under the system interpreter) reaches the same verdict, so the rule lives in seal (verdictOf plus one gap computation), not in wall.ts. External runs keep their own stricter rule (FC-EXTERNAL-SEAL-WITHOUT-WALL-VERIFIED). ALREADY_SATISFIED and runs with a Wall file are unchanged. A Wall that ran but does not exercise the changed symbol is not covered here.
1822
- - Mechanism: util/check_result.ts changedSymbols reads a git diff -U0 for the names declared on added lines (plus the hunk's enclosing declaration when the hunk removes lines or declares nothing) and symbolGap checks the whole-word references in the passing checks' test files; seal passes the gap to verdictOf. Conservative by design: an unreferenced symbol (for example one only reached through a helper) downgrades to PARTIAL, never the reverse.
1823
- - Fixture: loki-ts/tests/engine10/e2e.test.ts runs the stub fixture with LOKI_E10_PLAN absent and expects not VERIFIED plus the NOT PROVEN line, and a second case where the test calls multiply stays VERIFIED; unit cases in check_result.test.ts. Red on the previous head; mutants: drop the verdictOf rule, and make symbolGap always return empty. Added after rev-c: a third e2e case (stub mode localvar) where the diff has a local variable and a comment word that the test echoes is still PARTIAL; mutants: let symbolGap read comments, and let a const, let or var count at any indent.
1824
- - Declaration scope (rev-c repro): const, let, var and val count only at column 0 or on an export line, because an indented one is a local; comment text is stripped from added lines and from test sources before matching. Known false-PARTIAL shapes, accepted as the conservative side: an Express-style handler body edit (the hunk context is a call, not a declaration), an import-line edit (declares nothing), and a helper reached only through its caller (the test names the caller, not the helper). Comments are stripped over the whole added hunk, so a multi-line block comment cannot leak names (rev-a B2). Known false-VERIFIED shape, accepted for now and filed as a follow-on: a test title string or a Python docstring that names the symbol still counts as a reference, because string literals are not stripped.
1825
-
1826
- ## FC-WALL-FILE-NOT-DISCOVERED-BY-RUNNER Verify runs the Wall only if the stack's test discovery also finds it, so a sealed loki_wall_*.py is never run and the receipt says "wall result not recorded by verify"
1827
- - User-visible vs raw: user-visible. A Python run with a Wall file (`loki_wall_x.py`) never executes the Wall in verify; wall_passed stays undefined and Seal lists "wall result not recorded by verify" (seal.ts:250) as NOT PROVEN even though the Wall was authored, sealed and green on the base run.
1828
- - Law: the Wall is the proof the run is judged by, so verify runs exactly the files the Wall sealed (outputs wall.files), never whichever of them the runner's discovery filter happens to list.
1829
- - Cause (measured): wall.ts names every Wall file with the `loki_wall_` prefix and picks its runner with `guessRunner`; verify.ts built `wallTests` as `map.tests.filter(t => wallPaths.has(t.path))`, and `map.tests` comes from detect(), which pytest discovery limits to `test_*.py` and `*_test.py`. `loki_wall_x.py` matches neither, so the filter dropped it. The JS-only harness fallback below it covered one stack.
1830
- - Siblings swept: Go (`loki_wall_x_test.go` ends in `_test.go`, so detect lists it and the old filter worked; the new path keeps it, with a regression fixture), JS (kept: the harness fallback is now the same code path, runner chosen by guessRunner), Rust (NOT closed here: guessRunner has no `.rs` mapping and `cargo test` takes no file argument, so a sealed Rust Wall file has no runner in wall.ts either; open as its own row against wall.ts).
1831
- - Mechanism: `guessRunner` moved from wall.ts to testmap.ts (beside harnessRunners) and is exported; verify builds wallTests from the sealed paths with it, reusing the discovered ref when detect() already lists the file.
1832
- - Fixture: loki-ts/tests/engine10/verify.test.ts "FC-WALL-FILE-NOT-DISCOVERED-BY-RUNNER" holding loki_wall_x.py beside test_x.py (red on origin/main: the Wall check is absent), plus a Go loki_wall_x_test.go case.
1833
-
1834
- ## FC-WALL-RUST-FILE-HAS-NO-RUNNER A Rust Wall file is sealed and shipped but nothing ever runs it: guessRunner has no .rs mapping and the cargo command takes no file, so the Wall proves nothing on a Rust repo
1835
- - User-visible vs raw: user-visible. On a Rust repo the Wall author writes `loki_wall_x.rs`; wall.ts seals it (wall.sealed lists it) but `guessRunner` returns null, so the base run skips it and verify (after FC-WALL-FILE-NOT-DISCOVERED-BY-RUNNER) builds no check for it. The receipt carries a Wall that was never executed and says nothing about it.
1836
- - Law: a sealed Wall file is either run by a runner that names it, or reported NOT PROVEN with the reason; it is never silently dropped and no check is invented for it.
1837
- - Cause (measured): `guessRunner` in testmap.ts mapped .py, .go and JS only; `runnerCmd` in check_result.ts ran `cargo test` for every cargo ref, which selects no file. Cargo selects a single integration-test file only as `cargo test --test <stem>`, and only for a file directly under a `tests/` directory.
1838
- - Siblings swept: Go and JS already map (FC-WALL-FILE-NOT-DISCOVERED-BY-RUNNER). Other extensions (.java, .rb, .c) still have no mapping and are not covered here; they are named in the open list below.
1839
- - Mechanism: `guessRunner(path, runners)` takes the path and returns "cargo" for `<dir>/tests/<name>.rs` when the repo has the cargo runner; `runnerCmd` runs `cargo test --test <name>` for such a file; a sealed .rs path with no per-file runner is named by the generic FC-WALL-FILE-WITHOUT-RUNNER-SILENTLY-DROPPED line in not_proven ("Wall test not executed: <path>: no runner of this repo runs this file"). A cargo per-file red stays not_run (the coarse rule), never a false green.
1840
- - Open siblings: Wall files with extensions that guessRunner does not map (anything other than .py, .go, .rs, JS/TS) are still sealed without a runner or a not_proven note.
1841
- - Fixture: loki-ts/tests/engine10/verify.test.ts "FC-WALL-RUST-FILE-HAS-NO-RUNNER" (tests/loki_wall_x.rs runs `cargo test --test loki_wall_x`; src/loki_wall_x.rs is NOT PROVEN with the reason and invents no check) and loki-ts/tests/engine10/testmap.test.ts guessRunner cases.
1842
-
1843
- ## FC-WALL-GO-FILE-RUNS-WHOLE-PACKAGE A sealed Go Wall file is run as `go test ./<dir>`, the whole package, so a sibling test in the same package decides the Wall result
1844
- - User-visible vs raw: user-visible. A Go Wall file `loki_wall_x_test.go` shares its package directory with the repo's own tests. Verify ran `go test -json ./<dir>`, so a red or slow sibling test (written by the repo, not the Wall) turns the Wall check red or hides what the Wall file proves.
1845
- - Law: a Wall check reports on the Wall file, not on its neighbours. Go exit 0 stays never a pass (FC-16b): this row narrows what runs, it does not change how a Go result is trusted.
1846
- - Cause (measured): `runnerCmd` case "go" in check_result.ts ignored the file name and passed only `./${dirname(t.path)}`.
1847
- - Siblings swept: cargo is already per-file (FC-WALL-RUST-FILE-HAS-NO-RUNNER); pytest, vitest, jest, bun and node already take the file path. Go is the last per-file gap.
1848
- - Mechanism: `runnerCmd` reads the file's top-level Test, Fuzz and Example function names by name only, never by signature (TestMain excluded; a signature check dropped multi-line, unnamed, aliased or dot-imported `*testing.T` and non-ASCII names, turning a red Wall into not_run, rev-b B1) and runs `go test -json -run '^(TestA|TestB)$' ./<dir>`. A file with no such function keeps the package run (it can only end not_run). The classification is unchanged: exit 0 is not_run (GO_EXIT0_REASON), a failed run is fail only with real failure evidence.
1849
- - Open (needs a ruling, not decided here): a Go Wall file can never reach pass under FC-16b, so `wall_passed` stays unset for a Go-only repo and the receipt keeps saying the Wall result is not recorded. A pass from "exit 0 plus an ok line" would reopen the forgery FC-16b closed (TestMain os.Exit(0), a test printing `ok pkg`).
1850
- - Fixture: loki-ts/tests/engine10/verify.test.ts "FC-WALL-GO-FILE-RUNS-WHOLE-PACKAGE" (a red sibling test in the package does not make the Wall check fail; the argv carries -run with the file's test names) and a runnerCmd unit case.
1851
-
1852
- ## FC-TEST-ALIAS-FWD-REDUNDANT-SPAWN The alias-forwarding suite re-ran the same alias --json command for two assertions, so its CLI spawn floor was higher than its assertions need (#183)
1853
- - User-visible vs raw: raw. No product behavior changes; the suite is load-sensitive and a spawn count that is higher than needed lengthens every loaded run.
1854
- - Law: one command run feeds every assertion that reads the same stdout, unless the command has a side effect the second run depends on.
1855
- - Cause (measured): in tests/cli/test-alias-forwarding.sh assert_row, the alias --json command ran once for the stderr-suppression check and again for the 4b stdout parity check. The suite did 168 CLI spawns on origin/main 5cae48655 (counted by a wrapper around bin/loki, same on LOKI_ROUTE=bun and LOKI_ROUTE=bash).
1856
- - Siblings swept: the export row cannot share the run: `export markdown --json` treats --json as an output filename, so the first run creates it and the canonical comparison relies on the second alias run seeing it already exist (first-run output "Exported to --json", later "File already exists: --json"; found when sharing it turned the row red). It keeps its second spawn. The other assert_row spawns each read distinct output.
1857
- - Mechanism: the --json run keeps its stdout in a per-unit file and the 4b check reads it; every spawn goes through a counting wrapper and the total prints before the results line.
1858
- - Fixture: the printed "CLI spawns" line (168 before, 154 after) with 202 passed and 9 failed on both routes before and after (the 9 are the stale help-group reds, a separate row). Mutant: make the alias lookup return the canonical stdout; with a planted differing alias stdout, 14 rows fail without the mutant and 0 with it.
1859
-
1860
- ## FC-TEST-ALIAS-FWD-HELP-PAGE-DRIFT The alias-forwarding help assertions read plain `loki help` after it became the core page, so 9 of 211 checks went red on main (8 group headers and the entry count)
1861
- - User-visible vs raw: raw (a stale test; the help output is intentional, not a regression).
1862
- - Law: a test that pins a help surface reads the page that owns that surface; a change to which page is the default moves the test in the same change.
1863
- - Cause (measured): commit 826deafd2 (2026-10-15, "grouped core help, help topics") made plain `loki help` print Start/Check/Configure/More and moved the full grouped table to `loki help all`. tests/cli/test-alias-forwarding.sh kept reading `loki help` and expecting Build:/Session:/Verify / trust:/Observe:/Report:/Knowledge:/Modernize:/Config: plus a Commands: block. On origin/main b2fc64eca the file printed 202 passed, 9 failed on LOKI_ROUTE=bun and LOKI_ROUTE=bash. `loki help all` on both routes carries all eight headers and prints 32 entry lines in its Commands: block (30 distinct verbs), above the old bound of 28.
1864
- - Siblings swept: the other help suites (test-help-discoverability.sh, test-help-no-removed-surfaces.sh, test-help-start-flags.sh, test-help-changelog-drift.sh) were edited by 826deafd2 itself; no other file in tests/ names the Build:/Session: headers (git grep).
1865
- - Mechanism: the help-structure block runs `loki help all`; the entry-count bound is 32 with the measured count in the comment. No assertion text changed except the bound.
1866
- - Fixture: tests/cli/test-alias-forwarding.sh itself, 211 passed, 0 failed on both routes. Mutants: point the block back at `loki help` (all 9 red), rename a header (1 red), bound 31 (1 red), Usage text (1 red).
1867
-
1868
- ## FC-AUTH-CAUSE-ONLY-IN-IMPL-LOG A rejected API key ends the run as "exit 1 (general error)" while the cause sits only in the implement log
1869
- - User-visible vs raw: `ANTHROPIC_API_KEY=<bad> loki start "<brief>"` printed `Reason: exit 1 (general error)` and `next: loki doctor`. Raw: `.loki/iteration-<run>-impl.log` held `Invalid API key · Fix external API key`, and the SDK frame carried `error: "authentication_failed"` with `api_error_status: 401` (measured with `claude -p --output-format stream-json`). The run ran every later stage on a key the provider had already rejected.
1870
- - Law: a failure the provider names is reported by that name at the moment it happens, with the fix command (ENGINE-LAWS: user-visible output states the cause, not the exit code).
1871
- - Cause: `session.ts` classifies fatal auth only from child stderr plus `[sdk-loop error: ...]` lines. `sdk_stream_parser.ts` never wrote such a line for the structural `authentication_failed` assistant frame, so the machine's fatal:auth path (`machine.ts` sessionKind) never fired; stdout text is deliberately never classified (forgery guard in fatal_e2e.test.ts).
1872
- - Siblings swept: every stage that opens a provider session (wall, plan, implement, already_done, deep council, fix) calls `ctx.sessions.run`, which the machine wraps once, so one classifier covers all of them (`git grep -n 'sessions.run(' loki-ts/src`). Child-stderr route already classified and is unchanged. The `billing_error` assistant frame is the same shape but maps to quota_exhausted, which waits and reruns by design (P12-09); it is not changed here and stays a follow-up.
1873
- - Mechanism: the stream parser writes `[sdk-loop error: Failed to authenticate (authentication_failed): <provider text>]` for the structural frame only. `session.ended` records the provider text as `provider_error`; `reasonOf` appends it to "provider authentication failed", and `nextOf` names the fix: `claude login`, or the key to replace when the provider says API key.
1874
- - Fixture: loki-ts/tests/runner/sdk_stream_parser.test.ts, loki-ts/tests/engine10/output.test.ts and loki-ts/tests/util/run_summary.test.ts cases named FC-AUTH-CAUSE-ONLY-IN-IMPL-LOG. Red with the parser line, the event field or the reason suffix removed.
1875
-
1876
- ## FC-AGENT-TEXT-FORGES-ENGINE-MARKER Agent-controlled text could write a line the engine trusts as its own, so plain assistant prose or a provider CLI's stdout could end a run as fatal:auth
1877
- - User-visible vs raw: user-visible. An assistant text block (or any stdout line of a provider CLI) reading `[sdk-loop error: Failed to authenticate (authentication_failed): Invalid API key, ...]` plus exit 1 gave stop `fatal:auth`, and with the cause-in-Reason slice it was printed as the Reason with key advice. Reproduced by rev-c on main.
1878
- - Law: the engine classifies only text the provider or the engine wrote, never the agent transcript (A-113); an engine-written marker must be unforgeable from agent text.
1879
- - Cause (measured): sdk_stream_parser capture() appended assistant and result text verbatim to the iteration log, and providers.ts writeCaptured wrote a CLI provider's stdout verbatim; session.ts matches `^\[sdk-loop error: .*\]$` and retry_class SDK_ERR matches the marker anywhere in the same text.
1880
- - Siblings swept: every line-anchored reader of the log text (session.ts parseMarkers reads LOKI_ALREADY_DONE and LOKI_SPEC_CONFLICT, which the agent is meant to write, so they are not engine markers; budget.ts isRateLimited is an unanchored keyword heuristic on prose by design, not a trusted marker). Engine-written markers in the log: `[sdk-loop error:` (parser frame line, providers.ts catch) and `[rate_limit]` (a keyword the heuristic already matches in prose, so a forgery gains nothing). Also covered: the claude, codex, cline, aider and opencode CLI paths (all four writeCaptured stdout sites) and the `said` text inside the real frame line.
1881
- - Mechanism: retry_class.ts neutralizeEngineMarkers rewrites `[sdk-loop error:` to `[agent-text sdk-loop error:` anywhere in agent text (capture(), the frame's `said`, and every CLI stdout before writeCaptured). sdkAuthCause also returns redactSecrets(...) because provider_error goes into events.jsonl unredacted.
1882
- - Fixture: sdk_stream_parser.test.ts cases named FC-AGENT-TEXT-FORGES-ENGINE-MARKER (forged assistant and result text at line start and mid-line, mid-line quote read as null by sdkAuthCause, real frame still classified and redacted for an sk-ant- value) and fatal_e2e.test.ts (forged stdout plus exit 1 is not fatal). Each is red with neutralizeEngineMarkers as identity, the `^` anchor dropped, or the redaction removed.
1883
-
1884
- ## FC-CHILD-ENV-HAND-ALLOWLIST-DRIFT The LOKI_* names a dashboard-started build receives were a hand-typed set in build_supervisor.py, so a setting the runner reads but the set omits is silently dropped (#305)
1885
- - User-visible vs raw: user-visible. A LOKI_* setting exported before starting a build from the dashboard has no effect and nothing says so.
1886
- - Law: an allowlist that must track what the code reads is derived from, or checked against, what the code reads; it is never kept by hand alone.
1887
- - Cause (measured): dashboard/build_supervisor.py `_CHILD_LOKI_ENV_NAMES` listed 33 names. A scan of `env.LOKI_X` and `env["LOKI_X"]` reads under loki-ts/src at origin/main 8cc2534f5 finds 316 distinct names; 18 are in the set and 298 are not (15 of the 33 set names are read only outside loki-ts/src). Examples not forwarded today: LOKI_PROVIDER, LOKI_E10_MODEL_DEFAULT, LOKI_BUDGET_LIMIT, LOKI_E10_MAX_COST_USD.
1888
- - Siblings swept: `_CHILD_ENV_NAMES` (non-LOKI system names) is a fixed OS list, not a reflection of code reads, and is unchanged. The bash route's env is out of scope.
1889
- - Mechanism: loki-ts/data/child-env.json holds `forwarded` (the original 33, no addition) and `not_forwarded` (the other 298 read names, a baseline that preserves today's behavior and needs a per-name decision to move). build_supervisor.py loads `forwarded` at import and fails hard if it is missing or empty. A test scans loki-ts/src and fails naming any read that is in neither list, and fails if a forwarded name is secret-shaped (TOKEN, SECRET, KEY, PASSWORD, CRED) or listed twice.
1890
- - Known limit: a read through destructuring (`const { LOKI_X } = process.env`) or a computed key is not seen by the scan.
1891
- - Fixture: tests/dashboard/test_build_supervisor.py: a child sees LOKI_CLAUDE_TASK_BUDGET=64000 read from its own env, never LOKI_PR_BOT_TOKEN or LOKI_RECEIPT_SIGNING_KEY; a planted `process.env["LOKI_PLANTED_X"]` fixture is reported. Mutants: loader returns an empty set (2 tests red); scan drops the `env.X` form (planted test red).
1892
-
1893
- ## FC-DIST-FRESHNESS-GUARD-CLI-ONLY The dev-machine dist freshness guard compares only loki-ts/src/cli.ts, so editing any other src file runs stale code
1894
- - User-visible vs raw: user-visible on a dev checkout or worktree. After editing e.g. loki-ts/src/engine10/supervisor.ts, `loki start` still executes the old gitignored loki-ts/dist/loki.js, so the fix appears not to work. Released channels (npm, Docker, brew) ship no src and are unaffected.
1895
- - Law: a dev run never executes a bundle older than the source it was built from.
1896
- - Cause (measured): bin/loki and autonomy/loki (delegate resolver) both prefer dist and fall back to src/cli.ts only when `loki-ts/src/cli.ts -nt loki-ts/dist/loki.js`. cli.ts is a thin dispatcher that rarely changes, so an edit to any other file leaves dist looking fresh. Recorded as a sibling line by pd/357c3 993a25298.
1897
- - Siblings swept: the two sites above are the only `-nt` dist-vs-src guards (`git grep -n 'src/cli.ts" -nt'`); autonomy/loki:2169, :29943 and :35490 pick src only when dist is absent and need no change. tests/cli/test-alias-forwarding.sh pinned the cli.ts-only string and now pins the new form.
1898
- - Mechanism: both sites ask `find loki-ts/src -type f -newer loki-ts/dist/loki.js -print -quit`; any hit means dist is stale and the src entry is used. One find, early exit, no hashing, bash 3.2 and BSD/GNU compatible. With no src dir (tarball layout) find prints nothing and dist is kept.
1899
- - Fixture: tests/test-bin-loki-bun-resolve.sh cases (h) to (k): non-cli src file newer than dist routes to src/cli.ts; dist newer than all src keeps dist; no src dir keeps dist; src older than dist keeps dist. Mutant restoring the cli.ts-only check turns (h) red.
1900
-
1901
- ## FC-STUB-SESSION-WRITES-OUTSIDE-ITS-STAGE The first-run gate's stub claude acted in every session, so the empty-folder `loki start` run sealed PARTIAL and the gate could not pass on any commit (#357 criterion 3)
1902
- - User saw: `scripts/first-run-gate.sh --entry start-empty` printed `FAIL outcome-verified: no Outcome: VERIFIED (rc=1): Outcome: PARTIAL` with NOT PROVEN `only agent-authored tests executed (no agent-independent proof)`. Measured on origin/main a78f48031 (this slice) and on 9feace648 and 3f6e07547 (eng-mcpchain).
1903
- - Law: a fixture that stands in for a stage must do that stage's work and nothing else; a fixture result is evidence about the engine only if the fixture is faithful to the contract the engine relies on (the Wall base is the pristine base, the Wall author is independent of the implementer).
1904
- - Cause (measured, events.jsonl of the kept run): the stub wrote hello.js and hello.test.js in every session it was started in. The project-model and plan sessions run in the repo, so hello.js existed before the Wall stage; the Wall author session (cwd holds only task.md) wrote no loki_wall_ file, so the Wall was empty (first run) or, once a Wall file was added, ran green on the base (`base_run pass 1`, reason `first author green on the base tree`, NOT PROVEN `Wall green on base: no Wall test discriminates the change`). With no Wall and no pre-existing test, the only executed test was the agent's own, so the verdict was PARTIAL. The `quick` leg was never affected because its sum.test.js pre-exists and is the exogenous proof.
1905
- - Siblings swept: the `quick` and G8 legs of the same stub are unchanged (`first-run-gate.sh --stub` ends `GATE: 0 assertion(s) failed`). `grep -rl COMPLETION_REQUESTED scripts tests` lists the other stub claudes; tests/moat/p9-rule-of-two.sh already keys its stub on `Loki 10 implement stage` (line 1450), which is the discriminator used here. The remaining stubs (b9-scoreboard.sh, tests/test-quick-*.sh and others) were not re-run.
1906
- - Mechanism: scripts/first-run-gate.sh stub claude branches on the session. The Wall author (task.md names the brief) writes tests/loki_wall_hello.test.js content as an agent-independent test; only the implement session (`Loki 10 implement stage` in its prompt) builds hello.js and hello.test.js; project-model and plan sessions write nothing. The engine is unchanged: it already reports PARTIAL when only agent-authored tests ran, and that is the honest verdict for the old stub.
1907
- - Fixture: tests/test-first-run-gate.sh extracts the stub from the gate and runs it per session (plan and project-model write nothing, the Wall author writes only loki_wall_hello.test.js, implement builds the CLI); against the origin/main gate three of those four cases are red, against the fixed gate all pass, and `first-run-gate.sh --entry start-empty` ends `PASS outcome-verified: Outcome: VERIFIED`.
1908
- - Sibling class (dev-machine dist): bin/loki prefers loki-ts/dist/loki.js unless loki-ts/src/cli.ts is newer (bin/loki, the `-nt` freshness guard). It compares only cli.ts, so an edit to any other src file leaves a gitignored dist that silently runs stale code, and a worktree `bun run build` fails and deletes dist. Runs measured through bin/loki in a worktree therefore need dist rebuilt in the main checkout or `BUN_FROM_SOURCE=1`; reviewers state which one they used. No row covered this; this line is the record, and a code fix (compare the newest src mtime) is a separate slice.
1909
-
1910
- ## FC-SUBPROCESS-TEST-DEFAULT-TIMEOUT Tests that spawn CLIs ran against bun's 5s default timeout and failed on a loaded host with no logic fault
1911
- - User-visible vs raw: raw (red tests on main, no user message).
1912
- - Law: a test file that spawns CLIs sets its time budget through the one shared setter, setLoadAwareTimeout in loki-ts/tests/preload.ts (FC-TEST-TIMEOUT-OVERRIDES-CLI); it does not rely on bun's 5s default or a per-test literal.
1913
- - Cause (measured): "header [1] and a dict attestation exit 1 with attestation: FAILED" in loki-ts/tests/commands/proof_verify_parity.test.ts runs 2 forged shapes x (1 in-process verify + 2 spawned CLIs) plus signing; on origin/main a78f48031 at load about 24 it failed with "this test timed out after 5000ms" and passed with `--timeout 60000` (1 pass, 0 fail, 5.44s). rev-b saw it fail at 5044.65ms at load 30. eng-mcpchain measured loki-ts/tests/providers/mcp_config.test.ts "bash and TS produce the same launch for the same fixture" failing the same way.
1914
- - Not the fix: `timeout` under [test] in loki-ts/bunfig.toml. Measured on bun 1.3.13: with it set, the dict attestation test still failed at 5045.87ms, so bun ignores the key.
1915
- - Siblings swept (measured with `git grep -lE "Bun\.spawn|spawnSync|execFileSync|execSync|child_process" -- loki-ts/tests` minus `git grep -l "setLoadAwareTimeout|setDefaultTimeout" -- loki-ts/tests`): 164 files spawn a subprocess, 154 of them set no timeout (engine10 81, runner 15, util 14, commands 11, features 10, e10ext 7, project_model 4, integration 3, council 3, providers 2, others 4). Only the two measured failing files are bounded here. The other 154 are out of scope for this change because none has been timed and bounding all of them blind would hide real slowness; each file that fails the same way gets the same one-line call. Per-test durations were not recorded.
1916
- - Mechanism: setLoadAwareTimeout(30_000) at file level in both measured files, scaled by machine load and overridable with LOKI_TEST_TIMEOUT_SCALE and LOKI_TEST_TIMEOUT_MS.
1917
- - Fixture: the two named tests; red at the 5000ms default under load, green after (bun test of both files: 36 pass, 0 fail). Mutant on the sibling: setLoadAwareTimeout(1) in mcp_config.test.ts turns the file red with hook timeouts (3.55ms, 3.16ms); restored.
1918
-
1919
- ## FC-MODEL-OVERRIDE-TEST-TOO-SLOW tests/test-model-override.sh took 445s at load 20-29 and exceeded the 300s shard budget, so a reviewer could not run it to completion
1920
- - User-visible vs raw: raw (a guard nobody can finish running is a guard nobody trusts; no user message).
1921
- - Law: a guard script finishes inside the shard budget at the documented load, or it is split; a slow guard is not a skipped guard.
1922
- - Cause (measured, per-line $SECONDS stamps): two parity matrices took 181s and 249s of the 445s. Each of their 224 cells started a fresh python3 that imported dashboard.server (0.75s per import at load 28), so 448 imports cost about 340s.
1923
- - Siblings swept: the resolver matrix (section 7) and the session-pin matrix (section 9) both used the per-cell python leg. The estimator leg (`loki plan`) and the bash legs are different processes by design and stay per cell. Other python3 -c calls in the file run once.
1924
- - Mechanism: one `py_batch` helper resolves every cell in a single interpreter. Each cell runs against a fresh copy of the ambient environment plus its own exports, the same input the per-cell `env "$@" python3` gave. Cell counts stay at 224 and 224.
1925
- - Fixture: after the change the file finishes in 103s with 71 passed, 0 failed (before: 445s, 71 passed). Mutant: dashboard/server.py `_clamp_to_max_tier` opus arm returning the alias unchanged makes the resolver matrix report PARITY MISMATCH on the fable cells; restored and re-run green.
1926
-
1927
- ## FC-WORKFLOW-STUB-IGNORES-SUBCOMMAND-AND-SHARED-PUBLISH-GROUP (#360, rev-b BLOCK on 6252fe625)
1928
- - User-visible: if the fix-to-reseal verb mapping were dropped, `/loki fix` would call `loki verify-pr fix`, which takes the non-reseal path, and no receipt would be posted; the suite stayed green. Separately, a queued publish job could be dropped when a newer run arrived.
1929
- - Law: a test double must assert the contract it stands in for (the subcommand), and a job that consumes another job's output must not share its concurrency group.
1930
- - Cause: the verify-pr stub in tests/test-issue-to-pr-action.sh wrote the status for any argv; `publish` and `pr-publish` shared groups with `agent` and `pr-fix`, and GitHub keeps one running and one pending run per group, a newer pending run replacing the older.
1931
- - Siblings swept (`grep -n "group:" docs/examples/loki-issue-to-pr.yml.example`): four jobs carry a group; only the two publish jobs shared one.
1932
- - Mechanism: the stub records its subcommand and the tests require `reseal` for fix and `explain` for explain; groups are now `loki-issue-`, `loki-issue-publish-`, `loki-pr-fix-`, `loki-pr-publish-`, and a guard requires four distinct groups.
1933
- - Fixture: tests/test-issue-to-pr-action.sh; mutants: drop `SUB="reseal"` (2 failures), make the publish group equal the agent group (guard red).
1934
-
1935
- ## FC-LINE-CAP-GATED-AFTER-MERGE Main went red on core engine10 5091 >= 5000 and e10ext 1513 >= 1500 because the line caps are enforced only by budget.test.ts and structural-checks line_budgets, not before a slice merges
1936
-
1937
- - User-visible vs raw: raw (a red structural gate on main; no user-facing behavior).
1938
- - Law: a cap that blocks main must also block the slice that crosses it; a slice may not merge on a gate that does not run the cap.
1939
- - Cause (measured): `bash scripts/structural-checks.sh` on origin/main 736a43939 printed "core engine10 5091 >= 5000" and "e10ext 1513 >= 1500". Commits 62f946e19 and 484752c3b touched the wall and testmap files in those dirs most recently; whether a slice gate ran line_budgets before merge is not measured here.
1940
- - Siblings swept: modernize 4000, contrib 1200 and SKILL.md 500 are under their caps in the same run (only the two lines above failed).
1941
- - Mechanism: this slice restores headroom without changing a cap or any behavior: comment paragraphs and doc comments in engine10 are reflowed to 230 columns (code unchanged; comment text identical once whitespace and comment markers are stripped), and in e10ext redundant header and doc-comment lines are tightened and one unused import line is removed. Running line_budgets before merge is a separate change, not made here.
1942
- - Fixture: tests/engine10/budget.test.ts and scripts/structural-checks.sh line_budgets are the existing guards; both must exit 0 on this branch.
1943
-
1944
- ## FC-RELEASE-UNNAMED-STACKED-PARENT A release merge shipped stacked parent commits that no reviewer had approved by SHA, including slices on hold, because the approval gate only checked the VERSION line (#436)
1945
- - User-visible vs raw: users received v11.58.0 containing cw20-r2 parents e4c445b73 and 13b46699d, plus cw15/ux350/roadmap100 parents 9bf08f534, dc2e16314, 64e9f1a09 and cf177adf5, while the slice hold stood. Raw: the range held commits no TL or HIGH APPROVE line named; a rebase had also changed f8f263627 after approval.
1946
- - Law broken: D12/D13 (a release carries only what a named reviewer approved); a hold is a block (OPERATING-MODEL.md, Release Manager).
1947
- - Sibling sweep: scripts/release-approval-gate.sh (VERSION-only), scripts/train-cycle.sh phase_b (pushes a SHA to main), scripts/release.sh (stages release commits). Phase B is the only other push-to-main path.
1948
- - Mechanism: one default-deny check inside scripts/release-approval-gate.sh, run on every invocation before the VERSION check: each commit is named by an approval line, rebase-equal by git patch-id --verbatim (indentation edits are content), or exempt (clean merge, release-files-only release commit whose dist files are exactly release.sh RELEASE_DIST_FILES); a held slice id denies; an approval line with no board row is denied by name (holds cannot be checked); an unreadable range exits 1, never ALLOWED. LOKI_RELEASE_APPROVED and --approve do not reach it. train-cycle.sh phase B calls the same script.
1949
- - Fixture: tests/test-release-approval-gate.sh (stacked A,B,C; clean rebase; changed rebase; hold; exemptions; env bypass; replay 9556d6f16..8aad0307f; indentation-only rebase; bad range; 6-column parked row; dist/loki.js exempt vs dist/evil.js denied; rowless approval denied) plus one mutant per rule.
1950
-
1951
- ## FC-FULLSUITE-GO-CARGO-IDS A Go or Rust full suite already red on base was charged to the change because failIds knew only pytest, jest, vitest and node ids (FULLSUITE-GO-CARGO-IDS)
1952
- - User-visible vs raw: user-visible. FULL_SUITE_CMD runs `go test -json ./...` and `cargo test`, but failIds returned [] for both, and attributeFullSuite treats "no known ids" as nothing excused, so any base-red Go or Rust repo blocked the change for failures it did not cause.
1953
- - Law broken: a failure the change did not cause is never charged to the change (same law as FC-BASELINE-PER-PACKAGE).
1954
- - Siblings: pytest, jest, vitest and node ids (unchanged); the Rust Wall runner (FC-WALL-FILE-NOT-DISCOVERED-BY-RUNNER, guessRunner has no `.rs` mapping and cargo test takes no file argument; NOT closed here, gated on pd/wallfile merging); cargo's `--no-fail-fast` and doc-test ids.
1955
- - Mechanism: failures.ts goCargoIds, ahead of the generic ID_LINE path. go -json: id `pkg::Test` per Action=fail with a Test, the package for a package-only fail; count is the `--- FAIL:` output lines plus package-only fails. go plain: `--- FAIL:` names plus `FAIL<TAB>pkg` lines no test explains. cargo: `test x ... FAILED` against the summed `N failed`. Every branch stays fail-closed (ids fewer than the count returns []).
1956
- - Fixture: loki-ts/tests/engine10/verify_prered.test.ts "FC-FULLSUITE-GO-CARGO-IDS"; loki-ts/tests/runner/harness_intelligence.test.ts "FC-FULLSUITE-GO-CARGO-IDS: a go or cargo full suite red on base is excused for the same failing id, not for a new one".
1957
- | FC-GO-PACKAGE-ID-CHANGED-FILE | A go full suite fails on base and head with a package id ("pkg" build failure or "pkg::Test") and the change edits a .go file in that package; the id excuses itself as pre-existing though the change touched the failing code | user-visible: a verdict excuses a red the change may own | Engine Law 1 (no excuse without proof): an id excuses only if no changed file maps to it | Siblings: cargo ids (closed by FC-CARGO-ID-CHANGED-CRATE below); pytest, jest, vitest ids use the changed-file substring check already | Mechanism: repo_profile.ts goIdTouched walks up from each changed .go file to its nearest go.mod (nested modules and go.work members included) and compares the id to that module path plus the file's directory below it (suffix match only when no go.mod exists); a changed go.mod or go.sum touches every package of its module (id equal to the module path or under it); a changed go.work or go.work.sum touches every go id; attributeFullSuite eligibility requires !goIdTouched and the line gains "NOT PROVEN: package <p> did not build on base or head; its tests ran on neither" | Fixture (module-root M2 and non-.go M4 mutants): loki-ts/tests/runner/harness_intelligence.test.ts "FC-GO-PACKAGE-ID-CHANGED-FILE: the module-root package is touched only by a root .go file, and only a .go file touches a package"; also loki-ts/tests/runner/harness_intelligence.test.ts "FC-GO-PACKAGE-ID-CHANGED-FILE" (mutant dropping the check goes red) |
1958
- | FC-CARGO-ID-CHANGED-CRATE | A cargo full suite fails on base and head with the same test id and the change edits Rust source or a manifest; the id (a module path with no crate or file) excuses itself as pre-existing though the edited crate may own it | user-visible: a verdict excuses a red the change may own | Engine Law 1 (no excuse without proof): fail closed when the id cannot be mapped to the change | Siblings: go ids (FC-GO-PACKAGE-ID-CHANGED-FILE); pytest, jest, vitest ids use the changed-file substring check | Mechanism: repo_profile.ts cargoTouched makes every failing full-suite check ineligible, whatever its command (make test and npm test wrap cargo), when any changed file is .rs, Cargo.toml, Cargo.lock or build.rs, and the line gains "NOT PROVEN: <name> may run cargo tests and the diff edits Rust source or a manifest" | Fixture: loki-ts/tests/runner/harness_intelligence.test.ts "FC-CARGO-ID-CHANGED-CRATE"; mutants: drop the cargoTouched term, or key it on the command name again (wrapper fixture goes red); walk only the root go.mod (nested-module fixture goes red); ignore go.mod and go.sum edits (module-file fixture goes red) |
1959
-
1960
-
1961
- ## FC-GUARD-FIXTURE-MISSES-NEW-DEPENDENCY A change that added a guarded token (98a3c7a67 codex bench adapter) or a runtime file read (77f1196c5 secret-paths.json) did not update the guard fixture, so claude_flags.test.ts P12-26b and the dist-layout test went red on main
1962
- - User-visible vs raw: raw only. P12-26b fired on docstring prose in benchmarks/bench/adapters/codex.py, not a launch. The layout test gap fails closed to plan, never to skip-permissions.
1963
- - Law broken: a change that adds a guarded token or a runtime file read updates the guard's allowlist or fixture in the same slice.
1964
- - Siblings swept: claude_flags.ts loadPermissionsModule (repo-root and dist paths); autonomy/lib/claude-flags.sh and tests/test-claude-flags.sh (bash route, does not load the compiler); os_sandbox.py and tests/lib/test_os_sandbox.py (repo-relative secret-paths.json reads, unaffected); SCOPED_OUT rows claude_code.py and attribution.py (bench arms, same rationale as codex.py).
1965
- - Mechanism: bench adapters register in SCOPED_OUT when added. The layout test copy set follows the compiler's declared dependencies (pd/303d, not this row).
1966
- - Fixture: claude_flags.test.ts "P12-26b" goes red for any new unlisted benchmarks/ file carrying the flag.
1967
- - Open: the dist-layout half (secret-paths.json copy) is owned by pd/303d 1be1e118d.
1968
-
1969
- ## FC-VERIFY-PATH-REROOTED-UNDER-RUNS A `loki verify` argument that is a path to a missing receipt was treated as a run id, so the error named a path that never existed
1970
- - User-visible vs raw: user-visible. `loki verify .loki/runs/e10-nope/receipt.json` printed `receipt not found: .loki/runs/.loki/runs/e10-nope/receipt.json/receipt.json`.
1971
- - Law: a path the user typed is reported as given; the reporter never invents a location.
1972
- - Cause (measured): engine10/verify_cmd.ts treated only an EXISTING file as a path, so a missing one fell through to `join(runsRoot, runId, "receipt.json")`.
1973
- - Siblings swept: run id form (unchanged), absolute path, a receipt copied elsewhere, a non-receipt .json (diff verifier).
1974
- - Mechanism: an argument containing a path separator or ending in .json is a path whether or not the file exists.
1975
- - Fixture: tests/test-verify-receipt-route.sh "a missing receipt at a run path is UNCHECKED rc 2 with the path as given" (red before, green after).
1976
-
1977
-
1978
- ## FC-IMPLEMENT-RESUME-STARTS-OVER A run killed mid-implement resumes with a fresh implement session, so the 21s AC5 bound (#353) is unmet when the killed session wrote nothing
1979
- - User-visible vs raw: user-visible (the resume prints "resume from stage implement" but redoes the whole stage; elapsed time, not the verdict, is what is wrong).
1980
- - Law: the append-only event log is the durable checkpoint store (run_outputs.ts P12-07); it must hold the one fact needed to continue a stage, the provider session id. Today it does not, so resume cannot continue the killed session.
1981
- - Cause (measured): ac5-run3 (~/loki-ci-logs/ac5-run3.json) killed the run at 64.93s, about 4s into [implement]; auto-resume printed "resume from stage implement (settled: intake, plan, wall)" and reached VERIFIED in 51.46s at load 23.5. status_after_crash was "?? tests/": the killed session had written no source file, so nothing on disk could be reused. ac5-run1, a fresh run at lower load, timed "implement 20s, verify 1s, commit 0s, seal 1s", so one fresh implement session is about 20s and 21s is below it. At origin/main 8e4a9be54 the session id reaches .loki/e10-session-<iter>.json only after the stream ends (providers.ts:948 writeSessionId, after consumeSdkStream), so a SIGKILL loses it, and resumeMain treats phase implement as not auto-resumable (stage state is in memory). Not measured: the per-stage split of the 51.46s resumed run (its loki-run dir is gone).
1982
- - Siblings swept (does the stage lose its session id on SIGKILL, from the same writeSessionId-after-stream-end site): implement yes (one provider session, id written at stream end); fix yes (runFixSession and fix.ts chain the id from the prior round's event data, so the in-flight round loses it); deep yes but harmless today (sessionCouncil runs a read-only reviewer session, `-council` iteration, whose id nothing resumes); xreview no (it spawns a one-shot second-provider CLI and keeps no session chain; a kill reruns it, and no id exists to lose).
1983
- - Mechanism: write the provider session id at first sight and copy it into the event log, then have resume consume it. Cards: rq-82 (durable id at the first stream frame, mirrored in the heartbeat event) and rq-83 (resume continues the killed provider session). Neither is built; AC5 at 21s holds only if the resumed implement continues the killed session or skips work already on disk.
1984
- - Related rows: FC-CRASH-RERUN-STARTS-OVER and FC-CRASH-WORK-READ-AS-PREEXISTING (cw18c, cw18-r6) cover a crash that left files on disk; this row covers the case where the killed session wrote none. They are separate rows, not merged.
1985
- - Fixture: rq-84's test, to be named when it lands (the resume-continues-session test). No fixture exists yet; this row is a diagnosis, no code.
1986
-
1987
- ## FC-EGRESS-AUDIT-HAND-LISTED `loki doctor --airgap` named one egress host, so an operator who set a webhook, tracker or MCP url believed the install was sealed
1988
- - User-visible vs raw: user-visible. With LOKI_WEBHOOK_URL, LOKI_JIRA_URL or a remote `url` MCP server configured, the audit listed only model inference, telemetry and the update check, and exited as if nothing else could leave the machine.
1989
- - Law: an audit of outbound traffic derives its list from what the code actually reads, not from what its author remembered; it names a host, never a URL, path, query or token.
1990
- - Cause (measured): `_loki_airgap_audit` held a hand-written list of three rows. A grep of `LOKI_[A-Z_]*(URL|HOST|ENDPOINT|WEBHOOK)` over autonomy, loki-ts/src and providers found names that are not in that list; classified by reading each reader, 16 are egress (now in the registry) and 23 are internal variables, links or secrets never contacted (now in the non-egress list with a reason). The rq-08 card named `LOKI_SLACK_WEBHOOK_URL` as the Slack reader (the bash reader is `LOKI_SLACK_WEBHOOK`, the loki-ts one is `LOKI_SLACK_WEBHOOK_URL`), claimed an `LOKI_MCP_<NAME>_URL` reader (none exists) and `LOKI_DEPLOY_HEALTH_URL` as egress (a flag-only internal variable).
1991
- - Siblings swept: telemetry row (printed the full LOKI_TELEMETRY_ENDPOINT url including path; now host only), update check, model inference (unchanged, still the only REQUIRED row and the only one that sets the exit code), JSON output (same rows with `required: false`), a server name or env value holding a tab, newline or other control character (cleaned to `?` where each record is emitted, and the JSON reader skips a malformed line instead of raising), the three MCP config files (`.mcp.json`, `$LOKI_DIR/mcp-config.json`, `~/.claude/mcp.json`; `url` servers only, stdio servers are local). Unicode line separators U+0085, U+2028 and U+2029 in a server name or url host are masked like C0 and the --json loader splits on "\n" only (splitlines() would break on them and hide hosts).
1992
- - Mechanism: `_LOKI_AIRGAP_EGRESS_ENV` (name and meaning) and `_LOKI_AIRGAP_EGRESS_NOT` (name and reason) sit beside the audit; `_loki_airgap_configured_egress` emits one row per set registry name plus one per MCP url server, host only, with a loopback flag and the exact disable step. tests/test-airgap-commands.sh section 6 greps the tree and fails on any egress-looking name in neither list.
1993
- - Fixture: tests/test-airgap-commands.sh section 6: every registry name set to a host is named (path not leaked), JSON carries the host, nothing configured reports no extra rows and exit 0, an MCP url server is named and a stdio server is not, the tree guard is green, and a fixture tree with a fake `LOKI_FOO_URL` reader turns it red.
1994
-
1995
-
1996
- ## FC-WORKER-NO-PROCESS-LIMITS The engine10 worker and every process it starts ran with the caller's resource limits, so a runaway build or fork loop in a worker could exhaust memory or the process table of the whole machine (#265 slice B)
1997
-
1998
- - User-visible vs raw: user-visible (a machine-wide stall during a run).
1999
- - Law: an explicit opt-in limit must either hold for the worker tree or stop the run with a named reason; it must never run unlimited silently, and an unset knob must leave the spawn untouched.
2000
- - Cause (measured): `git grep -n ulimit -- autonomy providers loki-ts/src` returned no hit in any spawn wrapper, and spawnWorker in loki-ts/src/runner/worker_proc.ts passed [cmd, ...args] straight to spawn. Node cannot set rlimits, so a shell wrapper at this one call site is the only mechanism.
2001
- - Siblings swept: bash-route provider spawns are owned by the #335 confinement wrapper, and the subagent cap is #265 slice C; both are out of scope here. The detached deep-verify spawn in loki-ts/src/engine10/supervisor.ts is the same class (a model-driven child under the worker env) and now goes through `ulimitArgv` too (r2); an invalid knob records `deep verify not spawned: <reason>` in NOT PROVEN instead of spawning. Mutant: spawn the raw deepArgv and the new deep_wiring test (ulimit -u reads 2000 in the deep child) goes red.
2002
- - Mechanism: ulimitArgv in runner/worker_proc.ts wraps argv in `/bin/sh -c 'ulimit ... || exit 97; shift N; exec "$@"'` only when LOKI_ULIMIT_VMEM_KB or LOKI_ULIMIT_NPROC is set. Values travel as positional arguments, never in the script text, and a value that is not a positive integer is refused before any spawn. A ulimit the OS refuses (macOS rejects -v) exits 97 with a named reason.
2003
- - Fixture: loki-ts/tests/runner/worker_proc.test.ts covers the applied limit, the byte-identical unset argv, the refused injection value, the exit-97 path, and a mutant that drops the exit.
2004
-
2005
- ## FC-L0-GUARD-RED-ON-MAIN
2006
- - User-visible vs raw: raw (tests/engine10/l0_guard.test.ts red on main e4e5043ea, 2 of 3 tests failing; no user effect).
2007
- - Law broken: Engine Law gate integrity. A guard that is red on main means merged slices bypassed it, so it no longer protects the generic-not-per-stack rule.
2008
- - Cause (measured, git log -S): six new hits landed on 2026-10-23 in 6912e4a21 (failures.ts go test and cargo test, repo_profile.ts npm test comment), 8e5dc5028 (verify.ts pyproject.toml, requirements.txt) and e409e4b91 (util/shell.ts go test comment); 62f946e19 removed wall.ts jest and vitest without shrinking the allowlist. scripts/select-tests.sh --files-from on the 8e5dc5028 file list DOES select loki-ts/tests/engine10/l0_guard.test.ts (R8, measured), so the selector is not the gap; the slice gates that approved these commits did not run or did not heed the selected guard, and only the post-merge run showed it red. The guard itself was always correct.
2009
- - Siblings swept: all six hits plus both stale rows, decided per hit: 3 comment-only hits reworded away (repo_profile.ts, util/shell.ts) or kept as output-parser rows (failures.ts go and cargo, same role as the listed jest, pytest, vitest rows); verify.ts manifests allowlisted with a reason (lint tool detection of the target repo as data); 2 stale wall.ts rows removed.
2010
- - Mechanism: the allowlist stays reviewed one row at a time; no regeneration.
2011
- - Fixture: tests/engine10/l0_guard.test.ts 3 pass; mutation: re-adding a removed probe makes "no unlisted manifest probe" red.
2012
-
2013
- ## FC-PULSE-MEASURES-STALE-LOCAL-MAIN The pulse measured the local `main` branch, which releases pushed from worktrees never fast-forward, so seven metrics read UNKNOWN or stale with no line saying why (RQ-98)
2014
- - User-visible vs raw: user-visible. The pulse printed `Main CI (main @ 37f4d4f3): UNKNOWN` while `git rev-list --count main..origin/main` printed 288.
2015
- - Law broken: a status report never describes a main that is behind origin/main without saying so, and it measures the real main.
2016
- - Siblings swept: every MAIN_REF consumer (Main CI, unreleased-merge age, release cadence, moat baseline, unevidenced-claim scan, release-on-red) shares one resolved ref; the `gh --branch` streak call keeps the branch name `main`.
2017
- - Mechanism: one resolution in scripts/v10-pulse.sh. PULSE_MAIN_REF unset and local main a strict ancestor of refs/remotes/origin/main: MAIN_REF becomes the remote-tracking ref and STALE_MAIN_REF fires with N. Diverged: STALE_MAIN_REF says diverged and the ref is unchanged. An explicit PULSE_MAIN_REF wins. No fetch.
2018
- - Fixture: tests/test-v10-pulse.sh "RQ-98" (behind by 2, equal, diverged, explicit pin; two mutants).
2019
-
2020
- ## FC-VERDICT-FROM-UNKNOWN-INPUT The pulse emitted `VIOLATION: TRAIN_LATE` at main 530b63296 off a merged-but-unreleased count taken from a stale local tag (v11.70.4, four minors behind VERSION 11.74.0)
2021
- - User-visible vs raw: user-visible. Main was fully released (`git log -1 main` = "release: v11.74.0"), yet the pulse told the operator to push a release train.
2022
- - Root cause: a local tag was used as release truth. The main checkout does not fetch tags, so the newest local tag lags by whole minors and every count "since <tag>" is wrong. Gating the verdicts on a tag-vs-npm mismatch (v11.74.1) only turned the wrong answer into UNKNOWN.
2023
- - Law broken: a verdict is never computed from an input that is not release truth, and a disagreement between release sources is reported as a named state (publish pending or lagging), not hidden as UNKNOWN.
2024
- - Siblings swept: every reader of release state in scripts/v10-pulse.sh: the unreleased-age block and UNRELEASED_MERGE, TRAIN_LATE, check_release_cadence_d37, check_d44 (green unreleased main), check_merged_not_released_stale. All read the one `unreleased` result built from the resolver. Open sibling: the moat baseline still resolves its release by tag describe.
2025
- - Mechanism: ONE resolver `last_release_point()` in scripts/v10-pulse.sh: the last commit on main that changed VERSION (via the existing `resolve_version_bump_sha()`) plus the VERSION value there, cross-checked against npm `next`. A mismatch prints "Release point: ... npm next is X (publish pending or lagging ...)" and the commit count stays valid; only an unreadable VERSION history is UNKNOWN. The tag-based counting, the tag-vs-npm mismatch branch and `_unreleased_count_unknown()` are deleted.
2026
- - Fixture: tests/test-v10-pulse.sh "FC-VERDICT-FROM-UNKNOWN-INPUT" (tag v1.0.0, VERSION 1.0.3, npm next 1.0.3, push 26 min old: 0 commits after the release commit gives no TRAIN_LATE, 1 commit gives TRAIN_LATE) and T40 (VERSION 9.54.2 vs npm next 9.55.0: explicit "Release point" line, not UNKNOWN).