mandrel 2.25.0 → 2.27.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 (132) hide show
  1. package/.agents/agents/acceptance-critic.md +10 -6
  2. package/.agents/audit-checklists/baselines.md +21 -0
  3. package/.agents/docs/quality-gates.md +80 -18
  4. package/.agents/docs/workflows.md +3 -1
  5. package/.agents/instructions.md +1 -1
  6. package/.agents/schemas/audit-rules.json +15 -0
  7. package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
  8. package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
  9. package/.agents/schemas/baselines/crap.schema.json +8 -0
  10. package/.agents/schemas/model-attribution.schema.json +4 -0
  11. package/.agents/scripts/acceptance-eval.js +89 -6
  12. package/.agents/scripts/audit-baselines.js +136 -0
  13. package/.agents/scripts/check-arch-cycles.js +12 -93
  14. package/.agents/scripts/check-baseline-drift.js +16 -3
  15. package/.agents/scripts/check-baselines.js +19 -3
  16. package/.agents/scripts/check-cyclomatic.js +214 -0
  17. package/.agents/scripts/check-schema-references.js +392 -0
  18. package/.agents/scripts/check-test-temp-hygiene.js +38 -1
  19. package/.agents/scripts/check-workflow-timeouts.js +291 -0
  20. package/.agents/scripts/diagnose-friction.js +85 -19
  21. package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
  22. package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
  23. package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
  24. package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
  25. package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
  26. package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
  27. package/.agents/scripts/lib/audit-baselines/read.js +87 -0
  28. package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
  29. package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
  30. package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
  31. package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
  32. package/.agents/scripts/lib/audit-suite/index.js +0 -5
  33. package/.agents/scripts/lib/audit-suite/selector.js +9 -62
  34. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
  35. package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
  36. package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
  37. package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
  38. package/.agents/scripts/lib/baselines/git-base.js +26 -4
  39. package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
  40. package/.agents/scripts/lib/baselines/reader.js +52 -38
  41. package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
  42. package/.agents/scripts/lib/baselines/scope.js +39 -90
  43. package/.agents/scripts/lib/baselines/writer.js +16 -11
  44. package/.agents/scripts/lib/changed-files.js +8 -1
  45. package/.agents/scripts/lib/cli-args.js +115 -1
  46. package/.agents/scripts/lib/close-validation/runner.js +70 -25
  47. package/.agents/scripts/lib/crap-engine.js +32 -13
  48. package/.agents/scripts/lib/crap-method-identity.js +153 -0
  49. package/.agents/scripts/lib/crap-utils.js +13 -0
  50. package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
  51. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
  52. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
  53. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
  54. package/.agents/scripts/lib/git-utils.js +136 -80
  55. package/.agents/scripts/lib/import-graph.js +156 -0
  56. package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
  57. package/.agents/scripts/lib/observability/source-classifier.js +175 -2
  58. package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
  59. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
  60. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
  61. package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
  62. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
  63. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
  64. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
  65. package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
  66. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
  67. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
  68. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
  69. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
  70. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
  71. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
  72. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
  73. package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
  74. package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
  75. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
  76. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
  77. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
  78. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
  79. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
  80. package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
  81. package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
  82. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
  83. package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
  84. package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
  85. package/.agents/scripts/lib/story-adjacency.js +3 -3
  86. package/.agents/scripts/lib/test-runner-contract.js +134 -0
  87. package/.agents/scripts/lib/test-tiers.js +11 -2
  88. package/.agents/scripts/lib/util/concurrent-map.js +17 -0
  89. package/.agents/scripts/lib/util/parse-id-list.js +103 -0
  90. package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
  91. package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
  92. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
  93. package/.agents/scripts/lib/workers/crap-worker.js +2 -10
  94. package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
  95. package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
  96. package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
  97. package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
  98. package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
  99. package/.agents/scripts/lib/worktree-manager.js +14 -0
  100. package/.agents/scripts/plan-run-epilogue.js +17 -5
  101. package/.agents/scripts/providers/github/tickets.js +33 -10
  102. package/.agents/scripts/provision-git-hooks.js +85 -0
  103. package/.agents/scripts/quality-preview.js +112 -28
  104. package/.agents/scripts/resolve-stories.js +4 -1
  105. package/.agents/scripts/run-coverage.js +86 -35
  106. package/.agents/scripts/run-lint.js +20 -0
  107. package/.agents/scripts/run-tests.js +26 -36
  108. package/.agents/scripts/single-story-close.js +28 -2
  109. package/.agents/scripts/single-story-confirm-merge.js +22 -6
  110. package/.agents/scripts/stories-wave-tick.js +214 -38
  111. package/.agents/scripts/update-coverage-baseline.js +34 -4
  112. package/.agents/scripts/update-duplication-baseline.js +209 -83
  113. package/.agents/scripts/validate-docs-freshness.js +1 -0
  114. package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
  115. package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
  116. package/.agents/skills/skills.index.json +2 -2
  117. package/.agents/workflows/audit-baselines.md +289 -0
  118. package/.agents/workflows/audit-navigability.md +5 -4
  119. package/.agents/workflows/deliver.md +13 -4
  120. package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
  121. package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
  122. package/.agents/workflows/helpers/deliver-digest.md +41 -21
  123. package/.agents/workflows/helpers/deliver-reference.md +77 -1
  124. package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
  125. package/.agents/workflows/helpers/plan-reference.md +15 -5
  126. package/.agents/workflows/memory-consolidate.md +116 -0
  127. package/.agents/workflows/plan.md +3 -0
  128. package/README.md +13 -6
  129. package/docs/CHANGELOG.md +71 -0
  130. package/package.json +9 -4
  131. package/.agents/schemas/friction-event.schema.json +0 -56
  132. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
@@ -0,0 +1,289 @@
1
+ ---
2
+ description: Audit the committed baseline surface — dead instruments, stale baselines, cross-gate hotspot clusters, trend drift, and floor-tightening headroom — and emit findings whose remediation burns the measured debt down and tightens the ratchet behind it.
3
+ ---
4
+
5
+ # Baseline & Ratchet Audit
6
+
7
+ You are a Principal Engineer and Quality-Systems Owner auditing this
8
+ repository's **committed baseline surface**: the ratchet artifacts under
9
+ `baselines/` and the gate floors under `delivery.quality.gates` in
10
+ `.agentrc.json`. Those instruments only prevent **regression** — nothing owns
11
+ the loop that burns the measured debt **down** and tightens the floors behind
12
+ it, so a repo can hold a floor it cleared years ago and never notice. This lens
13
+ is that loop's read-only entry point. The shared lens machinery — read-only
14
+ constraint, scope interpretation, report envelope + finding-block skeleton,
15
+ severity scale, self-cross-check, and execution strategy — lives in
16
+ [`helpers/audit-lens-core.md`](helpers/audit-lens-core.md). Write the report to
17
+ `{{auditOutputDir}}/audit-baselines-results.md`. Dimension values:
18
+ `Dead Instrument | Staleness | Hotspot Cluster | Trend Drift | Tightening Headroom`.
19
+
20
+ > **Value-free titles (mandatory).** A finding title MUST NOT embed a measured
21
+ > number — write ``### `baselines/crap.json` — crap floor holds unused slack``,
22
+ > not ``… — crap floor 13 vs measured 8``. Every re-run re-measures, so a title
23
+ > carrying the reading changes each pass, its fingerprint changes with it, and
24
+ > `/audit-to-stories` files a duplicate instead of deduping against the open
25
+ > Story. Numbers belong in Current State, never in the title.
26
+
27
+ ## Scope
28
+
29
+ Interpret this lens's change-set fence per the core's Scope interpretation:
30
+
31
+ ```text
32
+ {{changedFiles}}
33
+ ```
34
+
35
+ When the fence resolves to a file list, keep only Hotspot Cluster findings
36
+ whose cluster key is in that list. The other four dimensions are properties of
37
+ the instrument set as a whole rather than of any changed file, so report them
38
+ only in codebase-wide mode.
39
+
40
+ ## Constraint (lens-specific carve-out)
41
+
42
+ This lens **refines** the core's read-only constraint; it never relaxes it.
43
+
44
+ - The only command it runs is the read-only engine in Step 0. It never runs a
45
+ test, coverage, mutation, duplication, or lint suite.
46
+ - It never writes under `baselines/` and never edits `.agentrc.json`. It never
47
+ invokes an `update-*-baseline` script. **Regeneration is a finding, never an
48
+ in-run step** — the remediation Story owns every write to the surface.
49
+ - Reading committed baseline rows is explicitly permitted and required: citing
50
+ an already-computed metric is analysis, not measurement.
51
+
52
+ ## Execution strategy
53
+
54
+ Run this lens as a single `subagent_type: auditor` dispatch returning the report
55
+ path + Executive Summary; sequential inline execution is the fallback (see the
56
+ core's Execution strategy).
57
+
58
+ ## Step 0: Run the engine (mandatory — measure before you judge)
59
+
60
+ ```bash
61
+ node .agents/scripts/audit-baselines.js --out temp/audit-baselines/envelope.json
62
+ ```
63
+
64
+ Optional flags: `--cwd` (repository root), `--top-n` (outlier rows per gate,
65
+ default 20), `--hotspot-limit` (clusters emitted, default 50), `--trend-depth`
66
+ (baseline commits sampled per kind, default 5). Exit 0 means evidence was
67
+ assembled **including every degraded input**; exit 1 means the envelope could
68
+ not be built or written — report that and stop, because no evidence base
69
+ exists to author against.
70
+
71
+ The envelope is validated against
72
+ `.agents/schemas/baselines/audit-baselines-envelope.schema.json` and is this
73
+ lens's sole evidence base. Cite its fields by name:
74
+
75
+ | Section | Fields you cite |
76
+ | --- | --- |
77
+ | root | `generatedAt`, `cwd`, `topN`, `configError`, `degradations` |
78
+ | `gateSurface[]` | `kind`, `surface`, `baselinePath`, `configured`, `baselineExists`, `stub`, `rowCount`, `measured` (`unit` plus `value`), `generatedAt`, `staleDays`, `staleCommits`, `surfaceStale`, `deadIgnoreGlobs`, `parseError` |
79
+ | `hotspots[]` | `path`, `gates` (each `kind`, `metric`, `value`, `rowCount`, `severityWeight`), `gateKinds`, `gateCount`, `severityWeight`, `churnWeight`, `centralityWeight`, `frictionWeight`, `rank` |
80
+ | `trend[]` | `kind`, `baselinePath`, `sampleCount`, `from`, `to` (each a `sha` plus `committedAt`), `deltas` |
81
+ | `headroom[]` | `kind`, `axis`, `floor`, `measured`, `direction`, `headroom` |
82
+
83
+ `surface` is `gate` (a closed `delivery.quality.gates` kind) or `ratchet` (an
84
+ out-of-band baseline the CI baselines job owns). `direction` is `gte` or `lte`.
85
+ `rowCount` counts rows **after** per-file aggregation; `measured` is the
86
+ quantity the instrument reports, in its own unit. Cite `measured` when the two
87
+ disagree — 589 dead-export symbols sit in 187 files.
88
+
89
+ Two envelope-level reads come **before** any finding:
90
+
91
+ - **`configError` non-null.** Floors, target directories, and ignore globs
92
+ were unavailable and the engine fell back to default baseline paths. Every
93
+ Tightening Headroom finding would be unfounded this run: file the config
94
+ failure itself as one `Dead Instrument` finding and skip that dimension.
95
+ - **`degradations`.** Each of `gitHistory`, `importGraph`, `frictionLedger`
96
+ reading `true` collapsed its rank multiplier to exactly 1.0, so the hotspot
97
+ ordering is weaker evidence. Name the degraded inputs in the Executive
98
+ Summary; never present a degraded rank as a churn-informed one.
99
+
100
+ ## Step 1: Evaluation Dimensions
101
+
102
+ 1. **Dead Instruments.** An instrument that cannot fail is worse than none: it
103
+ reads green forever and the surface it names looks governed. Four shapes,
104
+ read straight off `gateSurface[]`:
105
+ - `stub` is `true` — zero rows **and** an all-zero rollup, so the gate
106
+ passes vacuously. The engine requires both halves, so a ratchet with
107
+ genuinely nothing to report is never mistaken for a dead one.
108
+ - `configured` is `false` on a `gate` row — a baseline is committed but no
109
+ `delivery.quality.gates` block enforces it, so nothing reads it.
110
+ - `baselineExists` is `false`, or `parseError` is non-null — the instrument
111
+ cannot be read at all.
112
+ - `deadIgnoreGlobs` is non-empty — a configured ignore pattern matches zero
113
+ files. It protects nothing today and silently exempts the next file that
114
+ happens to match it.
115
+
116
+ Grade a stub or unenforced gate **Medium**, a `parseError` on an enforced
117
+ gate **High** (delivery reads that file every run), a dead glob **Low**.
118
+
119
+ 2. **Staleness.** Two clocks. `staleDays` is whole days since the baseline's
120
+ own `generatedAt`; `staleCommits` is commits touching the measured surface
121
+ since the baseline was last committed, with `surfaceStale` its boolean. A
122
+ `null` on either is never a fabricated zero — the stamp is unreadable, git
123
+ cannot answer, or the rows are not file paths — and is itself the finding.
124
+ **`surfaceStale` with `staleDays: 0` is still stale:** refreshed recently in
125
+ wall time, already behind the surface it scores. Grade an enforced gate
126
+ stale beyond roughly a month or `surfaceStale` **Medium**, a `null` stamp
127
+ **Medium**, an unenforced kind **Low** or **Info**.
128
+
129
+ **Regeneration is the remediation, never an in-run step.** The Agent Prompt
130
+ names the matching script and its one-shot acknowledgment:
131
+
132
+ | Kind | Regeneration script | Acknowledgment |
133
+ | --- | --- | --- |
134
+ | `coverage` | `npm run coverage:reanchor` | `COVERAGE_REFRESH=1` |
135
+ | `crap` | `npm run crap:reanchor` | `CRAP_REFRESH=1` |
136
+ | `duplication` | `npm run duplication:reanchor` | `DUPLICATION_REFRESH=1` |
137
+ | `maintainability` | `npm run maintainability:reanchor` | `MAINTAINABILITY_REFRESH=1` |
138
+
139
+ **Prescribe the `:reanchor` script, never the bare `:update` one.** Every
140
+ updater defaults to a **diff-scoped** refresh — only files changed in
141
+ `origin/main..HEAD` are re-scored, and everything else is preserved
142
+ verbatim. That is the right default for "I changed code, re-score what I
143
+ touched", and it is exactly wrong here: a baseline is stale because the
144
+ *world* moved (a scorer bump, a coverage-shape change, months of unrelated
145
+ drift), so a diff-scoped run leaves almost every stale row untouched and
146
+ the staleness finding re-fires on the next sweep. `:reanchor` is the same
147
+ script with `--full-scope`, which re-scores every file in every target
148
+ dir. Confirm the flag on any kind you are unsure of with that script's
149
+ `--help`.
150
+
151
+ Expect a re-anchor to touch far more rows than a code change would — that
152
+ breadth is the point, but say so in the finding so a reviewer can tell a
153
+ re-anchor from a mass regression.
154
+
155
+ The acknowledgment is the kind upper-snaked. It demotes that kind's
156
+ head-vs-base regressions to unchanged **for one run only** — floors stay
157
+ enforced, so a genuine breach is still caught. The durable equivalent is a
158
+ commit in the compared range whose subject carries the gate's `refreshTag`
159
+ (default `baseline-refresh:`) **and** whose diff touches that kind's
160
+ baseline file. Confirm both against
161
+ `node .agents/scripts/check-baselines.js --help` before writing the prompt,
162
+ and never invent an acknowledgment for a kind that ships no regeneration
163
+ script — there, the remediation is to add one, not to hand-edit rows.
164
+
165
+ 3. **Hotspot Clusters.** `hotspots[]` is already the cross-gate join, one entry
166
+ per cluster key, ranked highest first. **Emit one finding per cluster —
167
+ never one per metric row.** A file that is a CRAP outlier and a
168
+ maintainability outlier is one debt item with two symptoms; splitting it
169
+ files two Stories that fight over the same refactor. The cluster key is a
170
+ repository file path for every kind except `lighthouse` (a route) and
171
+ `bundle-size` (a bundle name) — say which it is when it is not a file.
172
+
173
+ Quote `rank` with the four factors behind it — `severityWeight`,
174
+ `churnWeight`, `centralityWeight`, `frictionWeight` — and the per-gate rows
175
+ under `gates`. Grade by breadth first: three or more entries in `gateKinds`
176
+ is **High**, two is **Medium**, one is **Low** unless its `severityWeight`
177
+ alone is extreme.
178
+
179
+ 4. **Trend Drift.** `trend[]` carries newest-versus-previous rollup deltas per
180
+ kind, bracketed by the commits in `from` and `to`. A delta moving **away**
181
+ from the floor is the finding; one moving toward it is headroom the next
182
+ dimension owns. Read the axis's `direction` in `headroom[]` before assigning
183
+ a sign — lower is not universally better. An entry needs `sampleCount` of at
184
+ least 2 to mean anything, and an empty `trend[]` means no readable history:
185
+ record that as **Info** rather than inferring a flat trend from silence.
186
+ Each `deltas` key **names its unit** — `symbols`, `bytes`, `filesTracked` —
187
+ so quote the axis with the number, never a bare delta.
188
+
189
+ 5. **Tightening Headroom.** `headroom[]` is what this lens exists for. Positive
190
+ headroom is slack the floor could be tightened into; negative headroom means
191
+ the floor is already breached — grade that **High** and route it as a
192
+ regression, not an opportunity. File a tightening finding only when the
193
+ slack is **durable**: the same kind's trend is flat or improving. A one-run
194
+ dip tightened into a floor turns the next honest change red for no defect.
195
+ Grade durable multi-point slack **Medium**, marginal slack **Low**.
196
+
197
+ ## Step 2: Hotspot budget and the dropped log
198
+
199
+ Cap the Detailed Findings at the **top 8 hotspot clusters by `rank`**. The
200
+ engine emits up to `--hotspot-limit` clusters, and the point of the lens is a
201
+ ranked actionable batch, not an exhaustive dump nobody schedules.
202
+
203
+ A silent truncation reads as full coverage, so the report MUST carry a
204
+ **Dropped Hotspots** section naming every cluster the cap excluded with its
205
+ cluster key, `rank`, and `gateKinds`. Write `_None dropped._` when the cap did
206
+ not bite; the section's absence is itself a defect. This budget log is separate
207
+ from — and additional to — the core's self-cross-check `kept / dropped` line,
208
+ which counts evidence-bar drops rather than budget drops. State the cap in the
209
+ Executive Summary and change it only on an explicit operator instruction.
210
+
211
+ ## Step 3: The floor-tightening contract (mandatory)
212
+
213
+ A remediation Story that only burns debt down leaves the floor where it was,
214
+ and the reclaimed slack is silently re-spent by the next change — the loop
215
+ runs and the ratchet never moves. So **every Hotspot Cluster and Tightening
216
+ Headroom finding's Agent Prompt MUST** end the remediation with the ratchet
217
+ tightened and gate-enforced:
218
+
219
+ 1. Lower the floor under `delivery.quality.gates` in `.agentrc.json` to the
220
+ newly measured level, **or** delete the burnt-down rows from that kind's
221
+ file under `baselines/`.
222
+ 2. Carry `node .agents/scripts/check-baselines.js --gate <kind>` in the
223
+ remediation Story's `verify[]`, so the tightened floor is enforced by the
224
+ gate that already exists at that Story's delivery time rather than by prose
225
+ nobody runs.
226
+
227
+ Use these two Agent Prompt templates verbatim, substituting the envelope's own
228
+ values for the angle-bracketed slots:
229
+
230
+ - **Hotspot Cluster template:**
231
+ `Burn down the measured debt in <hotspots.path>, an outlier across <gateKinds>. Refactor and add tests until its rows leave that kind's file under baselines/, then regenerate that baseline with the matching update-*-baseline script in a commit whose subject carries the baseline-refresh: tag. Finish by TIGHTENING the ratchet in the same Story — lower the kind's floor under delivery.quality.gates in .agentrc.json to the new measured level, or delete the burnt-down rows — and carry node .agents/scripts/check-baselines.js --gate <kind> in this Story's verify[] so the tightened floor is enforced at delivery.`
232
+ - **Tightening Headroom template:**
233
+ `The <kind> gate's <axis> floor sits at <floor> while the measured rollup is <measured> (headroom <headroom>, direction <direction>), and that kind's trend is flat or improving. Tighten it: set that axis under delivery.quality.gates in .agentrc.json to the measured level so no slack remains for the next change to re-spend, and carry node .agents/scripts/check-baselines.js --gate <kind> in this Story's verify[] so the new floor is enforced. Regenerate no baseline in this Story — the floor edit is the whole change.`
234
+
235
+ Staleness, Dead Instrument, and Trend Drift findings do **not** carry the
236
+ tightening clause: there is no measured slack to claim until the instrument is
237
+ alive and current again.
238
+
239
+ ## Step 4: Cadence (host-owned — documented, never scheduled)
240
+
241
+ This lens ships **no scheduler**, and building one is out of scope; cadence
242
+ belongs to the host that invokes it. Document the intent and let the operator
243
+ or the host's own timer drive it: **monthly** for a codebase-wide pass (long
244
+ enough for `trend[]` to hold signal, short enough to catch a stale instrument
245
+ before a release leans on it); **after a large refactor lands**, when headroom
246
+ appears and is most likely to be silently re-spent; and **before any floor is
247
+ raised**, so the raise is argued against measured headroom rather than
248
+ convenience. Nothing here self-triggers.
249
+
250
+ ## Step 5: Hand off to `/audit-to-stories`
251
+
252
+ The report is the deliverable. Hand it to the converter, which parses the
253
+ shared finding skeleton, fingerprints each finding for dedupe, and groups the
254
+ batch:
255
+
256
+ ```bash
257
+ node .agents/scripts/audit-to-stories.js --scan --glob temp/audits/audit-baselines-results.md --out temp/audits/audit-to-stories-plan.json
258
+ ```
259
+
260
+ Report the plan path and the group count; the converter owns everything
261
+ downstream, including whether a finding becomes a Story at all.
262
+
263
+ ## Report additions
264
+
265
+ Beyond the shared skeleton (Executive Summary + Detailed Findings from the
266
+ core), this report carries its own title, a Gate Surface Health table, a
267
+ Tightening Ledger, and the Dropped Hotspots budget log:
268
+
269
+ ```markdown
270
+ # Baseline & Ratchet Audit Report
271
+
272
+ ## Gate Surface Health
273
+
274
+ | Kind | Surface | Configured | Rows | Measured | Stale (days) | Stale (commits) | Verdict |
275
+ | --- | --- | --- | --- | --- | --- | --- | --- |
276
+ | [kind] | [gate / ratchet] | [yes / no] | [rowCount] | [measured.value measured.unit] | [staleDays or `null`] | [staleCommits or `null`] | [Live / Stub / Unenforced / Unreadable] |
277
+
278
+ ## Tightening Ledger
279
+
280
+ | Kind | Axis | Floor | Measured | Headroom | Trend | Proposed floor |
281
+ | --- | --- | --- | --- | --- | --- | --- |
282
+ | [kind] | [axis] | [floor] | [measured] | [headroom] | [improving / flat / worsening] | [value] |
283
+
284
+ ## Dropped Hotspots
285
+
286
+ | Cluster key | Rank | Gates |
287
+ | --- | --- | --- |
288
+ | [key] | [rank] | [gateKinds] |
289
+ ```
@@ -42,10 +42,11 @@ touched only one route file. Reachability is a global property: adding one
42
42
  route can orphan it, but removing or renaming a route elsewhere can also break
43
43
  a nav href that the change set never touched.
44
44
 
45
- Because of this, the navigability lens is registered in the **global-lens
46
- allowlist** (`GLOBAL_LENS_ALLOWLIST` in
47
- [`lib/audit-suite/selector.js`](../scripts/lib/audit-suite/selector.js)) and is
48
- **exempt from the cross-epic-leak guard** that narrows every other lens's
45
+ Because of this, the navigability lens declares `"scope": "global"` in
46
+ [`audit-rules.json`](../schemas/audit-rules.json) — the single source of truth
47
+ `resolveLensTier` in
48
+ [`lib/audit-suite/selector.js`](../scripts/lib/audit-suite/selector.js) reads
49
+ and is **exempt from the cross-epic-leak guard** that narrows every other lens's
49
50
  evidence to the change set's `changedFiles`. The exemption is scoped to this
50
51
  lens only — the guard is **not** weakened for any other lens, and it never lets
51
52
  a foreign change set leak into a scoped lens.
@@ -35,12 +35,15 @@ you read:
35
35
  | `/deliver` | bare | List the open `agent::ready` Stories and ask which to deliver. Deliver nothing until answered. |
36
36
  | `/deliver 4712` | ids | One Story via `helpers/deliver-story.md`, **inline in this session** — no `story-worker` spawn. |
37
37
  | `/deliver 4712 4713 …` | ids | Resolve the set, sequence by the discovered graph via `stories-wave-tick.js`, dispatch sub-agents. |
38
+ | `/deliver 4712 - 4716` | ids | A **range** — every id in the inclusive span. |
38
39
  | `/deliver add a --json flag to doctor` | prompt | Unplanned work: gate, author a receipt Story, land it — [`helpers/deliver-light.md`](helpers/deliver-light.md). |
39
40
 
40
- **The discriminator is lexical and total.** Every positional argument matching
41
- `^#?\d+$` means ids; anything else means a prompt. A **mixed** invocation (ids
42
- *and* prose) is a **hard error** refuse it and ask which was meant. A ticket
43
- not `type::story`, or carrying an `Epic: #N` footer, is a hard error too.
41
+ **The discriminator is lexical and total.** An argument matching `^#?\d+$` is an
42
+ id, and `^#?\d+\s*[-–—]\s*#?\d+$` an inclusive **range** pass one on as a
43
+ single unspaced token, never hand-expanded (reference § Ranges). Either shape
44
+ means ids; anything else means a prompt. A **mixed** invocation (ids *and*
45
+ prose) is a **hard error** — refuse it and ask which was meant. A ticket not
46
+ `type::story`, or carrying an `Epic: #N` footer, is a hard error too.
44
47
 
45
48
  ## Saying what you want
46
49
 
@@ -103,6 +106,12 @@ it to an operator or add it to an attended run.
103
106
  `node .agents/scripts/plan-run-epilogue.js --stories 101,102`. N=1 skips it
104
107
  ([reference § Per-run epilogue](helpers/deliver-reference.md)).
105
108
 
109
+ 5. **Correct what the change invalidated.** If a memory you recalled this
110
+ session is now wrong — a trap this landed, a budget it moved — fix that
111
+ entry now, while both the old belief and the new fact are in context, and
112
+ say so when you report. No memory substrate → skip silently. Sweeping the
113
+ whole pool is [`/memory-consolidate`](memory-consolidate.md), not this step.
114
+
106
115
  ## Closing what the workers hand back {#tail}
107
116
 
108
117
  **The tail is the orchestrator's, not the worker's.** A dispatched
@@ -40,8 +40,8 @@ mid-delivery, and evaluates the actual work product.
40
40
  `resolveCeremonyForRisk`). **Never run both**, and never run a
41
41
  preliminary self-assessment pass before dispatching the fresh critic —
42
42
  the redundant pre-pass buys no measurable quality and roughly triples
43
- the acceptance-block cost. Step 2's gate is the deterministic **scorer**
44
- of the one authored verdict, not a second (or third) pass over the
43
+ the acceptance-block cost. Step 3's gate is the deterministic **scorer**
44
+ of the one merged verdict, not a second (or third) pass over the
45
45
  criteria.
46
46
 
47
47
  > **Sub-agent type + derived-level ceremony.** When
@@ -134,20 +134,56 @@ mid-delivery, and evaluates the actual work product.
134
134
  fresh — a false-fresh coverage record without `coverage-final.json`
135
135
  silently weakens the floor. Limit the evidence-share to `lint` and
136
136
  `typecheck`.
137
- - Emits a verdict file under `temp/` conforming to
137
+ - Emits a **cluster** verdict file under `temp/` conforming to
138
138
  [`acceptance-eval-verdict.schema.json`](../../schemas/acceptance-eval-verdict.schema.json):
139
139
  one `{ index, criterion, verdict: met|partial|unmet, evidence,
140
- verifyEvidence[] }` record per acceptance item.
141
- 2. **Decide.** Run the gate against the verdict (the caller's Step 1a names the
142
- exact invocation omit `--epic`). The gate **scores the single verdict
143
- the round's owner authored** — schema validation, round cap, decision —
144
- and never re-scores the criteria itself:
140
+ verifyEvidence[] }` record per acceptance item **in that cluster**, each
141
+ `index` being the item's position in the Story's full `acceptance[]`
142
+ array. A fresh critic **returns that path to you** rather than calling the
143
+ gate itself.
144
+ 2. **Dispatch the round's clusters in parallel, then merge into one verdict.**
145
+ The clusters of a round are independent, so dispatch **all** of the round's
146
+ fresh critics as N `Agent` calls **in a single assistant turn** —
147
+ [`parallel-tooling.md`](parallel-tooling.md) **Rule 3** — never serially,
148
+ and never one round per cluster. Clusters routed `inline` are authored in
149
+ the same round alongside them.
150
+
151
+ Then **merge** the cluster verdicts into **one** verdict file under `temp/`:
152
+ concatenate every cluster's `criteria[]` records and order the merged array
153
+ by `index`, so it holds exactly one record per `acceptance[]` item in
154
+ **acceptance-array order**, under a single top-level `storyId`,
155
+ `schemaVersion`, `round` and `commitSha`. The verdict schema deliberately
156
+ carries **no `clusterId`** — the round's artifact is the merged verdict, and
157
+ which critic scored which record is not part of the contract.
158
+
159
+ > **Why one gate call and not N.** The round counter is **Story-scoped** —
160
+ > derived by counting `acceptance-eval` signals in the Story's
161
+ > `signals.ndjson` — and each cluster verdict has a distinct fingerprint, so
162
+ > the replay guard never collapses them. A gate call per cluster would spend
163
+ > one of the (default 2) rounds *per cluster*, so a Story with more than 8
164
+ > acceptance criteria would exhaust its redraft budget on cluster arithmetic
165
+ > alone; N concurrent calls would also race that same ledger. Cluster-scoped
166
+ > round counting exists in
167
+ > [`acceptance-eval-decision.js`](../../scripts/lib/orchestration/acceptance-eval-decision.js)
168
+ > but requires an integer `epicId`, which v2 pins `null` — it is not a way
169
+ > around the merge.
170
+ 3. **Decide — exactly one gate call per round.** Run the gate against the
171
+ **merged** verdict (the caller's Step 1a names the exact invocation — omit
172
+ `--epic`). The gate **scores the single verdict the round produced** —
173
+ schema validation, round cap, decision — and never re-scores the criteria
174
+ itself:
145
175
 
146
176
  ```bash
147
177
  node <main-repo>/.agents/scripts/acceptance-eval.js \
148
- --story <storyId> --verdict <verdict-path>
178
+ --story <storyId> --verdict <merged-verdict-path> \
179
+ --expected-criteria <number of acceptance[] items>
149
180
  ```
150
181
 
182
+ Pass `--expected-criteria` from the `acceptance[]` count you already read
183
+ off the Story body: a verdict whose `criteria[]` length differs — a single
184
+ cluster's verdict handed over unmerged — is rejected **before scoring**,
185
+ with an error naming the merge contract and consuming **no round**.
186
+
151
187
  The gate validates the verdict against the schema, applies the round cap,
152
188
  emits the per-criterion `acceptance-eval` signal into the retro / feedback
153
189
  substrate, prints a JSON envelope, and exits with one of three decisions:
@@ -161,4 +197,5 @@ mid-delivery, and evaluates the actual work product.
161
197
  (transition to `agent::blocked`) and post a `friction` comment naming the
162
198
  unmet criteria and their evidence. Never silently proceed to close.
163
199
 
164
- Write the verdict files under `temp/` only — they are scratch artifacts.
200
+ Write both the per-cluster verdicts and the merged verdict under `temp/` only —
201
+ they are scratch artifacts.
@@ -35,8 +35,15 @@ thresholds, sourced from
35
35
  | CC range | Action |
36
36
  | --- | --- |
37
37
  | ≤ 8 | Pass — no annotation required. |
38
- | > 8 (default `cyclomaticFlag`) | **Flag** in review: explain why, or split. The function is allowed to land but the audit report names it. |
39
- | > 12 (default `cyclomaticMustFix`) | **Must-fix**: refactor before the Story commits. `quality:preview` reports it as a violation; the close-validation chain refuses the merge. |
38
+ | > 8 (default `cyclomaticFlag`) | **Flag** `quality:preview` counts the function in its `new-method count over c=<flag>` column. The function is allowed to land but the report names it. |
39
+ | > 12 (default `cyclomaticMustFix`) | **Must-fix**: `check-cyclomatic.js` fails when a file gains a function above the ceiling, or when its worst function gets worse than the recorded baseline. |
40
+
41
+ `check-cyclomatic.js` is a **ratchet**, not a cliff: `baselines/cyclomatic.json`
42
+ records the over-ceiling functions a repository already carries, so adopting
43
+ the gate never demands a mass refactor. Burning a recorded breach down is
44
+ always allowed and re-records itself on the next `--update`; adding one is
45
+ what fails. It runs in the same required-check slot as `check-arch-cycles.js`
46
+ and `check-dead-exports.js`.
40
47
 
41
48
  A common refactor that pulls a 13-CC function under 8 is extracting the early-
42
49
  return guard chain into a named predicate, then collapsing the remaining
@@ -10,10 +10,9 @@ description: >-
10
10
  # Deliver digest (read once per session)
11
11
 
12
12
  > **Bundle, not a procedure.** [`deliver-story.md`](deliver-story.md) is still
13
- > the steps. This file is the material those steps referenced across five
14
- > separate files and a JSON schema bundled so one read covers the whole happy
15
- > path. Situational material (lease preflight, recovery routers, merge-wait
16
- > budgets, CI remediation) stays on demand in
13
+ > the steps; this file is the material they reference, bundled so one read
14
+ > covers the happy path. Situational material (lease preflight, recovery
15
+ > routers, merge-wait budgets, CI remediation) stays on demand in
17
16
  > [`deliver-story-reference.md`](deliver-story-reference.md) and
18
17
  > [`deliver-reference.md`](deliver-reference.md); read those **only** when an
19
18
  > envelope or a failure routes you there.
@@ -28,10 +27,10 @@ rule produces it:
28
27
  whatever its shape — sub-agent isolation is load-bearing only against a
29
28
  *concurrent* sibling racing the same checkout, and a one-Story run has none.
30
29
  2. **Every other run is `subagent`.** A multi-Story run dispatches every Story
31
- as a sub-agent however trivial its shape: a lite body does not conjure a
32
- second session for a sibling to run in, and the wave tick may hand you the
33
- whole set on one beat. Shape still sets ceremony and is reported alongside;
34
- the `route::lite` label is a human-visible hint, never the control signal.
30
+ as a sub-agent however trivial its shape a lite body does not conjure a
31
+ second session for a sibling, and the wave tick may hand you the whole set
32
+ on one beat. Shape still sets ceremony; the `route::lite` label is a
33
+ human-visible hint, never the control signal.
35
34
 
36
35
  `inline` removes model-side fan-out only — no `story-worker` boot, no fresh
37
36
  acceptance-critic spawn. **`subagent` and `inline` run the same engine**: same
@@ -54,21 +53,32 @@ the only sanctioned landing. A silent local build is not a delivery.
54
53
  ## 3. Change set — computed once, handed to everyone
55
54
 
56
55
  One enumeration per Story. A critic that re-runs its own `git diff`
57
- can score a different set than the one that routed it:
56
+ can score a different set than the one that routed it. Both routing calls take
57
+ a single options object and are **total — they never throw**, so a wrong-shaped
58
+ argument is silently absorbed into the `null` fail-safe:
58
59
 
59
60
  ```bash
60
61
  node --input-type=module -e '
61
- import { computeChangeSet } from "<main-repo>/.agents/scripts/lib/orchestration/change-set.js";
62
+ const lib = "<main-repo>/.agents/scripts/lib/orchestration";
63
+ const { computeChangeSet } = await import(`${lib}/change-set.js`);
64
+ const { deriveChangeLevel } = await import(`${lib}/review-depth.js`);
65
+ const { resolveCeremonyForRisk } = await import(`${lib}/ceremony-routing.js`);
62
66
  const { files } = computeChangeSet({ baseRef: "main", headRef: "story-<storyId>" });
63
- console.log(JSON.stringify(files));
67
+ // deriveChangeLevel({ changedFiles, injectedRules?, selectSensitivePathClassesFn? })
68
+ // -> { level, classes } — an OBJECT, never a bare level.
69
+ const { level, classes } = deriveChangeLevel({ changedFiles: files });
70
+ // resolveCeremonyForRisk({ derivedLevel, clusterIndex?, freshCriticSampleRate?,
71
+ // ceremonyProfile? }) -> { mode, reason, sampled, profile, verdictOwner }.
72
+ // derivedLevel is that level STRING. Handing it the object above matches no
73
+ // tier, so it routes to the null fail-safe: a fresh critic, silently.
74
+ const ceremony = resolveCeremonyForRisk({ derivedLevel: level, clusterIndex: 0 });
75
+ console.log(JSON.stringify({ files, level, classes, ...ceremony }));
64
76
  '
65
77
  ```
66
78
 
67
- Derive the level with `deriveChangeLevel`
68
- ([`review-depth.js`](../../scripts/lib/orchestration/review-depth.js)) over
69
- that one list: a sensitive path registered in `audit-rules.json` → `high`, none
70
- → `low`, an unenumerable diff (`files === null`) → `null`. Resolve
71
- fresh-vs-inline critics with `resolveCeremonyForRisk`
79
+ Level rules ([`review-depth.js`](../../scripts/lib/orchestration/review-depth.js)):
80
+ a sensitive path registered in `audit-rules.json` → `high`, none → `low`, an
81
+ unenumerable diff (`files === null`) → `null`. Ceremony rules
72
82
  ([`ceremony-routing.js`](../../scripts/lib/orchestration/ceremony-routing.js)):
73
83
  `minimal` → always inline, `strict` → always fresh, `standard` → `high`/`null`
74
84
  → fresh and `low` → inline unless the `freshCriticSampleRate` floor forces
@@ -78,16 +88,26 @@ fresh. An `inline` dispatch mode overrides all of it to inline critics. Close's
78
88
  ## 4. Acceptance self-eval (Step 1a, required)
79
89
 
80
90
  **One verdict-owner per cluster** — the fresh critic *or* the inline
81
- self-eval, named by `verdictOwner`, never both and never a warm-up pass. It
82
- scores each `acceptance[]` item against the change set above, with `verify[]`
83
- output as evidence. Bounded by `delivery.acceptanceEval.maxRounds` (default 2).
84
- Then score the authored verdict:
91
+ self-eval, named by `verdictOwner`, never both and never a warm-up pass. Each
92
+ scores its cluster's `acceptance[]` items against the change set above, with
93
+ `verify[]` output as evidence. Bounded by `delivery.acceptanceEval.maxRounds`
94
+ (default 2).
95
+
96
+ **One round = N cluster critics → ONE merged verdict → ONE gate call.** Merge
97
+ every cluster's records into a single `criteria[]` in `acceptance[]` order, one
98
+ per acceptance item, and score that once. A gate call per cluster spends a
99
+ round *per cluster* and races the round ledger:
85
100
 
86
101
  ```bash
87
102
  node <main-repo>/.agents/scripts/acceptance-eval.js \
88
- --story <storyId> --verdict <verdict-path>
103
+ --story <storyId> --verdict <merged-verdict-path> \
104
+ --expected-criteria <acceptance[] count>
89
105
  ```
90
106
 
107
+ Pass `--expected-criteria` — **without it the coverage assertion is inert**, so
108
+ an unmerged cluster verdict scores a fraction of the criteria and still reports
109
+ `proceed`. A mismatch is rejected before scoring and costs no round.
110
+
91
111
  `proceed` → close. `redraft` → one more round inside the cap. `block` → **do
92
112
  not close**: post a `friction` comment and flip `agent::blocked`.
93
113
  Per-round mechanics: [`acceptance-self-eval.md`](acceptance-self-eval.md).