loki-mode 9.50.2 → 9.50.3

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,382 @@
1
+ # Release Cadence (skill module)
2
+
3
+ **Purpose:** the shortest VERIFIED path from idea to published. Shortest and
4
+ verified are both load-bearing; this repo has shipped fast and shipped nothing
5
+ at the same time, four times in a row, and the cost was invisible for days.
6
+
7
+ **Authority:** the binding procedure is the CLAUDE.md "Release Workflow"
8
+ section (version-bump file list, dashboard build, pre-publish validation,
9
+ distribution verification). This module does not duplicate that list. It
10
+ encodes the CADENCE decisions around it: which gate blocks, what the pipeline
11
+ actually enforces for you, and the failure modes that make a release silently
12
+ publish nothing.
13
+
14
+ **Companion:** `skills/factory-operations.md` for role and coordination rules.
15
+ Section 5 there (release windows) and section 4 here are the same rule seen
16
+ from two sides.
17
+
18
+ **REGISTRATION REQUIRED (blocking).** `tests/test-skill-doc-accuracy.sh:126-133`
19
+ loops every `skills/*.md` and FAILS any file that `skills/00-index.md` cannot
20
+ route to. Measured on creation: the suite went from 57 passed / 0 failed to 57
21
+ passed / 2 failed, both failures being this file and `factory-operations.md`.
22
+ Until a routing row is added to `00-index.md`, the gate is red. A module the
23
+ index cannot name is also a module the agent never loads, however binding it
24
+ claims to be.
25
+
26
+ All measured numbers below carry their measurement date. Line numbers drift;
27
+ each citation also names a string or label to grep.
28
+
29
+ ---
30
+
31
+ ## 1. The gate decision, and why two files appear to disagree
32
+
33
+ **The FAST tier is the release gate. The FULL tier is not a blocker.**
34
+
35
+ That is the founder decision of 2026-07-31, recorded in CLAUDE.md under a
36
+ header that says so explicitly: "Local CI Before Every Push (2026-07-31 mandate
37
+ -- SUPERSEDES 2026-04-26)".
38
+
39
+ The measured basis, all from that date:
40
+
41
+ | Thing | Measured |
42
+ |---|---|
43
+ | GitHub CI Tests | 31 seconds |
44
+ | GitHub CI Release | 2 minutes |
45
+ | Local FULL tier | 26m50s |
46
+ | 323-suite shell run, serial | ~1440s |
47
+ | Same run, sharded 4 ways | 352s (4.1x, 0 failures, identical coverage) |
48
+
49
+ CI is fast because it shards the 323-suite shell run 4 ways; the local full
50
+ tier had no sharding at all. A 26-minute gate cannot sit in front of an hourly
51
+ release cadence.
52
+
53
+ ### 1.1 The apparent contradiction, resolved
54
+
55
+ `scripts/local-ci.sh` prints, on a FAST pass:
56
+
57
+ ```
58
+ All FAST-tier local-ci checks passed.
59
+ This is NOT push authorization.
60
+ ```
61
+
62
+ and on a FULL pass, `Safe to commit + push.` Read alone, that says the opposite
63
+ of the mandate above.
64
+
65
+ Both are correct, because they answer different questions, and the wording is
66
+ deliberately pinned. `tests/test-local-ci-tiers.sh:60-70` ASSERTS both strings:
67
+ that the fast verdict contains `NOT push authorization`, and that `Safe to
68
+ commit + push` appears ONLY in the full branch. This is an invariant under
69
+ test, not drift to be tidied away.
70
+
71
+ The reconciliation:
72
+
73
+ - **Fast tier is the gate.** A red fast tier BLOCKS the push. It is not
74
+ skippable and it costs about a minute.
75
+ - **Fast green is not a verification certificate.** The verdict line exists to
76
+ stop a fast pass being mistaken for a full one. It names what was deferred:
77
+ 282 shell suites (~10+ min), blanket pytest 1793 tests (128s), shellcheck
78
+ (118s), plus SBOM, npm audit, license-audit, bun-parity and MCP handshakes
79
+ (`scripts/local-ci.sh:1911-1918`).
80
+ - **Full tier is not a release precondition** because its BULK moved to CI,
81
+ which runs it faster. The 323-suite shell run and the pytest blanket are
82
+ covered by Tests, and `required-ci` enforces Tests, Bun Parity and Security
83
+ Audit at the exact release SHA (section 3). Blocking locally on those buys
84
+ latency without buying coverage.
85
+
86
+ Be precise about what that does NOT cover, because "CI has it" is false for
87
+ part of the deferred list. Measured by grepping `.github/workflows/`:
88
+ `sbom` appears in four workflows (`sbom.yml`, `provenance.yml`, `release.yml`,
89
+ `security-audit.yml`), but **shellcheck, license-audit and the MCP handshakes
90
+ match no workflow at all.** Those items are checked NOWHERE until somebody runs
91
+ a full tier deliberately. This is the same point CLAUDE.md makes from the other
92
+ direction: "Of seven real defects found on 2026-07-31, four were caught by the
93
+ local gate ALONE -- GitHub CI has no equivalent check."
94
+
95
+ So the trade is explicit: skip the full tier for cadence, and accept that a
96
+ shellcheck or license regression can reach main. Run the FULL tier when
97
+ diagnosing something specific, on a quiet cycle, or after changes concentrated
98
+ in shell scripts or dependencies. Never as a release precondition.
99
+
100
+ Caveat worth carrying: `scripts/local-ci.sh:1903-1905` justifies its verdict
101
+ text by citing "CLAUDE.md mandates the full gate before every push", which is
102
+ the SUPERSEDED 2026-04-26 rule. The string is right and pinned; its stated
103
+ reason is stale. Do not let the comment talk you into blocking a release on the
104
+ full tier.
105
+
106
+ ---
107
+
108
+ ## 2. What the fast tier must contain: the artifact rule
109
+
110
+ **A check that guards the SHIPPED ARTIFACT must run in the FAST tier.** It is
111
+ the only tier that runs before every push, and GitHub CI has no equivalent
112
+ check, because everything works fine from a git checkout.
113
+
114
+ This is the most expensive lesson in this repo's release history. Four releases
115
+ (v8.38.0 to v8.41.0) were spent discovering that the checks guarding the
116
+ shipped package were themselves unguarded:
117
+
118
+ - Four quality-gate detectors under `tests/` were never in `package.json`
119
+ `files[]`, so mutation-integrity fail-closed on EVERY iteration for EVERY npm
120
+ user. First-pass completion was impossible regardless of model output. Found
121
+ only because telemetry showed a gate failing in 0-1 seconds across 3 of 3
122
+ iterations; real analysis cannot run in zero seconds.
123
+ - The committed `loki-ts/dist/loki.js` hardcoded version 8.11.0 for 27
124
+ releases, because the dist-freshness check was DEFERRED by the very fast tier
125
+ that CLAUDE.md justifies with dist freshness.
126
+ - `npm pack tarball contents` was also deferred, and when promoted turned out
127
+ to pass on "6 or more" matches of 6 patterns that healthily produce 8. It
128
+ tolerated losing two required artifacts and could not say which.
129
+
130
+ Those checks are now on the fast-tier keep list by name, each with its measured
131
+ cost (`scripts/local-ci.sh:210-245`, grep `_FAST_KEEP`):
132
+
133
+ | Kept check | Measured cost |
134
+ |---|---|
135
+ | `dist/loki.js is a fresh build of src` | one `bun run build`, ~40ms |
136
+ | `npm pack tarball contents` | 1.6s |
137
+ | `Agent SDK is a resolvable root dependency` | 21ms |
138
+ | `parent checkout is not falsely marked bare` | sub-second |
139
+
140
+ against a roughly 60s tier. The keep list is an ALLOWLIST, not a denylist:
141
+ the fast tier states positively what it runs, so a newly added slow check
142
+ cannot quietly drift into it (`scripts/local-ci.sh:108-127`).
143
+
144
+ ### 2.1 The two rules that generalise past packaging
145
+
146
+ **Assert each required thing individually, never a count.** A threshold cannot
147
+ say WHICH artifact vanished, and it absorbs slack it was never meant to have.
148
+ The "6 or more of 6 patterns that produce 8" check is the worked example.
149
+
150
+ **Guard against vacuity.** An empty result is not evidence; it is an absent
151
+ measurement. A substring search over an empty listing reports nothing missing.
152
+ `npm pack` writes its listing to STDERR, so `2>&1 >file` captures build chatter
153
+ instead and makes every assertion pass. Assert the haystack is plausibly sized
154
+ first.
155
+
156
+ The pipeline already implements this rule in two places, and both are worth
157
+ copying rather than reinventing:
158
+
159
+ - `release.yml:120-130` VACUITY GUARD: an empty workflow-runs API result never
160
+ satisfies the wait loop, because "no failing runs" over zero runs is not
161
+ "everything passed."
162
+ - `release.yml:520-535` takes the LAST line of `npm pack --silent`, because
163
+ prepack's bun build writes to stdout too and a naive capture would be three
164
+ lines rather than a filename.
165
+
166
+ ---
167
+
168
+ ## 3. What the pipeline enforces for you
169
+
170
+ Know this before adding a local check: duplicating it wastes cadence, and
171
+ assuming it wastes releases.
172
+
173
+ `on: push, paths: [VERSION], branches: [main]` (`release.yml:3-8`). Then:
174
+
175
+ **`gate`** runs bash syntax validation, then Bun typecheck plus `bun test`,
176
+ then Python tests. The order is not cosmetic. v9.49.0 died on a one-line TS2322
177
+ in `doctor.ts`; because the typecheck ran LAST, the job spent ~3m46s on pip
178
+ installs and the full pytest suite before tsc failed in 7 seconds (measured on
179
+ run 34724377951: gate 4m03s total). Cheapest failing check first is now the
180
+ rule, and nothing about what is verified changed, only the order.
181
+
182
+ **`required-ci`** (`needs: gate`) polls the check-runs API for Tests, Bun Parity
183
+ and Security Audit AT THE EXACT RELEASE SHA, waiting up to 2400s and failing
184
+ closed. Its direction is load-bearing: "cancelled, timed_out, failure, skipped:
185
+ none is a pass." It deliberately polls rather than re-running the matrix inline,
186
+ because re-running would verify a DIFFERENT execution than the one the branch
187
+ was judged on, at double the cost for a weaker guarantee.
188
+
189
+ **`release`** (`needs: [gate, required-ci]`) creates the tag, the GitHub
190
+ release and its artifacts. Publish jobs hang off `needs: release`:
191
+ `publish-npm`, `publish-docker`, and the others are PARALLEL SIBLINGS. That has
192
+ a consequence the npm job documents: "the channel with the assertion cannot
193
+ stop the one without it, and npm has already shipped by the time Docker goes
194
+ red" (`release.yml:523-525`). A per-channel assertion protects only its own
195
+ channel.
196
+
197
+ `publish-npm` asserts the PACKED tarball embeds the current VERSION, not the
198
+ worktree, because `prepack` rebuilds dist during publish and SWALLOWS a failed
199
+ build (`|| echo 'WARN: ... using existing dist if present'`), yielding exit 0
200
+ and a tarball carrying a stale dist. The committed dist legitimately lags
201
+ VERSION: 9dfb18d2 shipped VERSION 9.50.1 with a committed dist embedding
202
+ 9.50.0, and npm published 9.50.1 correctly because prepack rebuilt it.
203
+ Asserting on the worktree would have failed that good release.
204
+
205
+ ---
206
+
207
+ ## 4. The two ways a release silently publishes nothing
208
+
209
+ Both have happened more than once. Both look like success from the terminal.
210
+
211
+ ### 4.1 The VERSION bump must be the push HEAD
212
+
213
+ `on: push, paths: [VERSION]` fires only if the VERSION-changing commit is the
214
+ HEAD of the push. GitHub creates a workflow run for the push head only. Bump
215
+ VERSION, keep working, push the batch, and the release silently no-ops.
216
+
217
+ Verified through the Actions API (`repos/.../actions/runs?head_sha=<sha>`):
218
+
219
+ | Commit | Tests runs |
220
+ |---|---|
221
+ | `c86115d5` (v9.14.0 bump) | 0 |
222
+ | `5d081dbc` (v9.15.0 bump) | 0 |
223
+ | `151b7401` (v9.16.0 bump) | 0 |
224
+ | push heads (`4158b6c1`, `1b7069ba`, `946cf472`) | 1 each |
225
+
226
+ loki-mode shipped 9.13.0 through 9.16.0 to git with full CHANGELOG entries
227
+ while npm latest stayed 9.12.6, and 15 commits landed after the 9.16.0 bump.
228
+ It survived four rounds because the releases were reported as shipped without
229
+ checking npm.
230
+
231
+ `scripts/release.sh` has always been correct: it commits (`:227`) and pushes
232
+ (`:235`) back to back, so the bump IS the head. The failure came from bumping
233
+ by hand and bypassing it. Use `scripts/release.sh`, or make the VERSION bump
234
+ the last commit before the push.
235
+
236
+ Recovery for an already-stranded bump needs BOTH dispatches, because
237
+ `security-audit.yml` shares the `paths: [VERSION]` trigger and `required-ci`
238
+ fails closed on "Security Audit: not reported yet":
239
+
240
+ ```bash
241
+ gh workflow run security-audit.yml --ref main
242
+ # wait for Tests + Bun Parity + Security Audit green at the SAME sha
243
+ gh workflow run release.yml --ref main
244
+ ```
245
+
246
+ Note: a fast-tier check named "VERSION is not stranded ahead of the last
247
+ release" was recorded as the going-forward detection for this. It is NOT
248
+ present in `scripts/local-ci.sh` today (grepped on two independent term sets,
249
+ zero matches). Until it exists, this failure mode is caught only by the
250
+ post-release npm verification in section 5. Treat that verification as
251
+ mandatory, not as belt and braces.
252
+
253
+ ### 4.2 Pushing during the window cancels the release's Tests
254
+
255
+ After a VERSION push, push NOTHING to that branch until npm shows the new
256
+ version. Any later push moves HEAD, GitHub's concurrency rules CANCEL the older
257
+ Tests run at the release SHA, and `required-ci` correctly refuses to treat
258
+ `cancelled` as a pass.
259
+
260
+ On 2026-08-08 v9.18.0 failed with `FAIL: a required workflow did not succeed at
261
+ 8892f477`. Nothing was broken:
262
+
263
+ ```
264
+ Tests: completed/cancelled
265
+ Bun Parity: completed/success
266
+ Security Audit: completed/success
267
+ ```
268
+
269
+ Re-broken on 2026-09-10 by a docs-only commit pushed during v9.27.3's release.
270
+ "It is only docs" is not an exemption: concurrency cancellation keys on the
271
+ BRANCH, not the diff.
272
+
273
+ Diagnose by reading the per-workflow status lines before assuming a test
274
+ failure; `cancelled` and `failure` look identical in the job summary and have
275
+ completely different fixes. Recovery without a version bump: rerun the
276
+ cancelled **Tests** at the release SHA first, then rerun **Release**. A later
277
+ commit that does not touch VERSION fires no Release of its own, so there is
278
+ nothing else to wait for.
279
+
280
+ Rewriting the same VERSION value is "nothing to commit" and the trigger never
281
+ fires. Re-releasing needs a REAL change to VERSION.
282
+
283
+ ---
284
+
285
+ ## 5. After the push: verify provenance, not the version string
286
+
287
+ A matching version on npm is NOT evidence that your tree shipped.
288
+
289
+ On 2026-08-08 npm `latest` read 9.17.0 within minutes of a release push and the
290
+ version matched the bump. It was a different build: the published tarball's
291
+ `gitHead` was `00b9f4e2`, a commit that is not even an ancestor of main,
292
+ published by a parallel agent's release run that won the race. The package was
293
+ functional and contained none of the fixes its own CHANGELOG described.
294
+
295
+ Several agents run against this checkout at once. Two release workflows for the
296
+ same version both start; the first to reach `npm publish` takes the version and
297
+ the second fails on version-conflict.
298
+
299
+ So verify:
300
+
301
+ ```bash
302
+ npm view loki-mode@<V> gitHead
303
+ git merge-base --is-ancestor <gitHead> HEAD
304
+ ```
305
+
306
+ and confirm a specific fix is present in the published tarball WITH A POSITIVE
307
+ CONTROL, so an empty grep cannot read as clean:
308
+
309
+ ```bash
310
+ npm pack loki-mode@<V> && tar xzf loki-mode-<V>.tgz
311
+ grep -c "<fix-string>" package/<file> # the fix: expect > 0
312
+ grep -c "<string-that-must-exist>" package/<file> # control: proves the grep works
313
+ ```
314
+
315
+ A zero without a control is an absent measurement, not a clean result.
316
+
317
+ ### 5.1 Confirm a release-critical fix THREE ways, never one
318
+
319
+ One green reading proves nothing. Require all three, and record all three:
320
+
321
+ 1. **LIVE** -- the real check against the real source reads the expected value.
322
+ 2. **NEGATIVE control** -- feed it a canned input representing the BAD state and
323
+ confirm it still reports bad. This proves the check can still fail, so a
324
+ green is informative.
325
+ 3. **POSITIVE control** -- perturb something that must not matter and confirm
326
+ the answer is unchanged. This proves the green came from the logic, not from
327
+ a coincidence of the input.
328
+
329
+ Worked example, the MCP registry staleness guard (2026-09-14). The guard read
330
+ 7.34.1 as live for hours after 9.50.1 was published, because it iterated
331
+ `servers[]` and took the first name match, and the registry keeps every
332
+ published version as a separate still-`active` row. The fix selects on
333
+ `isLatest`. It was confirmed:
334
+
335
+ - LIVE: reads 9.50.1 against the real registry.
336
+ - NEGATIVE: a canned response with `isLatest` on the OLD version still selects
337
+ the old version, proving real drift is still detected.
338
+ - POSITIVE: reversing the live array still selects 9.50.1, proving
339
+ order-independence rather than a lucky ordering.
340
+
341
+ Had only the LIVE reading been taken, a guard that happened to be right for one
342
+ ordering would have been declared fixed. See
343
+ `skills/factory-operations.md` section 7 for why this shape recurs, and for the
344
+ three other instances measured in the same session.
345
+
346
+ Applies to the artifact greps in this section too: a built artifact can contain
347
+ NUL bytes, which makes plain `grep` report no matches and exit 1 on a file that
348
+ demonstrably contains the string. Measured on `dashboard/static/index.html`:
349
+ `grep -c 'Loki'` finds nothing, `grep -ac 'Loki'` finds 27, Python finds 91.
350
+ Always pair a dist grep with a control string known to be present.
351
+
352
+ Then run the post-release distribution validation from CLAUDE.md across npm,
353
+ Docker, Brew and GitHub Release, on BOTH routes (Bun and `LOKI_LEGACY_BASH=1`).
354
+ Cleanup uses `loki_run_tmp_create` / `loki_run_tmp_cleanup` from CLAUDE.md's
355
+ "Test and Resource Cleanup"; never sweep shared ports, process names, `/tmp` or
356
+ `$TMPDIR`.
357
+
358
+ Timing note before calling a job hung: macOS Bun jobs legitimately take ~22
359
+ minutes. Compare against a prior successful run's `startedAt -> completedAt`
360
+ first. And npm's registry lags a green publish job by roughly 4 minutes, so a
361
+ green job is not yet proof the version is fetchable.
362
+
363
+ ---
364
+
365
+ ## 6. The cadence, end to end
366
+
367
+ 1. **Scope locked** by the PM. No guessing (`skills/sdlc-fleet.md:40`).
368
+ 2. **Build** inside owned file sets, at most 3 streams
369
+ (`skills/factory-operations.md` section 3).
370
+ 3. **Council**: unanimous 3-of-3 APPROVE. Any CONCERN or REJECT means fix and
371
+ re-run the WHOLE council.
372
+ 4. **Gate**: `bash scripts/local-ci.sh` (fast tier). Red blocks. One gate run
373
+ at a time, repo-wide.
374
+ 5. **Bump VERSION as the push HEAD**, via `scripts/release.sh`.
375
+ 6. **Window opens**: nobody pushes to main until npm shows the version.
376
+ 7. **Verify provenance**: `gitHead` ancestry plus a fix grep with a positive
377
+ control.
378
+ 8. **Window closes.** Queued commits land now.
379
+
380
+ Steps 4 through 7 are the verified part of "shortest verified path". Dropping
381
+ any one of them has already produced a release that reported success and
382
+ shipped nothing.