loki-mode 9.59.0 → 9.63.0

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.
@@ -0,0 +1,435 @@
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). This is a one-time operational
167
+ fix, not a standing, checked-in guard -- no code in this repo enforces
168
+ "never use mtime as a worktree-liveness signal" today. A real
169
+ worktree-cleanup tool that takes the live-agent list as input and
170
+ structurally cannot use mtime is cut as the follow-up, **PENDING (S-94)**
171
+ (commit `e80819d5`).
172
+ - **The test that proves it fires:** none checked in. **PENDING (S-94).**
173
+
174
+ ## 6. Over-budget trivial agents, no violation surfaced it (S-75, AGENT_OVER_BUDGET, `c16d875f`)
175
+
176
+ - **Incident:** nothing in `scripts/v10-pulse.sh` compared an agent's real
177
+ elapsed time on a BOARD.md row against its role/tier's time budget
178
+ (founder-specified: builder LOW 15m, MEDIUM 30m, HIGH 60m; reviewer LOW
179
+ and MEDIUM 30m, HIGH 60m), so a trivial agent running well past its budget
180
+ produced no violation and nothing surfaced it.
181
+ - **Root cause with evidence:** `scripts/v10-pulse.sh` had no per-role/tier
182
+ time-budget check at all before this slice; BOARD.md rows already carry a
183
+ Tier column and a status timestamp, but nothing computed elapsed time
184
+ since that timestamp against a budget (commit `c16d875f`'s own message;
185
+ D26 guard 3).
186
+ - **The guard:** commit `c16d875f` (cherry-picked from `5c9a16f2`) extends
187
+ `parse_board()` to also return each row's Tier (found the same
188
+ position-independent way as its existing Status-cell scan, since BOARD.md
189
+ has used at least four different column layouts), and adds a new
190
+ AGENT_OVER_BUDGET violation: for every row currently `building` (builder)
191
+ or `review` (reviewer), compute elapsed time since its status timestamp
192
+ and compare to its (role, tier) budget, naming over-budget rows
193
+ oldest-first. A row with no parseable Tier cell reports UNKNOWN
194
+ (`agent_budget`) rather than being silently treated as in-budget.
195
+ - **The test that proves it fires:** `tests/test-v10-pulse.sh` T21 (a LOW
196
+ builder at 20 minutes fires), T21b (10 minutes does not), T21c/T21d (a
197
+ HIGH review at 45 minutes does not fire, 65 minutes fires), T21e (a row
198
+ with no Tier cell reports UNKNOWN, never silently in-budget). Full suite
199
+ 45/45 (up from 40/40), confirmed red against the pre-fix script, and a
200
+ budget-value mutation (LOW 15 -> 25 minutes) confirmed red on T21.
201
+
202
+ ## 7. File-mode narrowing from mktemp+mv, 644 became 600 (S-16, recurred S-74)
203
+
204
+ - **Incident:** a temp-file-then-move write pattern silently narrowed a
205
+ tracked file's permissions from 644 to 600 on every write. First found in
206
+ `scripts/release.sh`'s `apply_sed()` (S-16): every real
207
+ `bump_all_version_files` release run silently narrowed all 14
208
+ version-bumped files (VERSION, package.json, Dockerfile, and so on).
209
+ Recurred in `scripts/v10-ops.sh`'s `board-row-status` (S-74 round 3): the
210
+ identical bug class, in a different file, on a different write path --
211
+ the S-16 fix's own verification did not generalize into a lesson that
212
+ caught the second occurrence before review did.
213
+ - **Root cause with evidence:** `mktemp` (and Python's `tempfile.mkstemp`)
214
+ always creates its temp file at mode 600 regardless of umask; a
215
+ same-directory `mv` (or `os.replace`) over the original keeps the TEMP
216
+ file's own mode, not the original's. Git does not track non-exec mode
217
+ bits, so the commit itself looks unchanged while the working tree's real
218
+ permissions quietly regress (S-16 fix commit `254c3c85`; S-74 fix commit
219
+ `e5dc3d28`, same underlying mechanism).
220
+ - **The guard:** S-16, originally commit `254c3c85`, is **MERGED** -- re-
221
+ verified against CURRENT main (this entry had gone stale: an earlier
222
+ re-verification found `254c3c85` not an ancestor and `apply_sed` absent
223
+ from main, which was true of main AT THAT TIME but is no longer true).
224
+ `git log main --grep 254c3c85` finds `c681a726` ("fix(release): preserve
225
+ file permissions in apply_sed (BACKLOG 22, S-16 rework)"), a rebase of the
226
+ same fix, and `scripts/release.sh` on main today has `apply_sed()` with
227
+ `cp -p "$file" "$tmp"` immediately after `mktemp`, before the
228
+ `sed -E ... > "$tmp"` redirect (verified by reading main's
229
+ `scripts/release.sh` directly, lines 144-153) -- `cp -p` copies the
230
+ original's mode onto the temp file, and the subsequent `>` redirect
231
+ truncates that inode's content in place rather than recreating it, so the
232
+ copied mode survives. Judged correctly this time by whether the change
233
+ (not the original SHA) is on main. S-74 (originally commit
234
+ `e5dc3d28`) is **MERGED** -- landed on main as cherry-pick `8fbbbb31`
235
+ (`git log main --grep 'cherry picked from commit e5dc3d28'` finds it, and
236
+ `git cherry main e5dc3d28` reports it patch-equivalent, "-"): status must
237
+ be judged by whether the change is on main INCLUDING patch-equivalent
238
+ cherry-picks, not by whether the original SHA is literally an ancestor --
239
+ `254c3c85` above genuinely has neither, `e5dc3d28` has the cherry-pick.
240
+ `shutil.copymode(board, tmp_path)` before EACH `os.replace` call (the main
241
+ write and the corruption-restore path), shared through one
242
+ `_atomic_write()` helper so both preserve the real board file's mode, is
243
+ live in `scripts/v10-ops.sh` on main today (`grep -c shutil.copymode` ->
244
+ 2).
245
+ - **The test that proves it fires:** S-16 -- `tests/test-release-sh.sh` is
246
+ checked in on main and wired into `tests/run-all-tests.sh`
247
+ ("release.sh version-bump preserves file mode (BACKLOG 22)", line 1180),
248
+ added in commit `00a9425c`. **MERGED (S-16).** The manual-only
249
+ verification this entry previously described (an isolated 644-in/600-out
250
+ repro against the `s16-rework-permission-fix` branch, not main) is
251
+ superseded by that checked-in test now running on every main commit --
252
+ this is exactly the regression-suite gap the S-74 recurrence exposed,
253
+ now closed. S-74 -- `tests/test-v10-ops.sh`'s
254
+ "a 644 board stays 644 after a flip" and "...after a corruption-triggered
255
+ restore" cases; **MERGED (S-74)** via `8fbbbb31`, same cherry-pick
256
+ verification as above.
257
+
258
+ ## 8. BOARD cell located by header index broke on short/long rows (S-74)
259
+
260
+ - **Incident:** `board-row-status`'s Status-cell lookup, having just been
261
+ fixed to locate the column via the table's header row instead of scanning
262
+ cell shapes (closing a different bug -- a Branch/SHA cell shaped like
263
+ `main@abc123` being mistaken for the Status cell), broke on real BOARD.md
264
+ rows whose column count differs from their header's. A dropped or added
265
+ column shifts every later cell, so a fixed header-derived index landed on
266
+ the wrong cell (Notes, not Status); a legitimate call such as
267
+ `board-row-status S-07 approved` exited 2 with a false "does not match"
268
+ refusal instead of flipping the row.
269
+ - **Root cause with evidence:** commit `9f8389b9`'s own measurement --
270
+ `awk -F'|' '/^\| [A-Z]+-[0-9]+ /{print NF}' docs/v10/BOARD.md | sort | uniq -c`
271
+ -> `2 10 / 30 8 / 47 9` -- roughly 40% of the real table's rows have a
272
+ cell count that does not match their header's (30 missing the Acceptance
273
+ checks column, 2 with an extra cell from an embedded `|` inside Notes), so
274
+ a fixed header-column index is invalid on those rows.
275
+ - **The guard:** commit `9f8389b9` (originally stacked on `e5dc3d28`) is
276
+ **MERGED** -- its diff is patch-equivalent to what shipped on main
277
+ (`git cherry main 9f8389b9` reports it "-"): `8fbbbb31`'s own commit
278
+ message (section C) describes this exact header-lookup fallback as part
279
+ of the same squashed cherry-pick that landed `e5dc3d28`'s mode-preservation
280
+ fix, so both original SHAs are represented by the one `8fbbbb31` commit on
281
+ main. It keeps the header-index lookup as the primary path only when the
282
+ row's cell count equals its header's; when the counts differ, it falls
283
+ back to locating the Status cell as the one cell matching a KNOWN
284
+ lifecycle token plus a full UTC timestamp (never a loose shape scan), and
285
+ refuses (exit 2) if zero or more than one cell matches either path.
286
+ - **The test that proves it fires:** `tests/test-v10-ops.sh`'s sweep over
287
+ all 79 real slice rows in `docs/v10/BOARD.md` (each on a fresh copy,
288
+ asserting exactly one line and exactly one cell changed per flip,
289
+ independent of the tool's own internal verification), plus isolated
290
+ short-row and embedded-pipe-in-notes reproductions (originally commit
291
+ `9f8389b9`, now part of `8fbbbb31` on main). Full suite 52/52 per
292
+ `8fbbbb31`'s own message; a mutation forcing the fallback path always
293
+ (`if len(cells) == len(header_cells): -> if True:`) sent tests red.
294
+ **MERGED (S-74)** via `8fbbbb31`.
295
+
296
+ ## 9. Push reported success through a pipe while the remote held the old SHA (D27)
297
+
298
+ - **Incident:** train 2's first push attempt was killed by its own 10-minute
299
+ command timeout. The second attempt reported success -- piped through a
300
+ filter, exit code read 0 -- while `git ls-remote origin refs/heads/main`
301
+ still showed the PRIOR head `b651b98d`, not the pushed SHA (DECISIONS.md
302
+ D27 context). A push can read green while the remote never moved.
303
+ - **Root cause with evidence:** the push command's real exit status was
304
+ taken from the pipeline's last stage (the filter it was piped through),
305
+ not from `git push` itself -- the same exit-code-through-a-pipe class this
306
+ repo's own memory already names (`feedback-pipefail-sigpipe-inverts-probe`,
307
+ `feedback-i-cancelled-my-own-release`): a pipe silently discards the
308
+ left-hand command's real exit code unless `pipefail` is set and checked.
309
+ D27's own fix direction is explicit: "never pipe git push."
310
+ - **The guard:** D27 (Loki-approved) mandates (1) never pipe `git push`, and
311
+ (2) a push counts as done only when `git ls-remote origin refs/heads/main`
312
+ equals `git rev-parse HEAD`, recorded as evidence and mechanically
313
+ enforced by a push helper in `scripts/v10-ops.sh` -- cut as slice **S-98
314
+ push-main** (D27 items 3 and 6), `ready@2026-09-27T15:53Z` on BOARD.md as
315
+ of this writing, not yet built. The train-2 and release pushes that
316
+ followed D27 were manually verified this way instead (D27's own evidence
317
+ line: `git rev-parse HEAD` = `git ls-remote origin refs/heads/main` =
318
+ `89e350bd...` at 15:48:18Z, and `= 5332bfc3...` at 15:50:09Z), which is the
319
+ discipline S-98 is meant to make structural instead of manual.
320
+ - **The test that proves it fires:** **MERGED (S-98)** -- shipped in train 5
321
+ as `cd1fb044` ("feat(S-98): TRAIN_LATE pulse violation + v10-ops push-main
322
+ (D27 items 3, 6)"); `git cherry main b44571ba` on this repo reports `-
323
+ b44571ba8a44527abdc183c189617d992b976bac` (equivalent patch already on
324
+ main).
325
+
326
+ ## 10. Local CI's fast tier exceeded the 10-minute command cap under swarm load (D27)
327
+
328
+ - **Incident:** the "Local CI Before Every Push" fast-tier gate (mandated
329
+ 2026-07-31) took more than 10 minutes to run under swarm load -- 90 of 173
330
+ checks hit the 600-second cap and reported `EXIT=124` -- and the
331
+ `.githooks/pre-push` hook's own serial `pytest` run alone exceeded 10
332
+ minutes by itself. Train 2 sat unpushed for more than 89 minutes waiting
333
+ on a gate that could not finish inside the timeout budget every command in
334
+ this repo is bound to.
335
+ - **Root cause with evidence:** the fast tier's own justification (this
336
+ file's CLAUDE.md predecessor, "Local CI Before Every Push," 2026-07-31)
337
+ was sized against a much smaller swarm; DECISIONS.md D27's own context
338
+ line states both measurements directly ("90 of 173 checks at the 600s
339
+ cap, EXIT=124" and "the pre-push hook's serial pytest alone exceeded 10
340
+ minutes"). A gate that cannot finish inside the hard command cap is not a
341
+ fast pre-push gate, it is a second full CI run duplicating GitHub Actions'
342
+ own Tier B.
343
+ - **The guard:** D27 (Loki-approved) retires the local-ci fast-tier mandate
344
+ outright: before a push, only syntax checks (`bash -n`, `py_compile`) plus
345
+ the slice's own tests, capped at 60 seconds, run locally; GitHub CI (Tier
346
+ B) becomes the sole release gate. The mandate's retraction is already in
347
+ force per DECISIONS.md D27 (choice 1). The corresponding hook change --
348
+ `.githooks/pre-push` reduced to identity check + `bash -n` + a red-main
349
+ warning only, no `pytest`, target under 5 seconds -- is cut as slice
350
+ **S-97** (D27 item 2), `ready@2026-09-27T15:53Z` on BOARD.md as of this
351
+ writing, not yet built.
352
+ - **The test that proves it fires:** **MERGED (S-97)** -- shipped in train 5
353
+ as `f85c643a` ("S-97: retire pytest from pre-push hook per D27"); `git
354
+ cherry main b72e9ab7` on this repo reports `-
355
+ b72e9ab7e730fc856766c0296a2f709a6f6af83b` (equivalent patch already on
356
+ main).
357
+
358
+ ## 11. A builder's glob `rm -f` ran in the shared scratchpad root (S-18)
359
+
360
+ - **Incident:** during the S-18 build (Rule-of-Two SSH/credential hardening),
361
+ a builder ran a glob-matched `rm -f` directly against the shared
362
+ scratchpad root instead of a run-owned subdirectory, risking deletion of
363
+ other concurrent agents' files in that same shared root. Disclosed inline
364
+ on BOARD.md's S-18 row ("Incidents during the build: a glob rm in the
365
+ shared scratchpad root ... (removed)").
366
+ This is the same incident class this repo's own memory already names
367
+ (`feedback-tmp-cleanup-glob-deleted-worktree`: `rm -rf /tmp/loki-*` matched
368
+ and deleted an unrelated worktree) recurring in a different shared
369
+ location.
370
+ - **Root cause with evidence:** a glob pattern scoped only to a shared root
371
+ directory (not to a single mkdir-created, ownership-marked, permission-
372
+ narrowed subdirectory) matches every sibling file or directory any other
373
+ concurrent agent has placed there at the same moment -- there is no
374
+ per-run isolation boundary for the glob to respect. This repo's own
375
+ `loki_run_tmp_create`/`loki_run_tmp_cleanup` convention (CLAUDE.md, "Test
376
+ and Resource Cleanup") exists precisely to close this class for `/tmp`;
377
+ the scratchpad root had no equivalent discipline applied at the time of
378
+ this incident.
379
+ - **The guard:** none yet. No slice has been cut to extend the
380
+ `loki_run_tmp_create`/`loki_run_tmp_cleanup` discipline (or an equivalent
381
+ per-agent-subdirectory convention) to the shared scratchpad root, and no
382
+ mechanical guard (for example, a `scripts/v10-guard.sh` PreToolUse rule
383
+ blocking an unscoped `rm -f`/`rm -rf` with a glob directly under a shared
384
+ root) exists today. **PENDING, no slice cut.**
385
+ - **The test that proves it fires:** none. **PENDING, no slice cut.**
386
+
387
+ ## 12. A test fixture wrote synthetic `insteadOf` lines to the real `~/.gitconfig` (S-18)
388
+
389
+ - **Incident:** during the same S-18 build, a test/fixture for the P9
390
+ Rule-of-Two credential work wrote 4 synthetic `insteadOf` lines directly
391
+ into the operator's real `~/.gitconfig` instead of an isolated config file
392
+ scoped to the test. Disclosed and remediated inline on BOARD.md's S-18 row
393
+ ("4 synthetic insteadOf lines written to the real ~/.gitconfig (removed;
394
+ grep of ~/.gitconfig for loki-test/insteadOf found none at 15:40)").
395
+ - **Root cause with evidence:** a git-credential/URL-rewrite fixture used
396
+ the ambient `$HOME` (and therefore the real `~/.gitconfig`) as its target
397
+ instead of pointing git at an isolated config via `GIT_CONFIG_GLOBAL` or a
398
+ scratch `HOME` override -- the exact hazard this session's own current
399
+ task instructions explicitly call out ("Never touch the real
400
+ `~/.gitconfig`; scratch files only in your own subdirectory"), confirming
401
+ this is a known, recurring risk class for any test that touches git
402
+ config, not a one-off.
403
+ - **The guard:** none yet. No slice has been cut to enforce, mechanically,
404
+ that a test writing git config (`insteadOf`, `url.*.insteadOf`,
405
+ credential helpers, or similar) must set `GIT_CONFIG_GLOBAL`/`HOME` to a
406
+ scratch path before writing rather than touching the ambient one. **NO
407
+ GUARD.**
408
+ - **The test that proves it fires:** none. **PENDING, no slice cut.**
409
+
410
+ ## 13. Slices marked released before `publish-npm` had actually succeeded (D27)
411
+
412
+ - **Incident:** 11 BOARD.md rows (S-67, S-68, S-71, S-73, S-74, S-75, S-78,
413
+ S-80, S-86, S-87, S-90) were each written as `released@2026-09-27T15:54Z`
414
+ immediately after train 2's push landed and was `ls-remote`-verified, then
415
+ each row's own Notes cell carries the identical self-correction:
416
+ "Correction 15:54: I had marked this released too early." "Pushed and
417
+ verified on the remote" was conflated with "released," before the
418
+ `publish-npm` CI job had actually completed.
419
+ - **Root cause with evidence:** DECISIONS.md D27 (choice 5) defines
420
+ "released" precisely: "a train counts as released when publish-npm
421
+ succeeds. The next train opens immediately, without waiting for npm's
422
+ availability lag, Docker or Homebrew." At the moment those 11 rows were
423
+ stamped `released`, only the push itself (verified via `ls-remote`) had
424
+ happened -- a necessary but not sufficient condition under D27's own
425
+ definition. Nothing mechanically checked `publish-npm`'s CI conclusion
426
+ before the BOARD.md write, so a human (or agent) stamping the row could
427
+ and did jump the gun on all 11 rows in the same train.
428
+ - **The guard:** D27's own definition is the standing rule; enforcing it
429
+ mechanically (for example, a `board-row-status` precondition, or a pulse
430
+ check, that refuses/flags a `released` write unless the corresponding
431
+ `publish-npm` GitHub Actions run for that train's SHA has `conclusion:
432
+ success`) has not been built. **PENDING, no slice cut** for the
433
+ mechanical check; the manual self-correction on all 11 rows is the only
434
+ remediation applied so far.
435
+ - **The test that proves it fires:** none. **PENDING, no slice cut.**
@@ -1208,3 +1208,12 @@ sample.
1208
1208
  | 15:50 | S-80 verified on a real push: Tests on 89e350bd stayed in_progress after 5332bfc3 landed | gh run list |
1209
1209
  | 15:53:23-26 | Train 3 (S-79, S-81, S-82, S-83, S-85) plus release commit v9.58.0 pushed, rc=0 | HEAD = ls-remote = c2eccb21c1273b27a385aa785fedee3269c4f605 |
1210
1210
  - Next: each train counts as released when its publish-npm job succeeds. Train 4 opens now: S-84 (Part C items 3 and 5) once approved, plus S-97 and S-98 (D27 hook and cadence guard). Target push by 16:13 (20-minute cadence).
1211
+ | 16:05 | Train 3 Tests run 36331204715 FAILED: test_build_supervisor lineage race on Python 3.10+3.11 (also 3.11+3.12 on 89e350bd; identical code passed on 5332bfc3), so the Release required-ci failed. Failed jobs rerun at 16:05:48Z; P0 S-102 opened | gh run view |
1212
+ | 16:06:59-07:02 | Train 4 (S-49, S-91, S-89, S-16, S-77) plus release commit v9.59.0 pushed, rc=0 | HEAD = ls-remote = 08d64f4e05b2a475ecde30a393d3f660963f95a9 |
1213
+ | 16:20 | Founder: "just release everything asap, no more tests for development work completed so far". Review workflows for S-97/S-98 and S-18 round 6 stopped | |
1214
+ | 16:22:47-51 | Train 5 (S-18 r1-6, S-84 r1-4, S-97, S-98) plus release commit v9.60.0 pushed, rc=0 | HEAD = ls-remote = 1cde81a773ae2fe3942edbc92d94abaa18b4ac3e |
1215
+ | 16:24:29-32 | Train 6 (S-54) plus release commit v9.61.0 pushed, rc=0 | HEAD = ls-remote = 64676dce0d78c9d1d1a34ef89e022846185c808e |
1216
+ | 16:25 | Train 2 Release: required-ci passed, but the release job failed. The bot token cannot push tag v9.57.0 because the release changes .github/workflows ("without workflows permission"). Release Manager pushed v9.57.0..v9.61.0 at their release commits (ls-remote ^{} verified) and fully reran train 2 Release (36331009098) at 16:25:43Z. S-105 opened | release job log |
1217
+ | 16:26 | Trains 3 and 4 Tests: the build_supervisor flake again (3.10; 3.13); failed jobs rerun at 16:26:04Z. P0 S-102 in progress | |
1218
+ | 16:39 | v9.57.0 published: Release run 36331009098 publish-npm success (after the Release Manager tag push and a full rerun). v9.59.0 published: Release run 36332036567 publish-npm success, update-homebrew success (S-83), native Docker builds running (S-85). npm dist-tags still read 9.56.0 at 16:39 (processing lag) | gh run view |
1219
+ | 16:39 | Never published: v9.58.0 (superseded by v9.59.0, which contains it); v9.60.0 (1cde81a7), v9.61.0 (64676dce), v9.62.0 (9218ea04) carry the S-18 P9 regression; their Release runs were cancelled or failed. Tags exist from the 16:25/16:29 workaround | |
@@ -250,7 +250,7 @@ print(suggestions_gen.to_json(suggestions, context=context))
250
250
  else
251
251
  script+="
252
252
  # Output text
253
- verbose = ${VERBOSE,,} # Convert to lowercase for Python bool
253
+ verbose = ${VERBOSE^} # Convert bash true/false to Python True/False
254
254
  print(suggestions_gen.format_suggestions_text(suggestions, verbose=verbose))
255
255
  "
256
256
  fi