mandrel 1.60.0 → 1.62.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.
Files changed (49) hide show
  1. package/.agents/README.md +74 -32
  2. package/.agents/docs/SDLC.md +18 -12
  3. package/.agents/docs/configuration.md +61 -4
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +3 -4
  6. package/.agents/runtime-deps.json +2 -2
  7. package/.agents/scripts/README.md +1 -1
  8. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  9. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
  10. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  11. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  12. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  13. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  14. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  15. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  16. package/.agents/scripts/lib/errors/index.js +4 -4
  17. package/.agents/scripts/lib/label-taxonomy.js +2 -2
  18. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  19. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  20. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  21. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  22. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  23. package/.agents/workflows/agents-update.md +14 -29
  24. package/.agents/workflows/deliver.md +87 -26
  25. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  26. package/.agents/workflows/helpers/deliver-epic.md +12 -5
  27. package/.agents/workflows/helpers/deliver-stories.md +13 -7
  28. package/.agents/workflows/plan.md +48 -4
  29. package/README.md +18 -30
  30. package/bin/mandrel.js +235 -16
  31. package/docs/CHANGELOG.md +36 -0
  32. package/lib/cli/doctor.js +45 -3
  33. package/lib/cli/init.js +66 -7
  34. package/lib/cli/registry.js +42 -146
  35. package/lib/cli/sync.js +122 -23
  36. package/lib/cli/uninstall.js +42 -7
  37. package/lib/cli/update.js +257 -198
  38. package/lib/cli/version-helpers.js +59 -0
  39. package/package.json +6 -6
  40. package/.agents/workflows/onboard.md +0 -208
  41. package/lib/cli/__tests__/migrate.test.js +0 -268
  42. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  43. package/lib/cli/__tests__/sync.test.js +0 -372
  44. package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
  45. package/lib/cli/__tests__/update-major.test.js +0 -217
  46. package/lib/cli/__tests__/update-reexec.test.js +0 -513
  47. package/lib/cli/__tests__/update.test.js +0 -696
  48. package/lib/cli/__tests__/version-check.test.js +0 -398
  49. package/lib/migrations/__tests__/index.test.js +0 -216
@@ -0,0 +1,796 @@
1
+ # Quality Gates
2
+
3
+ This is the consumer-facing reference for the quality gates the framework
4
+ runs against your repo: the lint baseline ratchet, the maintainability
5
+ ratchet, the CRAP per-method gate, the **absolute quality floors**
6
+ (90/85/90 coverage, MI ≥ 70, CRAP ≤ 20), the anti-thrashing protocol,
7
+ and the concurrent close-safety retry that protects the Epic branch when
8
+ multiple Stories close in quick succession.
9
+
10
+ The floor + ratchet duo is intentional: the ratchet protects against
11
+ regressions on touched files; the floor enforces an absolute threshold
12
+ on every in-scope file regardless of diff scope. See
13
+ [§ Absolute quality floors (Epic #1184)](#absolute-quality-floors-epic-1184)
14
+ below for the policy and [`docs/decisions.md`](../../docs/decisions.md) (ADR
15
+ 20260512-coupling-stance) for the framework-wide stance that motivates
16
+ the lift the floor gate represents.
17
+
18
+ The configuration knobs that drive these gates live in
19
+ [`.agents/docs/configuration.md`](../docs/configuration.md) under
20
+ `delivery.quality.*` and the framework-internal `DEFAULT_STORY_MERGE_RETRY` constant. This
21
+ file is the runbook side — what the gate does, when it fires, and how to
22
+ bootstrap or refresh it.
23
+
24
+ The **baseline envelope, per-kind shapes, component model, writer/reader
25
+ contract, and floor-override path** are documented in the
26
+ [Baseline reference](#baseline-reference) section at the end of this
27
+ document. Each per-gate section below cross-links to that section; consult
28
+ it once and reuse the context as you read through any individual gate.
29
+
30
+ > **Story-level gates.** Quality gates run against the Story branch
31
+ > after the single Story-implementation phase completes. Friction
32
+ > comments flip the Story to `agent::blocked` and post on the Story
33
+ > ticket.
34
+
35
+ ---
36
+
37
+ ## Concurrent close safety
38
+
39
+ `/deliver`'s wave loop may close multiple Stories into the same
40
+ `epic/<epicId>` branch in quick succession. The push step inside `story-close.js` retries
41
+ on a non-fast-forward rejection — fetch, replay the story merge on top of
42
+ the new remote tip, push again — bounded by
43
+ `DEFAULT_STORY_MERGE_RETRY.maxAttempts` (3) and
44
+ `DEFAULT_STORY_MERGE_RETRY.backoffMs` (`[250, 500, 1000]`) from
45
+ `.agents/scripts/lib/config/runners.js`.
46
+ A real
47
+ content conflict (both stories touched the same lines) aborts the loop
48
+ with a clear error and leaves the local tree clean for manual resolution.
49
+
50
+ ---
51
+
52
+ ## Test runner concurrency
53
+
54
+ `npm test` (via [`.agents/scripts/run-tests.js`](../scripts/run-tests.js))
55
+ derives `--test-concurrency` from `os.availableParallelism()` at startup,
56
+ clamped into the `[TEST_CONCURRENCY_MIN, TEST_CONCURRENCY_MAX]` range of
57
+ `[1, 16]` (`resolveTestConcurrency`). The clamp keeps the value sane at
58
+ both extremes: on the GitHub Actions 2-vCPU runner the derived value
59
+ matches the host instead of leaving wall-clock on the table, and on
60
+ very-wide dev hosts the cap of 16 bounds the filesystem-race surface
61
+ from shared FS fixtures (`memfs` mounts, `temp/` snapshot dirs, the
62
+ `coverage/` artifact directory shared with the CRAP gate).
63
+
64
+ The coverage run is the exception: `npm run test:coverage`
65
+ ([`.agents/scripts/run-coverage.js`](../scripts/run-coverage.js))
66
+ still pins `--test-concurrency=8` so coverage timings stay comparable
67
+ across hosts. That fixed 8 sits in the same neighborhood as the cap=8
68
+ orchestration helpers (`SUBTICKET_HYDRATION_CONCURRENCY`, and
69
+ historically the since-deleted wave-gate helper, removed in PR #3936)
70
+ that settled on 8 as the project house-style ceiling. Any change to the
71
+ clamp bounds or the coverage pin must be paired with a benchmark run on
72
+ both a Windows dev host and a GitHub Actions runner to confirm it
73
+ doesn't reintroduce concurrency flakes.
74
+
75
+ ---
76
+
77
+ ## Coverage baseline gate
78
+
79
+ > Baseline envelope, axes, and component model: see the
80
+ > [Baseline reference](#baseline-reference) section below.
81
+
82
+ `npm run test:coverage` drives
83
+ [`.agents/scripts/run-coverage.js`](../scripts/run-coverage.js),
84
+ which runs the unit-test suite with `NODE_V8_COVERAGE` set, post-processes
85
+ the V8 dumps with `c8 report`, then delegates to
86
+ [`.agents/scripts/check-baselines.js`](../scripts/check-baselines.js)
87
+ for the gate decision. There is no global `lines/branches/functions`
88
+ threshold — the gate compares **per-file** coverage in
89
+ `coverage/coverage-final.json` against the floors recorded in
90
+ [`baselines/coverage.json`](../../baselines/coverage.json) and fails on:
91
+
92
+ - a regression on any axis (lines, branches, or functions) for any file
93
+ whose coverage dropped more than `0.01` percentage points below its
94
+ recorded floor;
95
+ - an in-scope file with no baseline entry (a brand-new untested CLI
96
+ shell would otherwise sail through with 0 % coverage and no recorded
97
+ floor to drop below).
98
+
99
+ Scope (include/exclude) and reporters are declared in
100
+ [`.c8rc.cjs`](../../.c8rc.cjs); the gate reads the same file so `c8 report`
101
+ and the per-file checker agree on what's in scope. Bootstrap or
102
+ ratchet the baseline when an intentional scope change shifts coverage:
103
+
104
+ ```bash
105
+ npm run test:coverage # produces coverage/coverage-final.json (gate
106
+ # warns + passes when no baseline exists yet)
107
+ npm run coverage:update # writes baselines/coverage.json from the run
108
+ ```
109
+
110
+ `npm run coverage:check` runs the gate standalone against an existing
111
+ `coverage-final.json` artifact (useful from CI hooks or close-validation
112
+ runners that orchestrate coverage capture separately).
113
+
114
+ The same files-out-of-scope list as before, declared in `.c8rc.cjs`:
115
+
116
+ - `.agents/scripts/agents-bootstrap-github.js` — one-shot bootstrap CLI
117
+ whose meaningful logic (label taxonomy + project field defs) lives
118
+ in `lib/label-taxonomy.js` and is unit-tested there. The CLI shell
119
+ itself is integration-only against a live GitHub repo.
120
+ - `.agents/scripts/hydrate-context.js` — thin wrapper around the
121
+ unit-tested hydration engine; end-to-end coverage requires a real
122
+ provider tree and Story prompt context, which lives in integration
123
+ tests.
124
+ - `epic-plan-decompose.js`, `epic-plan-spec.js`,
125
+ `epic-plan-healthcheck.js` — `/epic-plan` slash-command CLI shells
126
+ with no unit-test seam; the meaningful orchestration logic lives in
127
+ `lib/orchestration/plan-runner/*` and is unit-tested there.
128
+ - A larger Story #1702 carve-out of top-level CLI gates, orchestration
129
+ CLIs, git-manipulation CLIs, and `lib/*` glue (e.g. `lint-baseline.js`,
130
+ `story-close.js`, `dispatcher.js`, `run-tests.js`,
131
+ `lib/config-schema.js`) — see the `.c8rc.cjs` header comment for the
132
+ per-category rationale and the authoritative entry list.
133
+
134
+ Each excluded file also carries `/* node:coverage ignore file */` at
135
+ the top of its source as a second line of defence; the full
136
+ justification for each exclusion lives in the header comment of
137
+ [`.c8rc.cjs`](../../.c8rc.cjs) and MUST be updated when the list changes.
138
+
139
+ The current shape of this pipeline (NODE_V8_COVERAGE +
140
+ `c8 report` instead of wrapping the run in `c8 <cmd>`) was chosen
141
+ after a one-off A/B benchmark showed it was ~19 % faster end-to-end
142
+ on a Windows dev host while producing the same `coverage-final.json`
143
+ artifact.
144
+
145
+ ---
146
+
147
+ ## Absolute quality floors (Epic #1184)
148
+
149
+ The per-file ratchet only protects against **regressions** — if a file
150
+ has been sitting at 60 % coverage or MI = 58 since the v5 baseline, the
151
+ ratchet is perfectly happy to keep it there forever. Epic #1184 layers
152
+ an absolute-threshold gate on top of the ratchet that fails the build
153
+ when any in-scope file is below floor, regardless of whether the diff
154
+ touched it:
155
+
156
+ | Metric | Floor | Scope |
157
+ | --- | --- | --- |
158
+ | Coverage — lines | ≥ 90 % | per file |
159
+ | Coverage — branches | ≥ 85 % | per file |
160
+ | Coverage — functions | ≥ 90 % | per file |
161
+ | Maintainability Index | ≥ 70 | per file |
162
+ | CRAP | ≤ 20 | per method |
163
+
164
+ The floors are declared in [`.agentrc.json`](../../.agentrc.json) under
165
+ `delivery.quality.gates.<gate>.floors.*` (defaults baked into the helper
166
+ match the table above) and resolved at runtime by the shared
167
+ helper [`lib/orchestration/check-baselines/phases/floors.js`](../scripts/lib/orchestration/check-baselines/phases/floors.js).
168
+ All three gates run through `check-baselines.js` (coverage,
169
+ maintainability, crap), which invokes the floors phase **after** the
170
+ ratchet decision so a file that's below floor but matched the (stale)
171
+ baseline still trips the gate.
172
+
173
+ ### When the floor gate fires
174
+
175
+ - **Pre-push** (`.husky/pre-push`): diff-scoped, fast path only —
176
+ `quality-preview.js --changed-since origin/main` (MI + CRAP preview),
177
+ then `coverage-capture.js` and `npm run crap:check` (unified
178
+ dispatcher, diff-scoped via `delivery.quality.gateScoping`). Full-repo
179
+ lint, docs generation checks, and the complete test suite are **not**
180
+ run on push; use `npm run verify` locally before a PR. CI enforces the
181
+ authoritative full gate set on every PR.
182
+ - **CI** (`.github/workflows/ci.yml`): the `validate` job runs
183
+ **Lint and Format** (`npm run lint`), a **Maintainability Check**
184
+ (`npm run maintainability:check` → `check-baselines.js --gate
185
+ maintainability`, diff-scoped on PRs via
186
+ `delivery.quality.gateScoping`, full scope on push-to-main via
187
+ `BASELINE_SCOPE=full`), and **Run Tests with Coverage**
188
+ (`npm run test:coverage`), uploading the `test-results` and
189
+ `coverage-final` artifacts. A separate required **baselines** job runs
190
+ the unified `node .agents/scripts/check-baselines.js --format text`,
191
+ which enforces floors across every configured gate.
192
+
193
+ ### Opt-out
194
+
195
+ There is no floor opt-out flag on the check path. The `*:update`
196
+ baseline-snap scripts snapshot whatever the current numbers are without
197
+ floor enforcement **by construction** — they are writers, not gates —
198
+ so no disable switch exists or is needed (the floors phase at
199
+ [`lib/orchestration/check-baselines/phases/floors.js`](../scripts/lib/orchestration/check-baselines/phases/floors.js)
200
+ has no off switch).
201
+
202
+ ### No silent excludes (`.c8rc.cjs` policy)
203
+
204
+ The floor gate is only as strict as its scope, so the `exclude` list in
205
+ [`.c8rc.cjs`](../../.c8rc.cjs) carries three hard requirements that are
206
+ enforced by review (and partially by the audit suite):
207
+
208
+ 1. **One-line rationale per entry.** Every file in `exclude[]` MUST have
209
+ a bulleted justification in the `.c8rc.cjs` header comment naming
210
+ *why* it is excluded — typically "thin CLI shell, meaningful logic
211
+ lives in `lib/<X>` and is unit-tested there." A bare path with no
212
+ rationale is a review-block.
213
+ 2. **`/* node:coverage ignore file */` pragma at source.** Every
214
+ excluded file MUST carry the Node coverage pragma at the top of its
215
+ own source. This is the second line of defence: when `c8 report` and
216
+ the baseline checker disagree about scope (different cwd, different
217
+ glob expansion, partial install), the pragma keeps the file out of
218
+ the gate's numerator from the inside.
219
+ 3. **Excluded file's callees clear the floor.** A CLI shell is only a
220
+ legitimate exclude if the `lib/` module it wraps actually clears the
221
+ floor (coverage 90/85/90, MI ≥ 70, CRAP ≤ 20). Excluding a shell
222
+ that delegates to under-tested helpers re-introduces the very
223
+ risk the floor gate exists to surface; the audit suite spot-checks
224
+ the callee map at exclude-list churn time.
225
+
226
+ Story #1602 audit pass (2026-05-13) removed two stale exclude entries
227
+ (`epic-runner.js`, `ticket-decomposer.js`) whose source files had already
228
+ been deleted in earlier refactors. Every remaining entry was re-verified
229
+ against requirements 1 and 2 above.
230
+
231
+ ### Discontinuity with v5 baselines
232
+
233
+ The floor gate landed alongside a fresh baseline reset
234
+ (Tasks #1623, #1625, #1626, #1629). Any direct numeric comparison
235
+ against pre-floor-gate baseline snapshots is meaningless because the
236
+ pre-rebrand scope included files the current tree excludes (CLI shells,
237
+ generated artifacts) and because the absolute-floor gate is new —
238
+ historical files that were "green" on the ratchet may now show as below
239
+ floor and require either real test additions or an intentional
240
+ `.c8rc.cjs` exclude. The Story #1602 close-out lists every file that
241
+ flipped category in the reset.
242
+
243
+ ---
244
+
245
+ ## Anti-thrashing protocol
246
+
247
+ Agents MUST halt, summarize blockers, and re-plan if they hit consecutive
248
+ tool errors or perform consecutive analysis steps without modifying a
249
+ file. When any threshold under
250
+ the qualitative anti-thrashing cues in
251
+ [`.agents/instructions.md`](../instructions.md) are tripped, the
252
+ friction logger flips the Story to `agent::blocked` and
253
+ posts a structured `friction` comment on the Task so the operator has
254
+ the trace.
255
+
256
+ ---
257
+
258
+ ## Per-Story acceptance self-eval gate
259
+
260
+ After a Story's implementation commits land and **before** the Story
261
+ proceeds to close, delivery runs a bounded acceptance self-eval loop
262
+ (Step 1a of
263
+ [`helpers/epic-deliver-story`](../workflows/helpers/epic-deliver-story.md)
264
+ and `helpers/single-story-deliver`; the shared per-round mechanic lives
265
+ in
266
+ [`helpers/acceptance-self-eval`](../workflows/helpers/acceptance-self-eval.md),
267
+ with the gate CLI at
268
+ [`.agents/scripts/acceptance-eval.js`](../scripts/acceptance-eval.js)).
269
+ Each round, a fresh-context **critic pass** — independent of the
270
+ implementing agent — scores the working diff against every inline
271
+ `acceptance[]` item, using `verify[]` output as evidence, and yields one
272
+ of three decisions:
273
+
274
+ - **proceed** — all criteria met; the Story continues to close.
275
+ - **redraft** — unmet criteria are redrafted and re-implemented, then
276
+ re-evaluated in the next round.
277
+ - **block** — criteria remain unmet after the round cap; the Story
278
+ escalates to the blocked path (`agent::blocked`) for operator review.
279
+
280
+ The loop is always on (hard cutover, no enable flag) and bounded by
281
+ `delivery.acceptanceEval.maxRounds` (default 2, clamped to a minimum of
282
+ 1 so the cap cannot be disabled). This gate is complementary to the
283
+ close-validation chain above: that chain proves the code is *healthy*;
284
+ this loop proves it satisfies *this Story's* acceptance criteria. See
285
+ [`.agents/docs/configuration.md`](../docs/configuration.md) for
286
+ the `delivery.acceptanceEval` field reference.
287
+
288
+ ---
289
+
290
+ ## Lint baseline ratchet
291
+
292
+ > Baseline envelope, axes, and component model: see the
293
+ > [Baseline reference](#baseline-reference) section below.
294
+
295
+ The lint baseline engine enforces zero-deterioration during Epic
296
+ workflows. Integrations fail if new lint warnings are introduced, and the
297
+ baseline automatically tightens when the codebase improves.
298
+
299
+ The canonical baseline file lives at `baselines/lint.json` (override via
300
+ `delivery.quality.gates.lint.baselinePath`). Refresh with:
301
+
302
+ ```bash
303
+ node .agents/scripts/lint-baseline.js capture
304
+ ```
305
+
306
+ Refresh commits should use a `baseline-refresh:` subject + non-empty body so
307
+ the operator can spot baseline edits in review — same convention as the CRAP
308
+ and maintainability ratchets. The CI guardrail that mechanically enforced
309
+ this was removed in a pre-npm-era release; the operator is now the gate.
310
+
311
+ ---
312
+
313
+ ## Maintainability ratchet
314
+
315
+ > Baseline envelope, axes, and component model: see the
316
+ > [Baseline reference](#baseline-reference) section below.
317
+
318
+ A per-file maintainability scoring engine computes composite scores based
319
+ on cyclomatic complexity, file length, and dependency counts. The
320
+ `baselines/maintainability.json` baseline prevents score degradation
321
+ between Epics.
322
+
323
+ Refresh with `npm run maintainability:update`.
324
+
325
+ `delivery.quality.gates.maintainability.targetDirs` controls the scanned
326
+ directories — defaults to `["src"]`, accepts `{ "append": [...] }` /
327
+ `{ "prepend": [...] }` for additive overrides.
328
+
329
+ ---
330
+
331
+ ## CRAP gate — Consumer onboarding
332
+
333
+ > Baseline envelope, axes, and component model: see the
334
+ > [Baseline reference](#baseline-reference) section below.
335
+
336
+ A sibling per-method gate alongside the maintainability ratchet. CRAP
337
+ scores each JavaScript method via `c² · (1 − cov)³ + c`, combining
338
+ `typhonjs-escomplex` cyclomatic complexity with per-method coverage from
339
+ the `coverage/coverage-final.json` artifact your test runner already
340
+ produces. No new runtime dependencies. Runs at three sites:
341
+ `close-validation` (story close), `ci.yml` (push + PR), and
342
+ `.husky/pre-push`.
343
+
344
+ If you're a consumer repo that installed the framework via the
345
+ `mandrel` npm package (`mandrel sync`), this is what you need to know.
346
+
347
+ ### First-run behavior — bootstrap before the first push
348
+
349
+ As of Story #791 the gate is hard-enforcing across all three firing sites
350
+ (close-validation, pre-push, CI). With `crap.enabled: true` and no
351
+ `baselines/crap.json` on disk, the CRAP gate (`npm run crap:check`)
352
+ prints:
353
+
354
+ ```text
355
+ [CRAP] ❌ no baseline found — run the matching baseline-update command and commit with a 'baseline-refresh:' subject to bootstrap
356
+ ```
357
+
358
+ …and exits `1`. Bootstrap explicitly: run `npm run test:coverage` to
359
+ produce `coverage/coverage-final.json`, then `npm run crap:update` to
360
+ generate `baselines/crap.json`, and commit the file with a
361
+ `baseline-refresh:` tagged subject + non-empty body so the
362
+ refresh-guardrail accepts it on the next PR.
363
+
364
+ The transitional informational mode (exit 0 on first sync) was retired in
365
+ Story #791 because it allowed broken pipelines to ride green for an
366
+ indeterminate window. If your test runner doesn't produce per-method
367
+ coverage, see "Disabling the gate" below.
368
+
369
+ ### Disabling the gate (single-flag opt-out)
370
+
371
+ If your repo doesn't run coverage, set `enabled: false` in your
372
+ `.agentrc.json`:
373
+
374
+ ```jsonc
375
+ {
376
+ "delivery": {
377
+ "quality": {
378
+ "gates": {
379
+ "crap": { "enabled": false }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ ```
385
+
386
+ All three gate sites self-skip with `[CRAP] gate skipped (disabled)` — no
387
+ source edits required. The maintainability ratchet keeps running.
388
+
389
+ ### Extending `targetDirs` without re-listing framework defaults
390
+
391
+ The config resolver supports deep-merge for list-valued keys. To add your
392
+ own source dirs to the framework default (`["src"]`):
393
+
394
+ ```jsonc
395
+ {
396
+ "delivery": {
397
+ "quality": {
398
+ "gates": {
399
+ "crap": {
400
+ "targetDirs": { "append": ["packages/foo/src", "packages/bar/src"] }
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ ```
407
+
408
+ `{ "append": [...] }` and `{ "prepend": [...] }` are the deep-merge forms.
409
+ Passing a plain array replaces the default entirely — useful when you
410
+ want exactly your dirs and not the framework's. Unknown keys under
411
+ `delivery.quality.gates.crap` warn but don't fail resolution, so you can
412
+ extend forward-compatibly.
413
+
414
+ ### Interpreting the JSON report
415
+
416
+ `npm run crap:check` runs the unified dispatcher
417
+ (`check-baselines.js --gate crap`), which emits its structured report on
418
+ **stdout** — `--format json` is the default (pass `--format text` for the
419
+ human-readable summary). There is no file-writing flag; to capture a file
420
+ artifact, redirect:
421
+
422
+ ```bash
423
+ npm run crap:check > temp/crap-report.json
424
+ ```
425
+
426
+ CI does **not** upload a `crap-report` artifact — `ci.yml` uploads only
427
+ `test-results` (the test/coverage run log) and `coverage-final`
428
+ (`coverage/coverage-final.json`).
429
+
430
+ The JSON envelope is the unified check-baselines report (see
431
+ [`lib/orchestration/check-baselines/phases/report.js`](../scripts/lib/orchestration/check-baselines/phases/report.js)):
432
+ top-level totals (`totalBreaches`, `totalRegressions`,
433
+ `kernelDriftCount`, `schemaErrors`) plus a `gates[]` array where each
434
+ gate entry carries its `kind`, breach/regression counts,
435
+ kernel-version match info, and per-`components[]` floor `violations[]`
436
+ (`axis`, `value`, `floor`, `direction`).
437
+
438
+ ### Refreshing the baseline (when the drift is justified)
439
+
440
+ `npm run crap:update` regenerates `baselines/crap.json`. The refresh
441
+ should land in a commit whose:
442
+
443
+ 1. Subject starts with the configured `refreshTag` (default
444
+ `baseline-refresh:`).
445
+ 2. Body is non-empty and explains why the refresh is justified.
446
+
447
+ The CI guardrail that mechanically rejected unlabeled baseline edits was
448
+ removed in a pre-npm-era release alongside the bot-approver pipeline. The convention is
449
+ preserved so the operator can grep refresh commits in PR diff, but
450
+ self-policing is the operator's job during `/deliver`'s Phase 7
451
+ watch loop — an unjustified baseline ratchet is no longer caught by CI.
452
+
453
+ ---
454
+
455
+ ## HITL blocker escalation
456
+
457
+ `risk::high` is informational/planning metadata only. Runtime execution
458
+ does not pause automatically on `risk::high`.
459
+
460
+ The sole runtime HITL pause point is `agent::blocked`: when an agent
461
+ encounters an unresolvable blocker (including unsafe destructive actions
462
+ lacking explicit authorization), it flips the ticket/Epic to
463
+ `agent::blocked`, posts friction context, and waits for operator resume
464
+ (`agent::executing`).
465
+
466
+ `planning.riskHeuristics` remains the rubric for identifying
467
+ high-impact operations that should trigger blocker escalation.
468
+
469
+ ---
470
+
471
+ ## Post-floor-gate baseline reset (Story #1701)
472
+
473
+ **Date:** 2026-05-14
474
+ **Commit:** `0657272` (Story #1701, Epic #1653)
475
+ **Files refreshed:** `baselines/coverage.json`,
476
+ `baselines/maintainability.json`, `baselines/crap.json`.
477
+
478
+ A one-time baseline reset captured fresh coverage, maintainability, and
479
+ CRAP snapshots on the post-remediation `main` HEAD. The ratchet
480
+ continues from this new floor, not from any pre-floor-gate history.
481
+
482
+ **Policy:** these baselines are **non-comparable** to any prior
483
+ baseline. Do not diff per-file numbers against pre-reset entries to
484
+ reason about regressions — the post-remediation tree contains refactors,
485
+ extractions, and coverage gains that shift the absolute numbers in ways
486
+ the per-file ratchet cannot reconcile across the discontinuity. Use the
487
+ post-reset capture as the new floor; ratchet from there.
488
+
489
+ **Why:** Epic #1184 closed the floor-gate rollout. The absolute-floor
490
+ gate (coverage 90/85/90, MI ≥ 70, CRAP ≤ 20) is wired into
491
+ `.husky/pre-push` and the CI coverage workflow (see
492
+ [`§ Absolute quality floors`](#absolute-quality-floors-epic-1184)).
493
+ With the floor enforced on every in-scope file, every per-file baseline
494
+ entry must clear the absolute floor — this snapshot is the first
495
+ capture that holds that invariant repository-wide.
496
+
497
+ **Operator action:** none. The baseline is committed and
498
+ `maintainability:check` / `coverage:check` / `crap:check` pass against
499
+ it out of the box. The next regression you see will be diffed against
500
+ this baseline, not against pre-reset history.
501
+
502
+ ---
503
+
504
+ ## Baseline reference
505
+
506
+ This is the authoritative reference for the canonical baseline shape used
507
+ by every quality gate in the framework — `lint`, `coverage`, `crap`,
508
+ `maintainability`, `mutation`, `lighthouse`, and `bundle-size`. It covers
509
+ the envelope, the per-kind shapes, the component model, how paths are
510
+ canonicalised, the writer/reader contract, how consumers override floors,
511
+ and how kernel-version drift surfaces as friction. The runbook sections
512
+ above describe the runtime behaviour of each gate (when it fires, what it
513
+ asserts, how to refresh); this section is the data-shape contract those
514
+ gates read and write.
515
+
516
+ Cross-references:
517
+
518
+ - [`.agents/docs/configuration.md`](../docs/configuration.md) — the `.agentrc.json`
519
+ configuration surface that backs the gates.
520
+ - [`.agents/README.md`](../README.md) — consumer onboarding.
521
+
522
+ > The `mutation` gate ships **dormant** (built-but-unwired, intentionally
523
+ > opt-in). The cost/fit analysis behind deferring its activation lives in
524
+ > the header comment of
525
+ > [`.agents/scripts/update-mutation-baseline.js`](../scripts/update-mutation-baseline.js).
526
+
527
+ ### Envelope
528
+
529
+ Every baseline file under `baselines/<kind>.json` shares the same
530
+ top-level envelope:
531
+
532
+ ```json
533
+ {
534
+ "$schema": ".agents/schemas/baselines/<kind>.schema.json",
535
+ "kernelVersion": "1.1.0",
536
+ "generatedAt": "2026-05-15T19:30:00.000Z",
537
+ "rollup": {
538
+ "*": { "<axis>": <number>, "...": <number> }
539
+ },
540
+ "rows": [
541
+ { "path": "<repo-relative-path>", "<axis>": <number>, "...": <number> }
542
+ ]
543
+ }
544
+ ```
545
+
546
+ | Field | Purpose |
547
+ | --------------- | ----------------------------------------------------------------- |
548
+ | `$schema` | Per-kind JSON Schema path. Drives validation in the shared AJV. |
549
+ | `kernelVersion` | Version stamp of the writer that produced the file. See below. |
550
+ | `generatedAt` | ISO 8601 timestamp; advisory — not load-bearing for gate logic. |
551
+ | `rollup` | Per-component aggregate keyed by component name. `*` is required. |
552
+ | `rows` | Sorted, canonicalised per-file (or per-route/per-bundle) entries. |
553
+
554
+ The schemas live under [`.agents/schemas/baselines/`](../schemas/baselines/).
555
+ The shared AJV instance is built by `buildBaselineSchemaAjv()` in
556
+ [`.agents/scripts/lib/baseline-schema-registry.js`](../scripts/lib/baseline-schema-registry.js).
557
+
558
+ ### Per-kind shapes
559
+
560
+ Each kind contributes a `rows[]` schema and a `rollup` axis set. The
561
+ authoritative declarations live in the per-kind modules at
562
+ [`.agents/scripts/lib/baselines/kinds/`](../scripts/lib/baselines/kinds/):
563
+
564
+ | Kind | Key field | Row axes | Rollup axes |
565
+ | ----------------- | --------- | -------------------------------------------------------------- | ---------------------------------------- |
566
+ | `lint` | `path` | `errorCount`, `warningCount` | `errorCount`, `warningCount` |
567
+ | `coverage` | `path` | `lines`, `branches`, `functions`, `statements` | `lines`, `branches`, `functions` |
568
+ | `crap` | `path` | `method`, `startLine`, `crap` | `max`, `p95`, `methodsAboveCeiling` |
569
+ | `maintainability` | `path` | `maintainability` | `min`, `p50`, `p95` |
570
+ | `mutation` | `path` | `score`, `killed`, `survived`, `noCoverage`, `timeout`, `total`| `score`, `survived`, `noCoverage` |
571
+ | `lighthouse` | `route` | `route`, `performance`, `accessibility`, `bestPractices`, `seo`| per-category scores |
572
+ | `bundle-size` | `bundle` | `bundle`, `bytes`, `gzippedBytes` | `bytes`, `gzippedBytes` |
573
+
574
+ The `keyField` is the per-row identifier the writer canonicalises and the
575
+ component grouper matches against (see below). Lighthouse keys rows on
576
+ `route`; bundle-size keys on `bundle`; every other kind keys on `path`.
577
+
578
+ ### Component model
579
+
580
+ A component is a named bucket of rows that share a floor and a tolerance.
581
+ Components let an operator slice a baseline so per-component floors can
582
+ be evaluated independently (e.g. `api`, `worker`, `infra` each with its
583
+ own coverage floor).
584
+
585
+ Shape:
586
+
587
+ ```json
588
+ "components": {
589
+ "<name>": ["<glob>", "<glob>", "..."]
590
+ }
591
+ ```
592
+
593
+ Rules:
594
+
595
+ - The component literally named `*` is the **whole-repo bucket** and
596
+ captures every row regardless of declared globs. Every baseline emits
597
+ `rollup['*']` for backwards compatibility with pre-component gates.
598
+ - Glob matching uses
599
+ [`minimatch`](https://github.com/isaacs/minimatch) with `dot: true`.
600
+ - **Overlap is allowed by design** — a row matched by two components is
601
+ reported under both.
602
+ - When a gate omits `components`, the default is `{ "*": ["**"] }`. The
603
+ resolver lives in
604
+ [`.agents/scripts/lib/baselines/components.js`](../scripts/lib/baselines/components.js)
605
+ (`resolveComponents` + `groupRows`).
606
+
607
+ ### Path canonicalisation
608
+
609
+ Every path-like field in a baseline (`rows[].path`, `rows[].route`,
610
+ `rows[].bundle`) is canonicalised to a forward-slashed, repo-relative
611
+ form before it is written:
612
+
613
+ - Windows backslashes are normalised to forward slashes.
614
+ - Leading `./` is stripped.
615
+ - A `.worktrees/<workspace>/` prefix — which would leak into a hand-edit
616
+ made inside a story worktree — is stripped.
617
+ - Absolute paths are rejected (the writer throws rather than silently
618
+ rewrite identity).
619
+
620
+ The canonicaliser lives at
621
+ [`.agents/scripts/lib/baselines/path-canon.js`](../scripts/lib/baselines/path-canon.js).
622
+ The reader applies a defensive second pass (`canonicaliseRowPath`) when
623
+ loading so downstream consumers never have to special-case the worktree
624
+ prefix.
625
+
626
+ ### Writer/reader contract
627
+
628
+ The single funnel for **writing** a baseline is
629
+ [`.agents/scripts/lib/baselines/writer.js`](../scripts/lib/baselines/writer.js)
630
+ — `write({ kind, rows, components, kernelVersion?, generatedAt? })`:
631
+
632
+ 1. Resolve the per-kind module from the kernel registry.
633
+ 2. Project every row through `projectRow` (which canonicalises the key
634
+ field and asserts the result with `assertCanonical`).
635
+ 3. Sort the rows deterministically for stable on-disk diffs.
636
+ 4. Compute the per-component rollup, always including `*`.
637
+ 5. Stamp `$schema`, `kernelVersion`, and `generatedAt` via
638
+ `buildEnvelope`.
639
+ 6. Validate the envelope against the per-kind schema via the shared AJV.
640
+ 7. Return the envelope. `writeFile(absPath, envelope)` is the separate
641
+ serialise + atomic-rename seam.
642
+
643
+ The single funnel for **reading** a baseline is
644
+ [`.agents/scripts/lib/baselines/reader.js`](../scripts/lib/baselines/reader.js)
645
+ — `reader.load(kind, { cwd?, configPath? })`:
646
+
647
+ 1. Resolve the on-disk path from `delivery.quality.gates.<kind>.baselinePath`,
648
+ falling back to the canonical default (`baselines/<kind>.json`).
649
+ 2. Read the file as UTF-8 JSON.
650
+ 3. Validate against the per-kind schema.
651
+ 4. Apply the defensive path canonicalisation pass to `rows[]`.
652
+ 5. Return `{ rollup, rows, kernelVersion, generatedAt }`.
653
+
654
+ Every gate reads through this module — the unified
655
+ [`check-baselines.js`](../scripts/check-baselines.js) dispatcher
656
+ (whose per-kind gate logic lives in
657
+ [`.agents/scripts/lib/baselines/kinds/`](../scripts/lib/baselines/kinds/)
658
+ — `lint.js`, `coverage.js`, `crap.js`, `maintainability.js`,
659
+ `mutation.js`, etc.), the audit-suite delta emitter, and the
660
+ per-component drift signals. No gate opens
661
+ `JSON.parse(readFileSync(...))` of a baseline directly.
662
+
663
+ `loadFile(absolutePath, { kind? })` is the same contract for ad-hoc
664
+ fixture paths; the kind is inferred from `$schema` when not supplied.
665
+
666
+ ### Floor overrides
667
+
668
+ Consumers override floors per gate in `.agentrc.json` under
669
+ `delivery.quality.gates.<kind>`:
670
+
671
+ ```json
672
+ {
673
+ "delivery": {
674
+ "quality": {
675
+ "gates": {
676
+ "coverage": {
677
+ "floors": {
678
+ "*": { "lines": 90, "branches": 85, "functions": 90 },
679
+ "api": { "lines": 95, "branches": 90, "functions": 95 }
680
+ },
681
+ "components": {
682
+ "api": ["src/api/**", "src/server/**"]
683
+ }
684
+ }
685
+ }
686
+ }
687
+ }
688
+ }
689
+ ```
690
+
691
+ Behaviour:
692
+
693
+ - `floors['*']` is the whole-repo floor. Every gate falls back to `*`
694
+ when a component-scoped floor is not declared.
695
+ - A per-component floor overrides `*` for that component only. Other
696
+ components still inherit `*`.
697
+ - The `components` map is optional. When omitted, the default
698
+ `{ "*": ["**"] }` applies and only `*` rows are ever evaluated.
699
+ - The unified `check-baselines.js` reports breaches per component, with
700
+ `*` always present in the output. The per-component progress signals
701
+ (`crap-drift.js#detectComponentRegressions`,
702
+ `maintainability-drift.js#detectComponentRegressions`) name the
703
+ breached component in their bullet so a `*` rollup is not falsely
704
+ implicated when only a component-scoped floor was crossed.
705
+
706
+ #### Floor axes must match rollup axes
707
+
708
+ A configured floor axis is only enforced when the rollup actually exposes
709
+ that axis — `check-baselines.js#compareToFloor` skips axes whose value is
710
+ missing from the rollup. As of Story #2193, the unified dispatcher
711
+ **fails closed** when a configured floor axis is absent from the rollup:
712
+ the gate exits non-zero with an actionable error naming the missing axis
713
+ and listing the available rollup keys (so a typo like
714
+ `{ maintainability: 70 }` against the maintainability rollup — which
715
+ exposes `min` / `p50` / `p95` — surfaces immediately instead of silently
716
+ passing).
717
+
718
+ Match the floor axis names to the rollup axes documented in the [Per-kind
719
+ shapes](#per-kind-shapes) table above. For maintainability specifically:
720
+
721
+ ```json
722
+ {
723
+ "delivery": {
724
+ "quality": {
725
+ "gates": {
726
+ "maintainability": {
727
+ "floors": {
728
+ "*": { "min": 70 }
729
+ }
730
+ }
731
+ }
732
+ }
733
+ }
734
+ }
735
+ ```
736
+
737
+ The maintainability rollup exposes `min` (lowest per-file `mi`), `p50`
738
+ (median), and `p95` (95th percentile); a floor on `min` is the framework
739
+ default and enforces a hard lower bound on individual files. Floors keyed
740
+ on the legacy `maintainability` axis (which never appears in the rollup)
741
+ are rejected with an explanatory error.
742
+
743
+ For the full configuration surface (every gate-level key with defaults
744
+ and types) see [`.agents/docs/configuration.md`](../docs/configuration.md) and the
745
+ `delivery.quality.*` section.
746
+
747
+ #### Shipped surface vs follow-up
748
+
749
+ The unified [`check-baselines.js`](../scripts/check-baselines.js)
750
+ ships **floor + tolerance + schema + kernel-mismatch** logic and is the
751
+ **only** baseline gate. Epic #1943 (Story #1981) absorbed the per-kind
752
+ regression / scope / git-base-ref logic and deleted the per-kind
753
+ `check-<kind>.js` CLIs (no `check-coverage.js`, `check-crap.js`, or
754
+ `check-maintainability.js` exists in `.agents/scripts/`; see the
755
+ `baselines` job comment in `.github/workflows/ci.yml` and the
756
+ Story #2210 note in
757
+ `.agents/scripts/lib/close-validation/gates.js`). Consumers wire only
758
+ the unified `baselines` status check into branch protection (see
759
+ `.agentrc.json` → `github.branchProtection.requiredChecks`).
760
+
761
+ ### Kernel-version friction
762
+
763
+ Every per-kind module exports a `kernelVersion()` function that returns
764
+ the writer's version of the analysis it produces. The writer stamps the
765
+ version on the envelope; the reader returns it; the unified gate
766
+ compares it against the running kernel.
767
+
768
+ When `baseline.kernelVersion !== runningKernelVersion`, the gate emits a
769
+ `baseline-kernel-mismatch` friction signal (suppressed with
770
+ `--no-friction`) but does **not** change its exit code — kernel drift is
771
+ advisory. The friction record points the reviewer at the regenerate
772
+ workflow for the kind in question.
773
+
774
+ Refresh paths:
775
+
776
+ - `npm run test:coverage` then `npm run coverage:update` — rewrites
777
+ `baselines/coverage.json`.
778
+ - `node .agents/scripts/update-crap-baseline.js` — rewrites
779
+ `baselines/crap.json`.
780
+ - `node .agents/scripts/update-maintainability-baseline.js` — rewrites
781
+ `baselines/maintainability.json`.
782
+ - `node .agents/scripts/lint-baseline.js capture` — rewrites
783
+ `baselines/lint.json`.
784
+
785
+ After a kernel bump, regenerate every baseline whose `kernelVersion`
786
+ drifted, then commit the refreshed files. The writer guarantees
787
+ deterministic ordering and canonical paths, so the diff is the kernel
788
+ delta and nothing else.
789
+
790
+ ### Baseline source of truth
791
+
792
+ - [`.agents/docs/configuration.md`](../docs/configuration.md) — full `.agentrc.json`
793
+ surface.
794
+ - [`.agents/scripts/lib/baselines/`](../scripts/lib/baselines/) —
795
+ source of truth for the writer, reader, kernel registry, components
796
+ resolver, envelope schemas, and per-kind modules.