loki-mode 11.74.2 → 11.75.1

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 (166) 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/browser-open.sh +1 -1
  8. package/autonomy/lib/chat.sh +1 -1
  9. package/autonomy/lib/cockpit-render.sh +1 -1
  10. package/autonomy/lib/done-recognition.sh +4 -4
  11. package/autonomy/lib/efficiency_cost.py +1 -1
  12. package/autonomy/lib/engine10-push.sh +2 -2
  13. package/autonomy/lib/prd-enrich.sh +1 -1
  14. package/autonomy/lib/proof-check.sh +1 -1
  15. package/autonomy/lib/proof-generator.py +3 -3
  16. package/autonomy/lib/proof-pr.sh +1 -1
  17. package/autonomy/lib/skill-link-heal.sh +1 -1
  18. package/autonomy/lib/voter-agents.sh +1 -1
  19. package/autonomy/loki +26 -37
  20. package/autonomy/prd-checklist.sh +1 -1
  21. package/autonomy/receipt_jwt.py +1 -1
  22. package/autonomy/run.sh +28 -28
  23. package/autonomy/trigger-schedule.py +6 -414
  24. package/autonomy/tui.sh +1 -1
  25. package/autonomy/verify.sh +4 -4
  26. package/completions/loki.bash +0 -5
  27. package/dashboard/__init__.py +1 -1
  28. package/docs/ARCHITECTURE.md +1 -2
  29. package/docs/CLI-REFERENCE.md +1 -1
  30. package/docs/COMPONENTS.md +1 -1
  31. package/docs/INSTALLATION.md +1 -1
  32. package/docs/ROUTING-EVIDENCE.md +1 -2
  33. package/docs/V10-RESEARCH.md +1 -1
  34. package/docs/dev/architecture-reference.md +3 -7
  35. package/docs/dev/project-structure.md +1 -4
  36. package/docs/dev/release-checklist.md +13 -31
  37. package/docs/research-2026-07/OBSERVABILITY-TOOL-SPEC.md +1 -1
  38. package/docs/v11/MARKET-SLICES.md +1 -1
  39. package/docs/v11/REGISTRIES.md +1 -1
  40. package/docs/v11/ROUTER-1.md +2 -2
  41. package/loki-ts/dist/loki.js +2 -2
  42. package/mcp/__init__.py +1 -1
  43. package/package.json +2 -2
  44. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  45. package/skills/extending.md +2 -3
  46. package/skills/factory-operations.md +11 -18
  47. package/skills/release-cadence.md +2 -2
  48. package/web-app/dist/assets/{AdminPage-jkjilfbl.js → AdminPage-xKgtIhk2.js} +1 -1
  49. package/web-app/dist/assets/{Avatar-bd5JWkSJ.js → Avatar-Q14AHox9.js} +1 -1
  50. package/web-app/dist/assets/{Badge-DVRit2Xw.js → Badge-yk5M-lip.js} +1 -1
  51. package/web-app/dist/assets/{Button-BNAB-pVZ.js → Button-ynznk0At.js} +1 -1
  52. package/web-app/dist/assets/{CockpitPage-DpG29h_w.js → CockpitPage-C8B765za.js} +1 -1
  53. package/web-app/dist/assets/{ComparePage-Dasg5V41.js → ComparePage-DqzmWKnf.js} +1 -1
  54. package/web-app/dist/assets/{ErrorBoundary-DJojse6b.js → ErrorBoundary-DdBPd_Lq.js} +1 -1
  55. package/web-app/dist/assets/{EvidenceReceiptPanel-Blxw7_u1.js → EvidenceReceiptPanel-jb4Tp38b.js} +1 -1
  56. package/web-app/dist/assets/{GitHubIssuesPanel-BBvSFj80.js → GitHubIssuesPanel-xJQiLcxs.js} +1 -1
  57. package/web-app/dist/assets/{GitHubPRsPanel-BCIKeSBg.js → GitHubPRsPanel-84W3Dhk_.js} +1 -1
  58. package/web-app/dist/assets/{HomePage-C7d__oNG.js → HomePage-St1Xl2tu.js} +2 -2
  59. package/web-app/dist/assets/{LoginPage-BXljL3yY.js → LoginPage-jRt0NUHb.js} +1 -1
  60. package/web-app/dist/assets/{MagicPage-Xjkts_yE.js → MagicPage-CQLFe7LR.js} +1 -1
  61. package/web-app/dist/assets/{MetricsPage-kQvVp-j2.js → MetricsPage-zzX9eTC5.js} +1 -1
  62. package/web-app/dist/assets/{NotFoundPage-D-4ZlEaO.js → NotFoundPage-DDu023SY.js} +1 -1
  63. package/web-app/dist/assets/{ProjectPage-DfzR5RXP.js → ProjectPage-D60UURaH.js} +3 -3
  64. package/web-app/dist/assets/{ProjectsPage-DFDsWzkV.js → ProjectsPage-DqVsKxlt.js} +1 -1
  65. package/web-app/dist/assets/{SettingsPage-CLPIoYT3.js → SettingsPage-G7pPKvo0.js} +1 -1
  66. package/web-app/dist/assets/{ShowcasePage-PZKkyGeY.js → ShowcasePage-CrU_aKgS.js} +1 -1
  67. package/web-app/dist/assets/{SystemSettingsPage-BsBlcnzo.js → SystemSettingsPage-ClKGQFsB.js} +1 -1
  68. package/web-app/dist/assets/{TeamsPage-BSjgNhoU.js → TeamsPage-B530vSNe.js} +1 -1
  69. package/web-app/dist/assets/{TemplatesPage-W2KeAzbg.js → TemplatesPage-COmv_Cqd.js} +1 -1
  70. package/web-app/dist/assets/{TerminalOutput-BjroTiCC.js → TerminalOutput-CufG9YzF.js} +1 -1
  71. package/web-app/dist/assets/{activity-BXZ5YYes.js → activity-Ct1Updr6.js} +1 -1
  72. package/web-app/dist/assets/{bell-D1urp5pa.js → bell-rcvhQuGr.js} +1 -1
  73. package/web-app/dist/assets/{bot-Chedf2bF.js → bot-DzeqEI-b.js} +1 -1
  74. package/web-app/dist/assets/{check-DhaCuOCV.js → check-zacnZHyC.js} +1 -1
  75. package/web-app/dist/assets/{chevron-left-D7BpQyA6.js → chevron-left-Lxnkl8Qg.js} +1 -1
  76. package/web-app/dist/assets/{circle-alert-DnGvQ_AQ.js → circle-alert-DoqSdQdB.js} +1 -1
  77. package/web-app/dist/assets/{clock-CqfxWhHM.js → clock-B7bJcVIC.js} +1 -1
  78. package/web-app/dist/assets/{cloud-BKxp5AWB.js → cloud-DdA9Bu6q.js} +1 -1
  79. package/web-app/dist/assets/{code-xml-BDQ6GBtt.js → code-xml-CAZEZ9T8.js} +1 -1
  80. package/web-app/dist/assets/{database-BmpEx1y_.js → database-Da2GkEIN.js} +1 -1
  81. package/web-app/dist/assets/{dollar-sign-DYNY9R_C.js → dollar-sign-DjDiG-37.js} +1 -1
  82. package/web-app/dist/assets/{file-code-corner-BJi0IBFd.js → file-code-corner-CSL-Jij-.js} +1 -1
  83. package/web-app/dist/assets/{file-plus-Dqyb2bcQ.js → file-plus-02DRA8pV.js} +1 -1
  84. package/web-app/dist/assets/{globe-DPh4RWfM.js → globe-CGyB90kK.js} +1 -1
  85. package/web-app/dist/assets/{hammer-B3xnuoA1.js → hammer-BaQB96TG.js} +1 -1
  86. package/web-app/dist/assets/{index-BpX8DcTr.js → index-lcoI1_Kk.js} +3 -3
  87. package/web-app/dist/assets/{layers-BRCmyCW8.js → layers-BWJBvoeg.js} +1 -1
  88. package/web-app/dist/assets/{loader-circle-n07Pky0M.js → loader-circle-CdcI0FeS.js} +1 -1
  89. package/web-app/dist/assets/{lock-BWqOuQmS.js → lock-BBkw5NJx.js} +1 -1
  90. package/web-app/dist/assets/{package-BJlB_Z-Z.js → package-BpPRO3LM.js} +1 -1
  91. package/web-app/dist/assets/{plus-oH17BZu2.js → plus-D7XbyK29.js} +1 -1
  92. package/web-app/dist/assets/{refresh-cw-CPQ476LL.js → refresh-cw-DLG3hyAi.js} +1 -1
  93. package/web-app/dist/assets/{rotate-ccw-7hLvOcnS.js → rotate-ccw-Bqau4R_S.js} +1 -1
  94. package/web-app/dist/assets/{scroll-text-BCmLauQn.js → scroll-text-BtIL3cNk.js} +1 -1
  95. package/web-app/dist/assets/{server-DfTfvW84.js → server-tZCmZ4yH.js} +1 -1
  96. package/web-app/dist/assets/{shield-alert-CaeGRq19.js → shield-alert-K5JXzFKg.js} +1 -1
  97. package/web-app/dist/assets/{thumbs-up-Dz-l2GPC.js → thumbs-up-CHCVyJdw.js} +1 -1
  98. package/web-app/dist/assets/{trash-2-B-OsDmSo.js → trash-2-DncNxVq0.js} +1 -1
  99. package/web-app/dist/assets/{trending-down-CXTFOxzq.js → trending-down-D8Bp0azX.js} +1 -1
  100. package/web-app/dist/assets/{trending-up-FQEdqr3v.js → trending-up-AixETdKR.js} +1 -1
  101. package/web-app/dist/assets/{upload-CacWSNTg.js → upload-VCbjFn2H.js} +1 -1
  102. package/web-app/dist/assets/{usePolling-cIjZ9qd2.js → usePolling-CQvKD2Jj.js} +1 -1
  103. package/web-app/dist/assets/{user-DAouWFHi.js → user-SK-YguQ6.js} +1 -1
  104. package/web-app/dist/index.html +1 -1
  105. package/autonomy/company-seats.json +0 -82
  106. package/docs/COMPETITIVE-INTEL-2026-09.md +0 -444
  107. package/docs/FOUNDER-DECISIONS-TRUST-SEMANTICS-2026-08-01.md +0 -145
  108. package/docs/FOUNDER-STATUS-AND-DECISIONS-2026-07-01.md +0 -132
  109. package/docs/LOKI-10-BUILD-PROMPT.md +0 -350
  110. package/docs/STRATEGY-2026-2028.md +0 -124
  111. package/docs/V10-VISION.md +0 -161
  112. package/docs/v10/ADOPTION-PLAN.md +0 -375
  113. package/docs/v10/ARCHITECT-CUTS.md +0 -744
  114. package/docs/v10/BACKLOG.md +0 -231
  115. package/docs/v10/BOARD.md +0 -1403
  116. package/docs/v10/CONTROL-PLANE.md +0 -120
  117. package/docs/v10/CONTROL.md +0 -36
  118. package/docs/v10/CP-ASK-PLAN.md +0 -85
  119. package/docs/v10/CP-ENTERPRISE-UI.md +0 -537
  120. package/docs/v10/D51-PHASE-B.md +0 -120
  121. package/docs/v10/D61-SPEED.md +0 -105
  122. package/docs/v10/D63-CARDS.md +0 -354
  123. package/docs/v10/DECISIONS.md +0 -915
  124. package/docs/v10/E2E-D65.md +0 -123
  125. package/docs/v10/ENGINE-LAWS.md +0 -163
  126. package/docs/v10/ENGINE-MEASURE.md +0 -69
  127. package/docs/v10/ENGINE.md +0 -743
  128. package/docs/v10/FAILURE-CLASSES.md +0 -2026
  129. package/docs/v10/FC21B-FC22-PLAN.md +0 -107
  130. package/docs/v10/FIRST-RUN-GATE.md +0 -39
  131. package/docs/v10/FOUNDER-QUEUE.md +0 -29
  132. package/docs/v10/GAP-RESEARCH-1003.md +0 -32
  133. package/docs/v10/GUARDS.md +0 -883
  134. package/docs/v10/L0-WAVE1.md +0 -81
  135. package/docs/v10/LEGACY-REMOVAL.md +0 -408
  136. package/docs/v10/MEDIUM-ANALYSIS.md +0 -346
  137. package/docs/v10/METRICS.md +0 -645
  138. package/docs/v10/MODERNIZE.md +0 -437
  139. package/docs/v10/OPENCODE-PARITY-PLAN.md +0 -84
  140. package/docs/v10/OPERATING-MODEL.md +0 -60
  141. package/docs/v10/PLAN-V12.md +0 -896
  142. package/docs/v10/PROGRESS.md +0 -2735
  143. package/docs/v10/RELEASE-11.md +0 -77
  144. package/docs/v10/RETRO-2026.md +0 -650
  145. package/docs/v10/ROADMAP-D63.md +0 -35
  146. package/docs/v10/SCALE.md +0 -140
  147. package/docs/v10/SCORECARD-PLAN.md +0 -250
  148. package/docs/v10/SWARM-PROMPT.md +0 -98
  149. package/docs/v10/SWARM.md +0 -293
  150. package/docs/v10/cp-redesign/PARITY.md +0 -188
  151. package/docs/v10/cp-redesign/legacy/INDEX.md +0 -1167
  152. package/docs/v10/roadmap-10/01-measurement.md +0 -113
  153. package/docs/v10/roadmap-10/02-cost.md +0 -75
  154. package/docs/v10/roadmap-10/03-speed.md +0 -65
  155. package/docs/v10/roadmap-10/04-first-run.md +0 -61
  156. package/docs/v10/roadmap-10/05-deploy.md +0 -61
  157. package/docs/v10/roadmap-10/06-stability.md +0 -64
  158. package/docs/v10/roadmap-10/07-quality.md +0 -57
  159. package/docs/v10/roadmap-10/08-proof.md +0 -48
  160. package/docs/v10/roadmap-10/09-safety.md +0 -74
  161. package/docs/v10/roadmap-10/README.md +0 -1464
  162. package/skills/sdlc-fleet.md +0 -170
  163. package/tools/cockpit/SPEC.md +0 -275
  164. package/tools/cockpit/index.html +0 -146
  165. package/tools/cockpit/roi-basis.json +0 -33
  166. package/tools/cockpit/server.py +0 -946
@@ -1,883 +0,0 @@
1
- # Guards
2
-
3
- D26 guard 5 (incident-to-guard). One entry per incident, four fields each:
4
- incident, root cause with evidence, the guard, and the test that proves it
5
- fires. A guard with no checked-in test is marked PENDING with the slice ID
6
- that owns building it -- never claimed as done. Newest last.
7
-
8
- MERGED status is judged by whether the change is reachable from main,
9
- INCLUDING a patch-equivalent cherry-pick under a different SHA -- never by
10
- whether the original commit SHA is literally an ancestor of main. Check
11
- both: `git merge-base --is-ancestor <sha> main` for a direct ancestor, and
12
- `git cherry main <sha>` (a `-` line means an equivalent patch already
13
- exists on main) or `git log main --grep 'cherry picked from commit <sha>'`
14
- for a cherry-pick. A stale re-verification using only the ancestor check
15
- previously mismarked two merged guards (S-16, S-74) as PENDING.
16
-
17
- ## 1. Unscoped `pkill` terminated unrelated sessions (D14, D15)
18
-
19
- - **Incident:** an unscoped `pkill -f` pattern match, in a test and
20
- separately in the session-teardown path, killed unrelated processes --
21
- including, three times in one session, the session's own CLI process, and
22
- separately, other users' unrelated Claude Code sessions on the same
23
- machine at the end of any signal-path session close.
24
- - **Root cause with evidence:** D14 -- `tests/test-backend-floor.sh:43` ran
25
- `pkill -f "index.mjs"` with no PID or path scoping; reproduced directly by
26
- starting a decoy process with `index.mjs` in its argv and confirming the
27
- bare `pkill -f` killed it. Root-caused three unexplained mid-session
28
- terminations in about 25 minutes, each landing 2-4 minutes into a
29
- HIGH-tier 4-agent council review (DECISIONS.md D14). D15 -- a sibling bug:
30
- `kill_provider_child()` in `autonomy/run.sh` ran
31
- `pkill -f "^${proc}( |$)"` for claude/codex/aider/cline on every
32
- signal-path session end, with no scoping to a PID, process group, or
33
- working directory; reproduced with a decoy process sharing no relationship
34
- to the run except the matching command-line substring (DECISIONS.md D15).
35
- - **The guard:** D14 -- `tests/test-backend-floor.sh` (commit `61547203`)
36
- finds the PID actually LISTENing on the target port via
37
- `lsof -ti tcp:$PORT -sTCP:LISTEN` and kills only that PID (TERM then KILL);
38
- the already-scoped trap-line pkill on the unique mktemp path was left
39
- unchanged. D15 -- `kill_provider_child()` in `autonomy/run.sh` (commit
40
- `9cd51d1a`) scopes its sweep to processes sharing this run's own process
41
- group id, never a bare name/pattern match across the whole machine. Both
42
- merged to main.
43
- - **The test that proves it fires:** `tests/test-backend-floor.sh` (the D14
44
- fix's decoy-process case, comment-anchored as "ROUND 3: the D14 fix"), and
45
- `tests/test-kill-provider-child-scoping.sh`, which proves both directions:
46
- a same-process-group leak is still cleaned up, and a different-process-group
47
- "unrelated session" decoy survives. That test itself needed two further
48
- hardening rounds (D16, D17) after review found its own decoy-selection and
49
- process-group isolation could reproduce the exact bug it was meant to
50
- guard against -- see those decisions for why a process-killing test must
51
- be reviewed at the same severity as the product code it tests.
52
-
53
- ## 2. `docs/v10/BOARD.md` emptied by a python heredoc (D18)
54
-
55
- - **Incident:** commit `de3a8503` committed `docs/v10/BOARD.md` as 0 bytes,
56
- silently destroying 13189 bytes of real coordination state (every GF/PF/S-NN
57
- row), with no diff review, no size check, and no read-back before the
58
- commit.
59
- - **Root cause with evidence:** a
60
- `python3 -c <<'EOF' ... open(p,'w').write(s) ... EOF` heredoc pattern
61
- applied a targeted edit via `re.sub` guarded only by `assert old in s`; an
62
- assertion failure aborts the script, but if the write happens (or a prior
63
- failed attempt already wrote a partial/empty `s`) before or independent of
64
- that guard, nothing catches it (DECISIONS.md D18).
65
- - **The guard:** interim discipline, in force today: every edit to a hot
66
- coordination file (BOARD.md, PROGRESS.md, DECISIONS.md, BACKLOG.md,
67
- METRICS.md) uses Read then Edit (which refuses to run unread and fails
68
- loudly on a no-match) or, if a script is genuinely needed, checks the
69
- resulting file's byte size against a sane lower bound and runs
70
- `git diff --stat` before `git add`, never committing a docs file whose diff
71
- shows only deletions with no matching insertions (DECISIONS.md D18).
72
- Mechanical enforcement at commit time -- a PreToolUse hook blocking a
73
- BOARD.md commit that drops existing rows (D26 guard 1) -- is
74
- `scripts/v10-guard.sh`, **MERGED (S-73)**: `db51311a`..`e9170698` are all
75
- direct ancestors of main (`git merge-base --is-ancestor <sha> main`),
76
- landing in release train 2 (v9.57.0, commit `5332bfc3`). A narrower
77
- mechanical guard for one specific write path (`board-row-status` in
78
- `scripts/v10-ops.sh`, which verifies the target row changed in exactly its
79
- Status cell and restores the original on any other mismatch) is also
80
- **MERGED (S-74)**, via cherry-pick `8fbbbb31` (verified below, section 7).
81
- - **The test that proves it fires:** none checked in for the general Read
82
- then Edit discipline -- it is a process rule, not code, so there is
83
- nothing to unit-test. `scripts/v10-guard.sh`'s BOARD.md-row-drop block and
84
- its test are **MERGED (S-73)**: `tests/test-v10-guard.sh` is present on
85
- main (925/640-line files respectively). `board-row-status`'s byte-for-byte
86
- post-write verification and its tests in `tests/test-v10-ops.sh` are
87
- **MERGED (S-74)** (same `8fbbbb31` cherry-pick).
88
-
89
- ## 3. CI self-cancellation on every push to main (D25)
90
-
91
- - **Incident:** pushing to main routinely cancelled the Tests workflow's own
92
- in-progress run on main -- 10 of the last 12 Tests runs on main were
93
- cancelled by the swarm's own subsequent pushes, not by any real code
94
- defect, against a real Tests runtime of 26-34 minutes and a push cadence
95
- of roughly 20 minutes, so main effectively never got a green verdict.
96
- - **Root cause with evidence:** `test.yml` (and, identically,
97
- `bun-parity.yml` and `security-audit.yml`) keyed its concurrency group on
98
- `${{ github.workflow }}-${{ github.ref }}` with `cancel-in-progress: true`
99
- unconditionally -- every push to main resolves to the same `ref`, so every
100
- push shared one group with the prior push's still-running job and
101
- cancelled it (DECISIONS.md D25).
102
- - **The guard:** first attempt, S-69 (commit `8cfa1eae`), scoped
103
- `cancel-in-progress` to `pull_request` events only in `test.yml`, leaving
104
- the group key unchanged -- this left a THIRD push able to still cancel a
105
- still-pending (queued, not yet running) main run, and covered only
106
- `test.yml`. Superseded before merge by S-80 (commit `d5d6bfa7`, also
107
- `3bd01787`), which instead keys the concurrency GROUP itself on
108
- `github.sha` for non-pull_request events, in all three gating workflows
109
- (`test.yml`, `bun-parity.yml`, `security-audit.yml`): every main commit
110
- gets its own group and can never be queued behind or cancelled by another
111
- commit's run, while PR runs keep the ref-keyed group and
112
- `cancel-in-progress: true` so a stale run of the SAME PR still cancels.
113
- - **The test that proves it fires:** verified with one real push landing
114
- without cancelling a concurrent run, per the S-80 slice's own acceptance
115
- check -- a GitHub Actions concurrency block cannot be exercised by a local
116
- unit test; it only takes effect on the real Actions runner. No fixture
117
- test exists or is planned for this guard.
118
-
119
- ## 4. Pulse's UNKNOWN main-CI reading suppressed UNRELEASED_MERGE (S-71, `59118705`)
120
-
121
- - **Incident:** `scripts/v10-pulse.sh`'s UNRELEASED_MERGE violation never
122
- fired while main's CI status read UNKNOWN, even once the unreleased age
123
- was well past its 30-minute budget -- an UNKNOWN CI reading silently read
124
- as "CI is fine, do nothing."
125
- - **Root cause with evidence:** UNRELEASED_MERGE's only firing condition was
126
- `age > 30 and ci_status == "green"`; `ci_status` is `None` when main CI
127
- reads UNKNOWN, so that comparison is always false and the violation can
128
- never fire under UNKNOWN. This was the single shared point causing the
129
- suppression -- every OTHER violation in the script already evaluated
130
- independently of `ci_status` (commit `59118705`'s own message).
131
- - **The guard:** commit `59118705` (cherry-picked from `b8f43d08`) adds a
132
- CI-independent `elif age > 45: ...` path alongside the existing
133
- green-CI-gated 30-minute branch, so UNRELEASED_MERGE can now fire on its
134
- own past 45 minutes regardless of CI status. The same commit also adds
135
- CI_CANCELLED_STREAK (3+ consecutive cancelled Tests runs on main),
136
- computed from its own dedicated `PULSE_GH_STREAK_CMD` call, independent of
137
- the main-CI-status lookup, so it too can fire when that lookup reads
138
- UNKNOWN.
139
- - **The test that proves it fires:** `tests/test-v10-pulse.sh` T18/T18b/T18c
140
- (streak fires at 3+ consecutive cancellations, does not fire on a
141
- non-consecutive run, reads UNKNOWN with no completed runs), T19/T19b
142
- (unreleased-merge fires at 45+ minutes under UNKNOWN main CI, does not
143
- fire at the 40-minute threshold), and T20 (LOW_READY still fires under
144
- UNKNOWN main CI, proving the fix did not just move the suppression
145
- elsewhere). Full suite 40/40, confirmed red against the pre-fix script.
146
-
147
- ## 5. Hung worktree classifier: `git status` touched the mtimes it measured (S-72)
148
-
149
- - **Incident:** S-72's worktree-cleanup pass used "the worktree's directory
150
- modified in the last 90 minutes" as its liveness signal for deciding which
151
- of 112 accumulated `.claude/worktrees` entries were safe to remove. That
152
- signal went dead mid-run -- pulse's own "Active builder worktrees" reading
153
- jumped to "123 of 129" -- because the cleanup pass's own `git -C <wt>
154
- status` calls refresh each worktree's index and touch its mtimes, so a
155
- live agent that has not yet committed or written a file looked exactly as
156
- safe to remove as a genuinely abandoned one, by every other rule the pass
157
- checked.
158
- - **Root cause with evidence:** PROGRESS.md, "Drift audit, turn 180": "S-72's
159
- worktree cleanup used 'directory modified in the last 90 minutes' as its
160
- liveness signal. Its own `git -C <wt> status` calls refresh each
161
- worktree's index and touch mtimes... the signal went dead... Caught before
162
- any removal (agent reported '0 worktrees removed so far')."
163
- - **The guard:** applied ad hoc, in the moment, during that same S-72
164
- cleanup pass: an explicit exclusion list of live and approved-unmerged
165
- worktrees, plus `.git` file birth time (`stat -f %B`) in place of mtime as
166
- the recency signal (PROGRESS.md, turn 180). That was a one-time operational
167
- fix. The standing guard is `scripts/v10-worktree-reap.sh --strict` (S-154,
168
- commit `e57262cf`, as `prune-worktrees.sh`; folded into the reaper in
169
- v11.6.0): it decides removal only from structural state (branch merged
170
- into main by ancestry or `git cherry`, worktree not locked, working tree
171
- clean, path under `.claude/worktrees/`) and never from a file-age signal.
172
- A live agent marks its worktree with `git worktree lock`, which the script
173
- always skips.
174
- - **The test that proves it fires:** `tests/test-worktree-reap.sh`
175
- (S-154). Its GUARDS 5 assertion greps the `strict_gate` function for `stat`,
176
- `mtime`, `-mmin` and `-newer` and fails on any hit ("strict gate uses no
177
- file-age signal (GUARDS 5)"); the same suite proves locked and dirty
178
- worktrees survive.
179
-
180
- ## 6. Over-budget trivial agents, no violation surfaced it (S-75, AGENT_OVER_BUDGET, `c16d875f`)
181
-
182
- - **Incident:** nothing in `scripts/v10-pulse.sh` compared an agent's real
183
- elapsed time on a BOARD.md row against its role/tier's time budget
184
- (founder-specified: builder LOW 15m, MEDIUM 30m, HIGH 60m; reviewer LOW
185
- and MEDIUM 30m, HIGH 60m), so a trivial agent running well past its budget
186
- produced no violation and nothing surfaced it.
187
- - **Root cause with evidence:** `scripts/v10-pulse.sh` had no per-role/tier
188
- time-budget check at all before this slice; BOARD.md rows already carry a
189
- Tier column and a status timestamp, but nothing computed elapsed time
190
- since that timestamp against a budget (commit `c16d875f`'s own message;
191
- D26 guard 3).
192
- - **The guard:** commit `c16d875f` (cherry-picked from `5c9a16f2`) extends
193
- `parse_board()` to also return each row's Tier (found the same
194
- position-independent way as its existing Status-cell scan, since BOARD.md
195
- has used at least four different column layouts), and adds a new
196
- AGENT_OVER_BUDGET violation: for every row currently `building` (builder)
197
- or `review` (reviewer), compute elapsed time since its status timestamp
198
- and compare to its (role, tier) budget, naming over-budget rows
199
- oldest-first. A row with no parseable Tier cell reports UNKNOWN
200
- (`agent_budget`) rather than being silently treated as in-budget.
201
- - **The test that proves it fires:** `tests/test-v10-pulse.sh` T21 (a LOW
202
- builder at 20 minutes fires), T21b (10 minutes does not), T21c/T21d (a
203
- HIGH review at 45 minutes does not fire, 65 minutes fires), T21e (a row
204
- with no Tier cell reports UNKNOWN, never silently in-budget). Full suite
205
- 45/45 (up from 40/40), confirmed red against the pre-fix script, and a
206
- budget-value mutation (LOW 15 -> 25 minutes) confirmed red on T21.
207
-
208
- ## 7. File-mode narrowing from mktemp+mv, 644 became 600 (S-16, recurred S-74)
209
-
210
- - **Incident:** a temp-file-then-move write pattern silently narrowed a
211
- tracked file's permissions from 644 to 600 on every write. First found in
212
- `scripts/release.sh`'s `apply_sed()` (S-16): every real
213
- `bump_all_version_files` release run silently narrowed all 14
214
- version-bumped files (VERSION, package.json, Dockerfile, and so on).
215
- Recurred in `scripts/v10-ops.sh`'s `board-row-status` (S-74 round 3): the
216
- identical bug class, in a different file, on a different write path --
217
- the S-16 fix's own verification did not generalize into a lesson that
218
- caught the second occurrence before review did.
219
- - **Root cause with evidence:** `mktemp` (and Python's `tempfile.mkstemp`)
220
- always creates its temp file at mode 600 regardless of umask; a
221
- same-directory `mv` (or `os.replace`) over the original keeps the TEMP
222
- file's own mode, not the original's. Git does not track non-exec mode
223
- bits, so the commit itself looks unchanged while the working tree's real
224
- permissions quietly regress (S-16 fix commit `254c3c85`; S-74 fix commit
225
- `e5dc3d28`, same underlying mechanism).
226
- - **The guard:** S-16, originally commit `254c3c85`, is **MERGED** -- re-
227
- verified against CURRENT main (this entry had gone stale: an earlier
228
- re-verification found `254c3c85` not an ancestor and `apply_sed` absent
229
- from main, which was true of main AT THAT TIME but is no longer true).
230
- `git log main --grep 254c3c85` finds `c681a726` ("fix(release): preserve
231
- file permissions in apply_sed (BACKLOG 22, S-16 rework)"), a rebase of the
232
- same fix, and `scripts/release.sh` on main today has `apply_sed()` with
233
- `cp -p "$file" "$tmp"` immediately after `mktemp`, before the
234
- `sed -E ... > "$tmp"` redirect (verified by reading main's
235
- `scripts/release.sh` directly, lines 144-153) -- `cp -p` copies the
236
- original's mode onto the temp file, and the subsequent `>` redirect
237
- truncates that inode's content in place rather than recreating it, so the
238
- copied mode survives. Judged correctly this time by whether the change
239
- (not the original SHA) is on main. S-74 (originally commit
240
- `e5dc3d28`) is **MERGED** -- landed on main as cherry-pick `8fbbbb31`
241
- (`git log main --grep 'cherry picked from commit e5dc3d28'` finds it, and
242
- `git cherry main e5dc3d28` reports it patch-equivalent, "-"): status must
243
- be judged by whether the change is on main INCLUDING patch-equivalent
244
- cherry-picks, not by whether the original SHA is literally an ancestor --
245
- `254c3c85` above genuinely has neither, `e5dc3d28` has the cherry-pick.
246
- `shutil.copymode(board, tmp_path)` before EACH `os.replace` call (the main
247
- write and the corruption-restore path), shared through one
248
- `_atomic_write()` helper so both preserve the real board file's mode, is
249
- live in `scripts/v10-ops.sh` on main today (`grep -c shutil.copymode` ->
250
- 2).
251
- - **The test that proves it fires:** S-16 -- `tests/test-release-sh.sh` is
252
- checked in on main and wired into `tests/run-all-tests.sh`
253
- ("release.sh version-bump preserves file mode (BACKLOG 22)", line 1180),
254
- added in commit `00a9425c`. **MERGED (S-16).** The manual-only
255
- verification this entry previously described (an isolated 644-in/600-out
256
- repro against the `s16-rework-permission-fix` branch, not main) is
257
- superseded by that checked-in test now running on every main commit --
258
- this is exactly the regression-suite gap the S-74 recurrence exposed,
259
- now closed. S-74 -- `tests/test-v10-ops.sh`'s
260
- "a 644 board stays 644 after a flip" and "...after a corruption-triggered
261
- restore" cases; **MERGED (S-74)** via `8fbbbb31`, same cherry-pick
262
- verification as above.
263
-
264
- ## 8. BOARD cell located by header index broke on short/long rows (S-74)
265
-
266
- - **Incident:** `board-row-status`'s Status-cell lookup, having just been
267
- fixed to locate the column via the table's header row instead of scanning
268
- cell shapes (closing a different bug -- a Branch/SHA cell shaped like
269
- `main@abc123` being mistaken for the Status cell), broke on real BOARD.md
270
- rows whose column count differs from their header's. A dropped or added
271
- column shifts every later cell, so a fixed header-derived index landed on
272
- the wrong cell (Notes, not Status); a legitimate call such as
273
- `board-row-status S-07 approved` exited 2 with a false "does not match"
274
- refusal instead of flipping the row.
275
- - **Root cause with evidence:** commit `9f8389b9`'s own measurement --
276
- `awk -F'|' '/^\| [A-Z]+-[0-9]+ /{print NF}' docs/v10/BOARD.md | sort | uniq -c`
277
- -> `2 10 / 30 8 / 47 9` -- roughly 40% of the real table's rows have a
278
- cell count that does not match their header's (30 missing the Acceptance
279
- checks column, 2 with an extra cell from an embedded `|` inside Notes), so
280
- a fixed header-column index is invalid on those rows.
281
- - **The guard:** commit `9f8389b9` (originally stacked on `e5dc3d28`) is
282
- **MERGED** -- its diff is patch-equivalent to what shipped on main
283
- (`git cherry main 9f8389b9` reports it "-"): `8fbbbb31`'s own commit
284
- message (section C) describes this exact header-lookup fallback as part
285
- of the same squashed cherry-pick that landed `e5dc3d28`'s mode-preservation
286
- fix, so both original SHAs are represented by the one `8fbbbb31` commit on
287
- main. It keeps the header-index lookup as the primary path only when the
288
- row's cell count equals its header's; when the counts differ, it falls
289
- back to locating the Status cell as the one cell matching a KNOWN
290
- lifecycle token plus a full UTC timestamp (never a loose shape scan), and
291
- refuses (exit 2) if zero or more than one cell matches either path.
292
- - **The test that proves it fires:** `tests/test-v10-ops.sh`'s sweep over
293
- all 79 real slice rows in `docs/v10/BOARD.md` (each on a fresh copy,
294
- asserting exactly one line and exactly one cell changed per flip,
295
- independent of the tool's own internal verification), plus isolated
296
- short-row and embedded-pipe-in-notes reproductions (originally commit
297
- `9f8389b9`, now part of `8fbbbb31` on main). Full suite 52/52 per
298
- `8fbbbb31`'s own message; a mutation forcing the fallback path always
299
- (`if len(cells) == len(header_cells): -> if True:`) sent tests red.
300
- **MERGED (S-74)** via `8fbbbb31`.
301
-
302
- ## 9. Push reported success through a pipe while the remote held the old SHA (D27)
303
-
304
- - **Incident:** train 2's first push attempt was killed by its own 10-minute
305
- command timeout. The second attempt reported success -- piped through a
306
- filter, exit code read 0 -- while `git ls-remote origin refs/heads/main`
307
- still showed the PRIOR head `b651b98d`, not the pushed SHA (DECISIONS.md
308
- D27 context). A push can read green while the remote never moved.
309
- - **Root cause with evidence:** the push command's real exit status was
310
- taken from the pipeline's last stage (the filter it was piped through),
311
- not from `git push` itself -- the same exit-code-through-a-pipe class this
312
- repo's own memory already names (`feedback-pipefail-sigpipe-inverts-probe`,
313
- `feedback-i-cancelled-my-own-release`): a pipe silently discards the
314
- left-hand command's real exit code unless `pipefail` is set and checked.
315
- D27's own fix direction is explicit: "never pipe git push."
316
- - **The guard:** D27 (Loki-approved) mandates (1) never pipe `git push`, and
317
- (2) a push counts as done only when `git ls-remote origin refs/heads/main`
318
- equals `git rev-parse HEAD`, recorded as evidence and mechanically
319
- enforced by a push helper in `scripts/v10-ops.sh` -- cut as slice **S-98
320
- push-main** (D27 items 3 and 6), `ready@2026-09-27T15:53Z` on BOARD.md as
321
- of this writing, not yet built. The train-2 and release pushes that
322
- followed D27 were manually verified this way instead (D27's own evidence
323
- line: `git rev-parse HEAD` = `git ls-remote origin refs/heads/main` =
324
- `89e350bd...` at 15:48:18Z, and `= 5332bfc3...` at 15:50:09Z), which is the
325
- discipline S-98 is meant to make structural instead of manual.
326
- - **The test that proves it fires:** **MERGED (S-98)** -- shipped in train 5
327
- as `cd1fb044` ("feat(S-98): TRAIN_LATE pulse violation + v10-ops push-main
328
- (D27 items 3, 6)"); `git cherry main b44571ba` on this repo reports `-
329
- b44571ba8a44527abdc183c189617d992b976bac` (equivalent patch already on
330
- main).
331
-
332
- ## 10. Local CI's fast tier exceeded the 10-minute command cap under swarm load (D27)
333
-
334
- - **Incident:** the "Local CI Before Every Push" fast-tier gate (mandated
335
- 2026-07-31) took more than 10 minutes to run under swarm load -- 90 of 173
336
- checks hit the 600-second cap and reported `EXIT=124` -- and the
337
- `.githooks/pre-push` hook's own serial `pytest` run alone exceeded 10
338
- minutes by itself. Train 2 sat unpushed for more than 89 minutes waiting
339
- on a gate that could not finish inside the timeout budget every command in
340
- this repo is bound to.
341
- - **Root cause with evidence:** the fast tier's own justification (this
342
- file's CLAUDE.md predecessor, "Local CI Before Every Push," 2026-07-31)
343
- was sized against a much smaller swarm; DECISIONS.md D27's own context
344
- line states both measurements directly ("90 of 173 checks at the 600s
345
- cap, EXIT=124" and "the pre-push hook's serial pytest alone exceeded 10
346
- minutes"). A gate that cannot finish inside the hard command cap is not a
347
- fast pre-push gate, it is a second full CI run duplicating GitHub Actions'
348
- own Tier B.
349
- - **The guard:** D27 (Loki-approved) retires the local-ci fast-tier mandate
350
- outright: before a push, only syntax checks (`bash -n`, `py_compile`) plus
351
- the slice's own tests, capped at 60 seconds, run locally; GitHub CI (Tier
352
- B) becomes the sole release gate. The mandate's retraction is already in
353
- force per DECISIONS.md D27 (choice 1). The corresponding hook change --
354
- `.githooks/pre-push` reduced to identity check + `bash -n` + a red-main
355
- warning only, no `pytest`, target under 5 seconds -- is cut as slice
356
- **S-97** (D27 item 2), `ready@2026-09-27T15:53Z` on BOARD.md as of this
357
- writing, not yet built.
358
- - **The test that proves it fires:** **MERGED (S-97)** -- shipped in train 5
359
- as `f85c643a` ("S-97: retire pytest from pre-push hook per D27"); `git
360
- cherry main b72e9ab7` on this repo reports `-
361
- b72e9ab7e730fc856766c0296a2f709a6f6af83b` (equivalent patch already on
362
- main).
363
-
364
- ## 11. A builder's glob `rm -f` ran in the shared scratchpad root (S-18)
365
-
366
- - **Incident:** during the S-18 build (Rule-of-Two SSH/credential hardening),
367
- a builder ran a glob-matched `rm -f` directly against the shared
368
- scratchpad root instead of a run-owned subdirectory, risking deletion of
369
- other concurrent agents' files in that same shared root. Disclosed inline
370
- on BOARD.md's S-18 row ("Incidents during the build: a glob rm in the
371
- shared scratchpad root ... (removed)").
372
- This is the same incident class this repo's own memory already names
373
- (`feedback-tmp-cleanup-glob-deleted-worktree`: `rm -rf /tmp/loki-*` matched
374
- and deleted an unrelated worktree) recurring in a different shared
375
- location.
376
- - **Root cause with evidence:** a glob pattern scoped only to a shared root
377
- directory (not to a single mkdir-created, ownership-marked, permission-
378
- narrowed subdirectory) matches every sibling file or directory any other
379
- concurrent agent has placed there at the same moment -- there is no
380
- per-run isolation boundary for the glob to respect. This repo's own
381
- `loki_run_tmp_create`/`loki_run_tmp_cleanup` convention (CLAUDE.md, "Test
382
- and Resource Cleanup") exists precisely to close this class for `/tmp`;
383
- the scratchpad root had no equivalent discipline applied at the time of
384
- this incident.
385
- - **The guard:** `scripts/v10-guard.sh` Rule 4, `rule4_glob_in_shared_root`
386
- (S-181). Any `rm` (with or without `-rf`) whose glob target has a literal
387
- prefix (the path before its first globbed component) that is exactly
388
- `/tmp`, `/private/tmp`, `$TMPDIR`, or a directory named `scratchpad` is
389
- blocked with exit 2, including `/tmp/loki-*/` and `/tmp/*/x.log`. A glob
390
- under a literal subdirectory of such a root (`/tmp/run-1/*.log`) is
391
- allowed. Known limit: a parent that is an
392
- unexpanded variable (`"$TMPDIR"/*`) is allowed, since the hook cannot see
393
- its value.
394
- - **The test that proves it fires:** `bash tests/test-v10-guard.sh`, section
395
- "Rule 4: glob rm directly in a shared root": `rm -f /tmp/*.log` and
396
- `rm -f <scratchpad>/*` blocked, `rm -f /tmp/run-1/*.log` allowed. Removing
397
- the `rule4_glob_in_shared_root` call fails the 7 blocked cases.
398
-
399
- ## 12. A test fixture wrote synthetic `insteadOf` lines to the real `~/.gitconfig` (S-18)
400
-
401
- - **Incident:** during the same S-18 build, a test/fixture for the P9
402
- Rule-of-Two credential work wrote 4 synthetic `insteadOf` lines directly
403
- into the operator's real `~/.gitconfig` instead of an isolated config file
404
- scoped to the test. Disclosed and remediated inline on BOARD.md's S-18 row
405
- ("4 synthetic insteadOf lines written to the real ~/.gitconfig (removed;
406
- grep of ~/.gitconfig for loki-test/insteadOf found none at 15:40)").
407
- - **Root cause with evidence:** a git-credential/URL-rewrite fixture used
408
- the ambient `$HOME` (and therefore the real `~/.gitconfig`) as its target
409
- instead of pointing git at an isolated config via `GIT_CONFIG_GLOBAL` or a
410
- scratch `HOME` override -- the exact hazard this session's own current
411
- task instructions explicitly call out ("Never touch the real
412
- `~/.gitconfig`; scratch files only in your own subdirectory"), confirming
413
- this is a known, recurring risk class for any test that touches git
414
- config, not a one-off.
415
- - **The guard:** S-138 (commits `cd661f3e` through `82b4aec1`). A lint
416
- refuses any test file that runs `git config --global` (any unambiguous
417
- prefix of `--global`) or writes the ambient `~/.gitconfig` unless the file
418
- first sources `tests/lib/isolated-git-home.sh` at top level, before any
419
- code that could save the real HOME. The helper points HOME and
420
- `GIT_CONFIG_GLOBAL` at a scratch path. The lint's own header lists its
421
- known ceiling (a home copied into another variable, obfuscated calls).
422
- - **The test that proves it fires:** `tests/test-no-ambient-gitconfig-writes.sh`
423
- (S-138), registered in `tests/run-all-tests.sh`. It scans every test file
424
- and fails on an offending line without the isolation prelude.
425
-
426
- ## 13. Slices marked released before `publish-npm` had actually succeeded (D27)
427
-
428
- - **Incident:** 11 BOARD.md rows (S-67, S-68, S-71, S-73, S-74, S-75, S-78,
429
- S-80, S-86, S-87, S-90) were each written as `released@2026-09-27T15:54Z`
430
- immediately after train 2's push landed and was `ls-remote`-verified, then
431
- each row's own Notes cell carries the identical self-correction:
432
- "Correction 15:54: I had marked this released too early." "Pushed and
433
- verified on the remote" was conflated with "released," before the
434
- `publish-npm` CI job had actually completed.
435
- - **Root cause with evidence:** DECISIONS.md D27 (choice 5) defines
436
- "released" precisely: "a train counts as released when publish-npm
437
- succeeds. The next train opens immediately, without waiting for npm's
438
- availability lag, Docker or Homebrew." At the moment those 11 rows were
439
- stamped `released`, only the push itself (verified via `ls-remote`) had
440
- happened -- a necessary but not sufficient condition under D27's own
441
- definition. Nothing mechanically checked `publish-npm`'s CI conclusion
442
- before the BOARD.md write, so a human (or agent) stamping the row could
443
- and did jump the gun on all 11 rows in the same train.
444
- - **The guard:** D27's own definition is the standing rule. S-139 (commit
445
- `bd6ffdee`) adds the pulse check `RELEASED_AHEAD_OF_NPM` in
446
- `scripts/v10-pulse.sh`: a BOARD row whose `released@` timestamp is later
447
- than npm's newest publish time raises a VIOLATION line in the pulse block.
448
- This is a flag, not a refusal: nothing blocks the BOARD write itself, so a
449
- premature `released` stamp still lands and is surfaced on the next pulse.
450
- A released row with no parseable timestamp reads UNKNOWN instead.
451
- - **The test that proves it fires:** `tests/test-v10-pulse.sh` case T39
452
- (S-139) asserts the VIOLATION fires for a `released@` row stamped after
453
- npm's newest publish; T39b asserts it stays silent for a row stamped
454
- before it.
455
-
456
- ## 14. Eval harness orphans survived the arm's process-group KILL (EV-10)
457
-
458
- - **Incident:** 00:08Z, pulse ORPHAN_WORKTREE flagged 6 orphaned processes
459
- left running after `eval/loki10/harness.py` runs against the legacy arm;
460
- all 6 were stopped by PID once found. Each was a detached
461
- `/tmp/loki-run-*.sh` loop legacy loki spawns via `setsid`, which had
462
- reparented to launchd (ppid 1) once the arm process it came from exited.
463
- - **Root cause with evidence:** `kill_orphans()` only ever did
464
- `os.killpg(timeout_pid, SIGKILL)` -- correct for anything still in the
465
- `timeout` process group the arm ran under, but a `setsid`'d child
466
- deliberately leaves that group and gets its own pgid, so the group KILL
467
- can never reach it once the escape has completed. Reproduced locally: a
468
- script that setsid-spawns a sleeper, confirms (by polling its pgid) that
469
- the escape finished, then exits still leaves the sleeper running after
470
- `capped_run()` returns and `kill_orphans()` has already run.
471
- - **The guard:** `kill_orphans()` now also reaps by two signals that are
472
- never a process name or pattern: any PID recorded in a `*.pid` file under
473
- the run's own clone (`<work>/.loki/**/*.pid`, one per line, never through
474
- a symlinked `.loki/` or a non-regular/oversized file), and any process
475
- anywhere on the box whose cwd (`/proc/<pid>/cwd` where present, else one
476
- system-wide `lsof -a -d cwd -Fpn` call, resolved with `os.path.realpath`)
477
- is that same clone directory or a path inside it. Both are scoped to
478
- `cwd`, the exact per-run clone `capped_run()` was given. Every candidate
479
- PID is vetted by `_pid_is_ours()` before it ever reaches `os.kill`: never
480
- `<= 1` (kill(2) treats -1/0 as "every process the caller may signal," not
481
- a single-PID cleanup), never this process or its parent, and never older
482
- than the run's own start (a stale or forged `.pid` entry naming an
483
- unrelated, longer-lived process is left alone).
484
- - **The test that proves it fires:** `eval/loki10/test-harness.sh`, EV-10.
485
- Leg Rj: the `setsidorphan` stub-arm mode setsid-spawns a sleeper in its
486
- own clone, polls until the sleeper's pgid equals its own pid and only
487
- then records an "escaped" marker (ruling out a vacuous pass from a spawn
488
- that never actually left the group), then exits; the leg asserts the
489
- marker is present and the sleeper is dead once the harness run returns.
490
- Leg Rk: the same escape, chdir'd out of the clone first so the cwd path
491
- cannot see it, reaped instead from its own PID in a `.loki/*.pid` file; a
492
- decoy PID that predates the run, planted in the same file (plus a literal
493
- `-1`), is asserted to survive. Leg Rl exercises `_clone_orphan_pids`
494
- directly to assert `-1`/`0`/`1` and a pre-run PID are filtered before any
495
- kill is attempted, without ever risking a live `kill(-1, SIGKILL)`.
496
- Mutation-tested: disabling the cwd path fails only Rj, disabling the
497
- pidfile path fails only Rk, and bypassing `_pid_is_ours` (checked offline
498
- against a throwaway copy of the module, never run against a live process)
499
- lets `-1` through, which Rl's assertion catches.
500
- ## 15. Two releases pushed with unbaselined secret-shaped fixtures never reached npm (E-60)
501
-
502
- - **Incident:** Security Audit (the `secret-scan` job in
503
- `.github/workflows/security-audit.yml`, a required-ci gate) failed on the
504
- release SHAs for v9.79.0 (`9dc0b1d3`, 2026-09-27) and v9.80.0 (`085306ae`,
505
- same day). Both pushes landed on main, but `publish-npm` never ran because
506
- the required Security Audit conclusion was red, so npm latest stayed at
507
- 9.78.0 through both releases -- 00:45Z is when the fix (`5190756f`) landed
508
- and unblocked v9.80.1.
509
- - **Root cause with evidence:** `5190756f`'s own commit message names the
510
- findings exactly: "a planted AWS-shaped string for the deep-verify
511
- secret-scan test and two CANARY/WITHHELD token canaries." Three new Loki 10
512
- test fixtures were secret-shaped (matched gitleaks' built-in rules) and
513
- landed on main across the two releases without a matching
514
- `.gitleaksignore` entry, so gitleaks' full-history scan in CI reported
515
- three new, unreviewed findings and blocked exactly as designed -- the gate
516
- worked; nothing local had run the same scan before push to catch it there
517
- instead of at CI, so two releases went out DOA on the fastest-available
518
- feedback channel (CI, not a local gate).
519
- - **The guard:** E-60 adds a fast-tier gitleaks step to `scripts/local-ci.sh`
520
- (`_FAST_KEEP` entry `"gitleaks (secrets, origin/main..HEAD)"`): when a
521
- `gitleaks` binary is on PATH, every pre-push run scans just the commits the
522
- branch adds on top of `origin/main` against the same reviewed
523
- `.gitleaksignore` baseline CI uses, so an unbaselined secret-shaped fixture
524
- is caught before push, not after a release SHA is already tagged. Fails
525
- closed the other way too: an absent binary prints an explicit SKIP line
526
- (never a silent PASS), matching this file's shellcheck-absent posture.
527
- - **The test that proves it fires:** `tests/test-local-ci-gitleaks.sh`
528
- (registered in `tests/run-all-tests.sh` and `tests/shard-durations.tsv`).
529
- Static assertions confirm the skip-not-pass path, the `.gitleaksignore`
530
- baseline, the `origin/main..HEAD` scope, and `_FAST_KEEP` membership; when
531
- a gitleaks binary is present, two live temp-repo scenarios drive the exact
532
- command shape `scripts/local-ci.sh` runs: a commit adding a literal
533
- AKIA-shaped token fails the step, and a commit adding the identical
534
- characters via source-level concatenation (never contiguous in the
535
- committed bytes) passes it.
536
-
537
- ## 16. A "far-future" `latest` version literal in a test went stale (E-73)
538
-
539
- - **Incident:** `tests/test-start-update-hint.sh` hardcoded `latest=9.99.0`
540
- (two occurrences, lines 130 and 163 as of `306b6b0c`'s parent) as a value
541
- meant to always read as "far in the future" so the update-available hint
542
- would reliably fire. The v10.0.0 major release made `9.99.0` older than
543
- the real `VERSION`, so the hint stopped firing and Tests shards 5/8 (this
544
- test) and 1/8 (trust-core baseline, rc 67) went red on commit `898fa081`.
545
- - **Root cause with evidence:** commit `306b6b0c` (`fix(tests): stale-install
546
- hint test used 9.99.0 as "far-future", older than 10.0.0`) states it
547
- directly: "The 10.0.0 bump made latest=9.99.0 older than VERSION, so the
548
- hint stayed silent." A literal future-version constant was compared only
549
- against the `VERSION` at the time it was written, with nothing keeping it
550
- ahead of later major bumps. The fix bumped the literal to `999.0.0`, but
551
- nothing stopped a second copy of the same mistake from being added
552
- elsewhere in tests/.
553
- - **The guard:** `tests/test-no-stale-future-version.sh` scans `tests/`
554
- (`.sh`, `.py`, `.ts`) for literal `latest` version assignments
555
- (`"latest":"X.Y.Z"`, `latest=X.Y.Z`, `latest: 'X.Y.Z'`) and fails, printing
556
- `file:line`, whenever a found version's major component is not strictly
557
- greater than the current `VERSION` major plus 10 -- so a planted
558
- "far-future" literal must stay comfortably ahead of the next several major
559
- releases, not just the one at the time it was written. Each `latest`
560
- occurrence is scored on its own matched version text, so a sibling version
561
- number elsewhere on the same line (a `"current"` or `"version"` key, say)
562
- can never mask or fake a violation.
563
- - **The test that proves it fires:** `tests/test-no-stale-future-version.sh`
564
- is itself the test: it runs its own scanner against the real `tests/` tree
565
- (must be clean on main) and against a synthetic fixture directory planting
566
- `latest="9.99.0"` alongside a `latest="999.0.0"` fixture and two
567
- mixed-line fixtures pairing a stale/fresh `latest` with a fresher/staler
568
- sibling number on the same line, asserting file:line for every case that
569
- must be caught and silence for every case that must not. Replaying the
570
- scanner against the actual pre-`306b6b0c` tree (`VERSION=10.0.0` plus the
571
- old `test-start-update-hint.sh`) reproduces the incident directly: exit 1,
572
- reporting both `9.99.0` lines by file:line. Run:
573
- `bash tests/test-no-stale-future-version.sh`.
574
-
575
- ## 17. `--self-test` reached the network only in CI, not locally (E-92)
576
-
577
- - **Incident:** `scripts/dep-inventory.py --self-test` passed on the
578
- engineer's machine but went red on main in CI. The fixture for
579
- `actions/setup-node@v4` stubbed only the `gh_release` cache bucket; ref
580
- `v4` also matches the bare-major-tag pattern in `collect_actions`, which
581
- resolves it through `resolve_floating_tag()` and the `floating_tag` cache
582
- bucket. With that bucket unstubbed, the resolver fell through to a real
583
- `gh api` call. Locally that call succeeded (a working, authenticated `gh`
584
- was on PATH), so the fixture's gap was invisible; in CI (no `gh`, no auth,
585
- no network) it failed, `resolve_entry["ok"]` was `False`, and the row's
586
- bump fell back to `"unknown"` instead of the expected `"MAJOR"`.
587
- - **Root cause with evidence:** commit `81cdba4d` ("stub the floating_tag
588
- cache in --self-test for actions/setup-node@v4") states it directly: the
589
- self-test "passed locally only because gh happened to be authenticated
590
- there." No leg of `tests/test-dep-inventory.sh` ran the self-test with
591
- `gh`/network absent, so the environment gap that separates "green
592
- locally" from "green in CI" was never exercised before main did it for
593
- real.
594
- - **The guard:** `tests/test-dep-inventory.sh` T6 runs
595
- `python3 scripts/dep-inventory.py --self-test` under
596
- `env -i HOME=<empty temp dir> PATH=/usr/bin:/bin`, which drops
597
- `GH_TOKEN`/`GITHUB_TOKEN` and every other inherited variable and wipes
598
- `gh`'s own `~/.config/gh` auth store by emptying `HOME`; it also excludes
599
- `gh` from PATH wherever it is installed outside `/usr/bin:/bin` (true on
600
- this machine: `/opt/homebrew/bin/gh`). Either way -- `gh` absent, or
601
- present but with no credentials to present -- any cache bucket a fixture
602
- forgets to stub hits a `gh api` call that fails the way it failed in CI,
603
- instead of a developer's real, authenticated one. This does not block
604
- outbound network access; the npm/PyPI/endoflife fetchers can still reach
605
- it, so an unstubbed bucket for one of those is not caught by T6.
606
- - **The test that proves it fires:** reproduced the incident directly by
607
- removing the `floating_tag` fixture E-92 added (the
608
- `cache4.data["floating_tag"] = {...}` block for
609
- `actions/setup-node@v4`, `scripts/dep-inventory.py` lines 1281-1286) and
610
- running `bash tests/test-dep-inventory.sh`: T1 (the script's own
611
- self-test, run with the developer's normal, authenticated `gh`) still
612
- showed `[PASS] dep-inventory.py --self-test passed` -- proving the
613
- existing leg is blind to this class -- while T6 showed `[FAIL]
614
- self-test failed unauthenticated/gh-less`; overall `Results: 5 passed, 1
615
- failed`, exit 1. The fixture was then restored from a backup
616
- (`git diff --quiet scripts/dep-inventory.py` exit 0, confirming an exact
617
- restore) and a clean rerun showed `Results: 6 passed, 0 failed`, exit 0.
618
- Run: `bash tests/test-dep-inventory.sh`.
619
-
620
- ## 18. Hardcoded fixture ports raced concurrent runs of the same test (E-95)
621
-
622
- - **Incident:** commit `8f2179cd`, Tests run `36453069628`,
623
- `tests/test-app-runner-watchdog-health.sh` failed with "healthy fixture
624
- server never came up (cannot validate no-restart case)" and passed on an
625
- unmodified rerun of the same SHA.
626
- - **Root cause with evidence:** the fixture hardcoded three TCP ports
627
- (`CLOSED_PORT=59731`, `HEALTHY_PORT=59732`, `API_PORT=59733`) and polled
628
- readiness for only ~3s (15 iterations of `sleep 0.2`). Any other process
629
- bound to the same port at that moment -- another worktree running this
630
- exact fixture concurrently (this swarm runs 12+ engineers in parallel
631
- worktrees on one machine), or a leftover process from a prior killed
632
- run -- makes the `python3 -m http.server "$HEALTHY_PORT"` bind fail (or,
633
- on kernels where `SO_REUSEADDR` lets a second bind coexist with an active
634
- listener, makes it non-deterministic which socket answers), so the fixed
635
- 3s readiness window times out with no HTTP server ever answering; a
636
- rerun once the colliding process is gone passes with no code change.
637
- Verified locally (Darwin): holding port 59732 with an `SO_REUSEADDR`
638
- listener during a run did NOT reproduce a bind failure here -- macOS/BSD
639
- `SO_REUSEADDR` allows a second bind to coexist with an active listener,
640
- which Linux (the actual CI runner) does not; the hardcoded-port class is
641
- confirmed by code inspection and by the incident report, not by a local
642
- bind-conflict repro, which is honestly reported as platform-dependent
643
- here.
644
- - **The guard:** `tests/test-app-runner-watchdog-health.sh` (this slice)
645
- adds a `pick_free_port()` helper (binds `("127.0.0.1", 0)`, reads back the
646
- OS-assigned port, closes the socket) and uses it for `CLOSED_PORT`,
647
- `HEALTHY_PORT` and `API_PORT` instead of any fixed literal, so the fixture
648
- can never collide with another instance of itself or a stale leftover
649
- regardless of kernel `SO_REUSEADDR` semantics. Readiness polling was also
650
- widened from ~3s to 10s (50 iterations), and both server fixtures now log
651
- to a file (`healthy_server.log`, `api404_server.log`) whose tail is
652
- included in the failure message alongside the actual port, so a genuine
653
- future timeout is diagnosable instead of a bare "never came up".
654
- - **The test that proves it fires:** `tests/test-app-runner-watchdog-health.sh`
655
- is itself the guard (fixed-port fixtures were the bug, not a separate
656
- probe of them). Run 20 times back to back while 4 parallel copies of
657
- `tests/test-v10-pulse.sh` provided CPU load: 20/20 passed
658
- (`bash tests/test-app-runner-watchdog-health.sh`, run in a loop; all 4
659
- concurrent `test-v10-pulse.sh` copies completed clean, exit 0, over the
660
- same window). `bash -n` and `shellcheck` both clean on the file.
661
-
662
- ## 19. A local pass depended on `gh` being authenticated on the dev Mac (E-94)
663
-
664
- - **Incident:** `tests/test-dep-inventory.sh` passed locally and failed on
665
- the CI runner. Root-caused to `scripts/dep-inventory.py`'s `self_test()`:
666
- one resolver path (the `actions/setup-node@v4` floating-tag lookup) had no
667
- fixture in its stub cache, so a cache miss fell through to a real `gh api`
668
- call. That call succeeds silently wherever `gh` happens to be authenticated
669
- (this Mac) and fails wherever it is not (the CI runner, which has neither
670
- `gh` nor `GH_TOKEN`/`GITHUB_TOKEN`) -- so local-ci reported green on a test
671
- that was never actually hermetic, and CI became the discovery channel
672
- instead of push time.
673
- - **Root cause with evidence:** replaying the pre-fix file directly (`git
674
- show 4f7f1487^1:scripts/dep-inventory.py` extracted to a temp path, then
675
- `python3 <that path> --self-test`) passes on this machine (23/23 `[PASS]`
676
- lines, real `gh api` reachable) and fails under a stripped environment
677
- (`env -i HOME=<fresh dir> PATH=/usr/bin:/bin:<bun+python3 only>`) with
678
- exactly one failure: `[FAIL]
679
- actions: Bump is measured against the release lookup` -- the same
680
- resolver path, the same missing fixture, now unable to reach `gh`. E-92
681
- (`4f7f1487`) fixed this specific instance by adding the missing
682
- `floating_tag` cache fixture; nothing stopped the next uncached fallthrough
683
- in the next test file from shipping the same way.
684
- - **The guard:** `scripts/local-ci.sh`'s `_lci_hermetic_scan` (fast-tier
685
- step `"hermetic changed-tests (no gh/network, E-94)"`, on `_FAST_KEEP`,
686
- called from the SERIAL spine AFTER section 7's `bun install` -- not right
687
- after `harvest_lanes`: a fresh worktree has no `loki-ts/node_modules`
688
- until section 7 installs it, so a changed `loki-ts/tests/*.test.ts` file's
689
- normal `bun test` would fail for a missing-toolchain reason and the scan
690
- would silently never actually check it. Like section 3's pytest gates, it
691
- runs a NORMAL pass against real `HOME`/repo state, which is exactly the
692
- class of check #588 pins off the concurrent background lanes). For every
693
- real test file this branch changed vs `origin/main` (merge-base diff,
694
- basename-anchored to `test[-_]*.sh`/`test[-_]*.py` under `tests/` or
695
- `*.test.ts` under `loki-ts/tests/` -- NOT a bare extension match, so
696
- `tests/run-all-tests.sh` and a `loki-ts/tests/**/fixtures/*.ts` data file
697
- are never swept in as if they were runnable suites), it runs the file once
698
- normally and once under `env -i` with a fresh `HOME`, and `PATH` set to a
699
- private directory (listed FIRST, not last) holding ONLY symlinks to a
700
- short, curated list of the real, ambient interpreter/runtime binaries a
701
- test legitimately needs (`bash`, `bun`, `python3`, `node`, `timeout`) --
702
- never `gh`, then `/usr/bin:/bin`. Measured, not assumed, on each point: a
703
- bare-last-position PATH let `/usr/bin`'s own Xcode-stub `python3` shadow
704
- the real one for any bare `python3` call (including a test's OWN internal
705
- one, like `test-dep-inventory.sh`'s `python3 "$SCRIPT" --self-test`);
706
- macOS's `/bin/bash` is 3.2 with no `mapfile`/`declare -A`, false-failing
707
- any test written against a modern bash; `node` and `timeout` are not on
708
- `/usr/bin` on this class of machine either, and real in-scope tests call
709
- both directly; and a fresh `HOME` alone breaks any package installed under
710
- `~/Library/Python/.../site-packages` (fastapi among them), so
711
- `PYTHONUSERBASE` is preserved from the real machine to keep those
712
- importable without reintroducing a credential -- all four false-positive
713
- classes have nothing to do with credentials and would otherwise have
714
- blocked a legitimate push. `env -i` alone already drops every inherited
715
- variable, so `GH_TOKEN`/`GITHUB_TOKEN` need no separate unset;
716
- `GIT_CONFIG_NOSYSTEM=1` additionally blocks the macOS system gitconfig's
717
- `credential.helper=osxkeychain` (a fixed path, unrelated to `HOME`, that a
718
- fresh `HOME` alone does not neutralize -- the identical leak class as
719
- `gh`, just through `/usr/bin/git`). A file that passes normally but fails
720
- stripped is reported by name; a file already failing (or timing out)
721
- normally is counted separately and never claimed as hermetic-clean.
722
- Skipped (not silently passed) when the branch changed no in-scope file, or
723
- when no `timeout`
724
- binary is on PATH (fail-closed, matching the gitleaks/shellcheck posture).
725
- Fails closed the other way too: this is a keep-list member, so it cannot
726
- be silently deferred out of the fast (pre-push) tier.
727
- - **The test that proves it fires:** `tests/test-local-ci-hermetic.sh`
728
- (registered in `tests/run-all-tests.sh` and `tests/shard-durations.tsv`,
729
- 18 cases: 12 static + 6 live). Static assertions confirm the scope, the
730
- stripped-env shape (bindir-first PATH, `PYTHONUSERBASE`,
731
- `GIT_CONFIG_NOSYSTEM`, the curated `bash`/`bun`/`python3`/`node`/`timeout`
732
- symlink list), the serial (not background-lane) call site, the keep-list
733
- membership, and both skip paths. The live half awk-extracts the REAL
734
- `_lci_hermetic_scan` function body out of `scripts/local-ci.sh` (never a
735
- mirrored reimplementation) and runs it against disposable fixture repos: a
736
- new test that calls `gh` directly passes normally and fails stripped, so
737
- the scan fails and names it (gated on `gh` actually being reachable
738
- normally and actually unreachable under a bare `/usr/bin:/bin`, not
739
- assumed); a hermetic-clean new test, a `mapfile`/`declare -A` (bash4+-only)
740
- test, and a test shelling out to `node`/`timeout` directly all pass both
741
- runs; the actual pre-E-92 `scripts/dep-inventory.py` (`git show
742
- 4f7f1487^1:scripts/dep-inventory.py`), replayed through a copy of the real
743
- test wrapper, is caught (gated on the old self-test actually passing
744
- normally first); and the REAL, current (post-E-92) `dep-inventory.py` and
745
- `test-dep-inventory.sh` from this repo, replayed verbatim, pass both runs
746
- -- the must-not-regress case, since a scan that cannot survive the exact
747
- file it was written to guard would block every future push that touches
748
- it. Verified red-then-green by hand: pointed at `origin/main`'s
749
- `scripts/local-ci.sh` (no `_lci_hermetic_scan` at all) the suite goes 0
750
- passed / 12 failed; restored, it is 18/18 (16/16 before the node/timeout
751
- scenario was added). A mutation that widens the stripped `PATH` back to
752
- the ambient one (`PATH="$PATH:$spath"`) flips exactly the gh-calling
753
- scenario to FAIL while every other case stays green, confirming the live
754
- half is not vacuous. Dogfooded on this branch's own commit adding this
755
- file: the real (non-test-harness) `_lci_hermetic_scan`, sourced and called
756
- directly against this worktree, found the one real in-scope change and
757
- returned `hermetic-clean` in 5.6s, well inside the 60s budget. Run:
758
- `bash tests/test-local-ci-hermetic.sh`.
759
-
760
- ## 20. Eval results lost when a worktree was force-removed (E-96/EV-14, E-101)
761
-
762
- - **Incident:** `eval/loki10/results/*/results.jsonl` is gitignored
763
- (`eval/loki10/.gitignore`), so it exists only inside the worktree that ran
764
- the eval. The 17:36Z pruning incident (guard 5's family, PROGRESS.md)
765
- force-removed 7 live builder worktrees including EV-14's; that eval's
766
- per-run results were destroyed with it (METRICS.md 18:00Z note) and could
767
- not be re-audited.
768
- - **Root cause with evidence:** nothing ever copied `results.jsonl` outside
769
- the worktree that produced it, and no existing prune tool checked for
770
- un-copied results before removing a worktree.
771
- - **The guard:** `eval/loki10/harness.py` `cmd_run` now writes a redacted
772
- copy of every row (`redact_row`: drops the `arm_stdout`/`arm_stderr`/etc.
773
- log paths and any secret-shaped string) to both
774
- `${LOKI_EVAL_ARCHIVE:-$HOME/loki-ci-logs/eval}/<run-name>/results.jsonl`
775
- (outside the repo) and the committed `eval/loki10/archive/<run-name>.results.jsonl`,
776
- per row inside the same lock that writes the worktree-local copy, so a
777
- killed run keeps what it already archived. `scripts/v10-worktree-reap.sh --strict`
778
- refuses to remove a worktree that has a `results.jsonl` whose `run_id`s
779
- are not all present in an archived copy, and separately refuses to remove
780
- one with a live process inside it (`lsof -d cwd -Fn`) or a branch commit
781
- less than 30 minutes old (`git log -1 --format=%ct`) -- any check that
782
- cannot run (lsof missing/broken, unparseable JSON) REFUSES rather than
783
- guessing. No file-age signal (`stat`, `find -newermt`, etc.) is used
784
- anywhere in the strict gate.
785
- - **The test that proves it fires:** `tests/test-worktree-reap.sh` (strict
786
- section) keeps a
787
- worktree with a live process inside it, keeps one whose results are not
788
- yet archived, removes one whose results ARE archived (positive control),
789
- and refuses everything when `lsof` is stubbed to fail. Deleting any one of
790
- the three new checks from `strict_gate` in `scripts/v10-worktree-reap.sh`
791
- (live-process, 30-minute, or archived-results) turns the corresponding
792
- case red. `tests/test-eval-archive.sh` plants a token
793
- (`sk-ant-plantedTOKEN9999`, a GitHub-token-shaped string, and a
794
- `KEY=VALUE`-shaped one) in a synthetic row and asserts none of it survives
795
- `redact_row`, that the log paths are gone, that real fields (`task`,
796
- `run_id`, `harness_sha`, `cost_usd`) survive intact, and that the redacted
797
- rows still `summarize_rows()` correctly (an empty/over-stripped archive
798
- would otherwise pass the secret check too). Replacing `redact_row` with a
799
- no-op (`return dict(row)`) reproduces the incident directly: the planted
800
- secret and the log paths both survive.
801
- Run: `bash tests/test-worktree-reap.sh && bash tests/test-eval-archive.sh`.
802
-
803
- ## 21. A failed `cd` let an agent detach the shared main checkout (E-161)
804
-
805
- - **Incident:** an agent ran `cd <temp clone>` which failed, then fell back
806
- to the shared main checkout and ran `git checkout --detach <tag>` there 7
807
- times, leaving the founder's checkout detached each time.
808
- - **Guard:** RULE8 in `scripts/v10-guard.sh` (PreToolUse Bash hook). In the
809
- primary worktree (git-dir equals git-common-dir) it blocks `git checkout
810
- --detach`, `git checkout <tag|sha|remote-ref>` and `git switch --detach`
811
- for every caller. When the hook input carries `agent_id` (set only for
812
- subagents; the leader has none) it also blocks all `git checkout`/`switch`,
813
- `reset --hard`, `stash`, `clean` and `worktree remove` of the main
814
- checkout. A dangerous git command after a `cd` not followed by `&&`/`||`
815
- is blocked when the hook cwd is the primary worktree. The leader keeps
816
- `git checkout main`, merge, revert and commit.
817
- - **Limit:** `cd X || true; git ...` counts as handled. If a host stops
818
- sending `agent_id`, only the always-on detach forms remain.
819
- - **The test that proves it fires:** `tests/test-v10-guard.sh` (Rule 8
820
- section): detached checkout in the main checkout blocked, same in a linked
821
- worktree allowed, leader `git checkout main` allowed, subagent forms and
822
- the unchecked-`cd` case blocked.
823
- Run: `bash tests/test-v10-guard.sh`.
824
-
825
- ## cp-redesign image allowlist (third-party screenshot incident)
826
-
827
- - **Incident:** a third-party product screenshot showing a real person's name
828
- and customer data was committed under `docs/v10/cp-redesign/` by a UI slice
829
- and was caught only in review.
830
- - **Root cause:** nothing restricted which images may live in the doc folder;
831
- evidence is the review finding on the CP-REDESIGN merge.
832
- - **The guard:** `docs/v10/cp-redesign/ALLOWED-IMAGES.txt` is an explicit
833
- allowlist with one-line provenance per image. `tests/test-cp-redesign-images.sh`
834
- fails on any tracked image there that is not listed, and on any tracked image
835
- anywhere named vorflux, lovable, bolt, v0-, or competitor, or under a path
836
- containing `ui-refs` or `research/`. `.githooks/pre-commit` runs the same
837
- check on staged files (`--staged`).
838
- - **The test that proves it fires:** the self-test inside
839
- `tests/test-cp-redesign-images.sh` builds a fixture with an unlisted image,
840
- a ui-refs image and a banned name and asserts each fails.
841
- Run: `bash tests/test-cp-redesign-images.sh`.
842
-
843
- ## D91 finding guards (review BLOCK classes become tests)
844
-
845
- - **Incident:** the same six review BLOCK classes recurred across trains and were caught only by human or agent reviewers.
846
- - **Guards** (all in `loki-ts/tests/util/`, each under 2s, picked up by `bun test tests/util`; allowlists in `loki-ts/tests/util/guard-allowlists/`, one `path | reason` per line (full-env-spawn and model-output-regex use `path@N` per-site counts, so a new site in a listed file fails), stale entries and missing reasons fail):
847
- 1. `raw_gh_spawn_guard.test.ts` (FC-25, FC-40): raw `gh` spawn in loki-ts/src, and raw `git push` / `gh pr create` in autonomy bash, outside the allowlist. Raw `git` in TS stays with `fc25_raw_spawn_guard.test.ts`. Known follow-up: autonomy/loki auto-PR paths push raw.
848
- 2. `start_flag_parity_guard.test.ts` (FC-37, FC-38): the `loki start` flag sets of bash `cmd_start` and Bun `start.ts` against the declared table `start-flags.txt`. Three real divergences are recorded (`--budget-limit`, `--no-pr`, `--session-model` are Bun-only).
849
- 3. `registration_list_guard.test.ts` (FC-32, FC-39): every module under src/commands and src/engine10/stages is imported somewhere, and every literal package.json files[] entry exists. Test-runner and shard lists stay with `scripts/structural-checks.sh` and `tests/test-shard-coverage.sh`. Known orphan: commands/status.ts.
850
- 4. `swarm_internal_read_guard.test.ts`: product code (loki-ts/src, autonomy/) naming docs/v10/BOARD.md, scripts/v10-* or .loki/v10-leader. Zero exceptions today.
851
- 5. `full_env_spawn_guard.test.ts` (FC-40): a spawn call site with no env or a bare process.env copy, unless the file is baselined. The baseline groups WORKER, UTILITY and SWEEP; SWEEP entries are the conversion backlog. Limit: an env variable built earlier than 14 lines above the call is not seen.
852
- 6. `model_output_regex_guard.test.ts` (L0, FC-36 family): regex exec/match on model-output variables under engine10. Baselined: xreview VERDICT and session sentinel parsing (follow-up to schema-checked JSON).
853
- - **The test that proves it fires:** each file plants a violation in the working tree during review (mutation run recorded in the slice report); `full_env_spawn_guard` and `model_output_regex_guard` also carry in-file detector self-tests.
854
- Run: `cd loki-ts && bun test tests/util`.
855
-
856
- ## 22. Repo-wide guards skipped by the diff-selected gate (GATE-GUARDS, FC-48)
857
-
858
- - **Incident:** the CI-FAST planner selects tests by diff, so a slice touching
859
- only `loki-ts/src/runner/attempts.ts` never ran `fc25_raw_spawn_guard`, and a
860
- raw git spawn passed 3 reviews.
861
- - **Root cause:** every guard was selected only when something it names
862
- changed; repo-wide guards name nothing in a given diff.
863
- - **Guard:** `scripts/global-guards.tsv` is the single declared list of
864
- repo-wide guards (loki-ts/tests/util, spawn_env_guard, never_below_raw,
865
- l0_guard, structural checks, no-hardcoded-paths, moat P9).
866
- `scripts/select-tests.sh` emits it as rule R8 on every non-empty diff,
867
- docs-only included, and via `--guards-only`; `scripts/ci/fast-gate.sh plan`
868
- and `scripts/impacted-gate.sh` consume it. Add a guard by editing that file
869
- only.
870
- - **The test that proves it fires:** `tests/test-select-tests.sh` (R8 cases):
871
- an attempts.ts-only diff and a docs-only diff each select every declared
872
- guard. Run: `bash tests/test-select-tests.sh`.
873
-
874
- ## 23. Deleting the token env vars left gh and git credential stores reachable (GH-KEYRING, FC-90)
875
-
876
- - **Incident:** with the token vars removed, `gh auth token` still returned the user's login via the keyring and git still ran osxkeychain and gh credential helpers; tokenFreeEnv and plainTestEnv did nothing about either.
877
- - **Root cause:** each env builder deleted a subset of credential paths itself; only the worker env reset the rest.
878
- - **Guard:** `loki-ts/tests/util/fc90_credential_env.test.ts` plants a fake gh hosts.yml and a fake global git credential helper in a fake HOME, proves the raw env sees them, and proves an env from plainTestEnv does not; it also pins the one-dir-per-process GH_CONFIG_DIR lifecycle. It lives in loki-ts/tests/util, which `scripts/global-guards.tsv` already selects on every diff.
879
- - **The test that proves it fires:** removing the GH_CONFIG_DIR line, the credential.helper reset or the sentinel from `loki-ts/src/util/credential_env.ts` turns it red. Run: `cd loki-ts && bun test tests/util/fc90_credential_env.test.ts`.
880
-
881
- ### 2026-10-23: release manager shipped unnamed stacked parents
882
- - **Incident:** rm-1 shipped unnamed stacked parents (cw20-r2 e4c445b73 13b46699d; cw15/ux350/roadmap100 parents 9bf08f534, dc2e16314, 64e9f1a09, cf177adf5). cw20-r2 shipped as v11.58.0 while a hold on that slice stood.
883
- - **Guard:** the approval line must name every shipped SHA and its reviewer; holds match by slice name, not SHA; a queue order is never an approval. The check is scripted: `scripts/release-approval-gate.sh` denies any range commit not named by SHA, rebase-equal by patch-id or exempt, and any commit whose slice is held; `scripts/train-cycle.sh` phase B calls it. Proof: `bash tests/test-release-approval-gate.sh` (stacked parents, clean and changed rebase, hold, exemptions, env bypass, and mutants that must go red).