mandrel 2.30.0 → 2.32.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 (253) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. package/.agents/workflows/helpers/signals.md +0 -112
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * update-dead-exports-baseline.js — the producer behind the dead-export
5
+ * ratchet (Story #5011).
6
+ *
7
+ * `check-dead-exports.js` has shipped since Story #1852 with no way to write
8
+ * the baseline it ratchets against. Every seed and every refresh was therefore
9
+ * a hand-edit: run the checker with `--json`, copy `currentRows` out of the
10
+ * report, re-sort them, and preserve `$schema` / `kernelVersion` / `mode` by
11
+ * hand. That is the gap this CLI closes — dead-exports now sits beside its four
12
+ * `update-*-baseline.js` siblings instead of being the one baseline an operator
13
+ * had to author in a text editor.
14
+ *
15
+ * **Fail closed, unlike the checker.** `check-dead-exports.js` treats a knip
16
+ * spawn or parse failure as advisory (exit 0 + a stderr warning) because it
17
+ * still holds a committed snapshot to compare against — a broken knip install
18
+ * must not redden CI on its own. The producer has no such fallback: the file it
19
+ * is about to write *is* the snapshot. An empty row set persisted from a failed
20
+ * run would silently grandfather every dead export in the repository and blind
21
+ * the ratchet permanently. So this CLI exits non-zero and writes **nothing**
22
+ * whenever knip cannot run, its report cannot be parsed, or the knip version
23
+ * cannot be resolved. That asymmetry with the checker is deliberate.
24
+ *
25
+ * **Envelope, not the shared writer.** Dead-exports does not route through
26
+ * `lib/baselines/writer.js`: that writer admits only the kinds registered in
27
+ * `lib/baselines/envelope.js` and requires a `*` rollup row, while dead-exports
28
+ * is an out-of-band ratchet kind (`lib/audit-baselines/kinds.js`) that carries
29
+ * no rollup. This CLI writes exactly the shape the checker already reads and
30
+ * both committed baselines already carry:
31
+ *
32
+ * { $schema, kernelVersion, generatedAt, [mode], rows: [{ file, symbol }] }
33
+ *
34
+ * `kernelVersion` is knip's own installed version — knip is the scorer, so a
35
+ * knip upgrade is what invalidates the rows. `mode` is stamped on the
36
+ * `--production` pass only, matching the committed production baseline.
37
+ *
38
+ * Rows are de-duplicated and sorted by `(file, symbol)` with the same
39
+ * comparator `check-dead-exports.js` uses for its diff output, so a re-run
40
+ * against an unchanged tree differs only in `generatedAt` and review sees real
41
+ * movement rather than reordering noise.
42
+ *
43
+ * The pass is selected with `--production`, and the baseline/label/mode triple
44
+ * comes from `lib/dead-exports-mode.js` — the same resolver the checker uses.
45
+ * Resolving that pairing independently here is precisely how a producer would
46
+ * end up writing production rows over the default baseline.
47
+ */
48
+
49
+ import fs from 'node:fs';
50
+ import path from 'node:path';
51
+ import process from 'node:process';
52
+ import { runAsCli } from './lib/cli-utils.js';
53
+ import {
54
+ extractRowsFromKnip,
55
+ readKnipOutput,
56
+ runKnip,
57
+ } from './lib/dead-exports-knip.js';
58
+ import { resolveDeadExportsMode } from './lib/dead-exports-mode.js';
59
+
60
+ /** `$schema` ref stamped into every dead-export baseline envelope. */
61
+ export const DEAD_EXPORTS_SCHEMA_REF =
62
+ 'https://mandrel.dev/baselines/dead-exports.schema.json';
63
+
64
+ /**
65
+ * Parse argv for `--production`, `--baseline <path>` and `--knip-output
66
+ * <path>`. `--knip-output` is the test seam: it feeds a pre-captured knip JSON
67
+ * report instead of spawning knip, and mirrors the checker's flag of the same
68
+ * name so a captured report drives both sides of the ratchet.
69
+ *
70
+ * @param {string[]} argv
71
+ * @returns {{ baselinePath: string | null, knipOutputPath: string | null, production: boolean }}
72
+ */
73
+ export function parseArgv(argv = []) {
74
+ const out = { baselinePath: null, knipOutputPath: null, production: false };
75
+ const valueFlags = {
76
+ '--baseline': 'baselinePath',
77
+ '--knip-output': 'knipOutputPath',
78
+ };
79
+ for (let i = 0; i < argv.length; i += 1) {
80
+ const arg = argv[i];
81
+ if (arg === '--production') {
82
+ out.production = true;
83
+ continue;
84
+ }
85
+ const field = valueFlags[arg];
86
+ const next = argv[i + 1];
87
+ if (field && next && !next.startsWith('--')) {
88
+ out[field] = next;
89
+ i += 1;
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Resolve the knip version to stamp as `kernelVersion`, read from the
97
+ * installed package rather than recalled. Returns `null` when knip is not
98
+ * installed or its manifest is unreadable — the caller treats that as a
99
+ * fail-closed condition, because an unstamped baseline cannot signal to a
100
+ * later run that the scorer moved underneath it.
101
+ *
102
+ * @param {{ cwd?: string, readFileImpl?: typeof fs.readFileSync }} [opts]
103
+ * @returns {string | null}
104
+ */
105
+ export function resolveKnipKernelVersion({
106
+ cwd = process.cwd(),
107
+ readFileImpl = fs.readFileSync,
108
+ } = {}) {
109
+ try {
110
+ const manifest = path.resolve(cwd, 'node_modules', 'knip', 'package.json');
111
+ const version = JSON.parse(readFileImpl(manifest, 'utf-8'))?.version;
112
+ return typeof version === 'string' && version.length > 0 ? version : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * De-duplicate and sort `{ file, symbol }` rows into the committed order.
120
+ *
121
+ * Identity is `(file, symbol)` — the same identity `diffRows` in
122
+ * `check-dead-exports.js` uses — so two knip issue records naming the same
123
+ * dead file collapse to one row. The comparator is that checker's comparator,
124
+ * which is why re-sorting an already-committed baseline is a no-op.
125
+ *
126
+ * @param {Array<{ file?: unknown, symbol?: unknown }>} rows
127
+ * @returns {Array<{ file: string, symbol: string }>}
128
+ */
129
+ export function normalizeRows(rows) {
130
+ const seen = new Set();
131
+ const out = [];
132
+ for (const row of rows ?? []) {
133
+ if (typeof row?.file !== 'string' || typeof row?.symbol !== 'string')
134
+ continue;
135
+ const key = `${row.file}\0${row.symbol}`;
136
+ if (seen.has(key)) continue;
137
+ seen.add(key);
138
+ out.push({ file: row.file, symbol: row.symbol });
139
+ }
140
+ return out.sort(
141
+ (a, b) => a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol),
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Assemble the baseline envelope. `mode` is stamped on the production pass
147
+ * only: the default baseline has never carried the key, and adding it would
148
+ * churn the committed file for no signal.
149
+ *
150
+ * @param {{ kernelVersion: string, mode: string, rows: Array<object>, generatedAt: string }} args
151
+ * @returns {object}
152
+ */
153
+ export function buildEnvelope({ kernelVersion, mode, rows, generatedAt }) {
154
+ const envelope = {
155
+ $schema: DEAD_EXPORTS_SCHEMA_REF,
156
+ kernelVersion,
157
+ generatedAt,
158
+ };
159
+ if (mode === 'production') envelope.mode = mode;
160
+ envelope.rows = rows;
161
+ return envelope;
162
+ }
163
+
164
+ /**
165
+ * Obtain a knip report, either from the `--knip-output` seam or by spawning
166
+ * knip for the requested pass. Never throws; a failure is reported as
167
+ * `{ ok: false, error }` so the caller can fail closed without a try/catch.
168
+ *
169
+ * @param {{
170
+ * cwd: string,
171
+ * production: boolean,
172
+ * knipOutputPath: string | null,
173
+ * runKnipImpl: typeof runKnip,
174
+ * readKnipOutputImpl: typeof readKnipOutput,
175
+ * }} args
176
+ * @returns {{ ok: true, envelope: unknown } | { ok: false, error: string }}
177
+ */
178
+ export function collectKnipReport({
179
+ cwd,
180
+ production,
181
+ knipOutputPath,
182
+ runKnipImpl,
183
+ readKnipOutputImpl,
184
+ }) {
185
+ if (knipOutputPath) {
186
+ const envelope = readKnipOutputImpl(path.resolve(cwd, knipOutputPath));
187
+ return envelope == null
188
+ ? { ok: false, error: `could not read knip report at ${knipOutputPath}` }
189
+ : { ok: true, envelope };
190
+ }
191
+ return runKnipImpl({ cwd, production });
192
+ }
193
+
194
+ /**
195
+ * Reject a report the extractor cannot read. `extractRowsFromKnip` is total —
196
+ * it answers `[]` for any shape it does not recognise — which is the right
197
+ * posture for an advisory checker and the wrong one for a producer: an
198
+ * unrecognised report and a genuinely clean repository would persist the same
199
+ * empty row set. Returns an error string, or `null` when the report is usable.
200
+ *
201
+ * @param {unknown} envelope
202
+ * @returns {string | null}
203
+ */
204
+ export function describeUnusableReport(envelope) {
205
+ if (!envelope || typeof envelope !== 'object')
206
+ return 'knip report is not a JSON object';
207
+ if (!Array.isArray(envelope.issues))
208
+ return 'knip report carries no `issues` array';
209
+ return null;
210
+ }
211
+
212
+ /**
213
+ * Top-level CLI entry, exported so tests drive the whole pipeline through
214
+ * injected hooks without spawning knip or touching a committed baseline.
215
+ *
216
+ * @param {{
217
+ * argv?: string[],
218
+ * cwd?: string,
219
+ * stdout?: { write: (s: string) => void },
220
+ * stderr?: { write: (s: string) => void },
221
+ * runKnipImpl?: typeof runKnip,
222
+ * readKnipOutputImpl?: typeof readKnipOutput,
223
+ * readFileImpl?: typeof fs.readFileSync,
224
+ * writeFileImpl?: typeof fs.writeFileSync,
225
+ * renameImpl?: typeof fs.renameSync,
226
+ * now?: () => string,
227
+ * }} [opts]
228
+ * @returns {Promise<number>} 0 on a written baseline; 1 on any fail-closed path.
229
+ */
230
+ export async function runCli({
231
+ argv = process.argv.slice(2),
232
+ cwd = process.cwd(),
233
+ stdout = process.stdout,
234
+ stderr = process.stderr,
235
+ runKnipImpl = runKnip,
236
+ readKnipOutputImpl = readKnipOutput,
237
+ readFileImpl = fs.readFileSync,
238
+ writeFileImpl = fs.writeFileSync,
239
+ renameImpl = fs.renameSync,
240
+ now = () => new Date().toISOString(),
241
+ } = {}) {
242
+ const { baselinePath, knipOutputPath, production } = parseArgv(argv);
243
+ const { mode, label, baseline } = resolveDeadExportsMode(production);
244
+ const target = path.resolve(cwd, baselinePath ?? baseline);
245
+
246
+ const kernelVersion = resolveKnipKernelVersion({ cwd, readFileImpl });
247
+ if (!kernelVersion) {
248
+ stderr.write(
249
+ `[${label}] ❌ cannot resolve knip's version from node_modules/knip/package.json — refusing to write ${target}\n`,
250
+ );
251
+ return 1;
252
+ }
253
+
254
+ const report = collectKnipReport({
255
+ cwd,
256
+ production,
257
+ knipOutputPath,
258
+ runKnipImpl,
259
+ readKnipOutputImpl,
260
+ });
261
+ const failure = report.ok
262
+ ? describeUnusableReport(report.envelope)
263
+ : report.error;
264
+ if (failure) {
265
+ stderr.write(
266
+ `[${label}] ❌ ${failure} — refusing to write ${target} (an empty baseline would grandfather every dead export)\n`,
267
+ );
268
+ return 1;
269
+ }
270
+
271
+ const rows = normalizeRows(extractRowsFromKnip(report.envelope));
272
+ const envelope = buildEnvelope({
273
+ kernelVersion,
274
+ mode,
275
+ rows,
276
+ generatedAt: now(),
277
+ });
278
+ // Write-then-rename, matching `lib/baselines/writer.js`: a crash or a full
279
+ // disk mid-write must not leave a truncated envelope behind. An unparseable
280
+ // baseline reads as empty to `check-dead-exports.js`, which would report
281
+ // every pre-existing row as newly added.
282
+ const tmpTarget = `${target}.tmp`;
283
+ writeFileImpl(tmpTarget, `${JSON.stringify(envelope, null, 2)}\n`, 'utf-8');
284
+ renameImpl(tmpTarget, target);
285
+ stdout.write(
286
+ `[${label}] ✅ wrote ${rows.length} row(s) to ${target} (kernelVersion=${kernelVersion}).\n`,
287
+ );
288
+ return 0;
289
+ }
290
+
291
+ async function main() {
292
+ return runCli();
293
+ }
294
+
295
+ runAsCli(import.meta.url, main, {
296
+ source: 'dead-exports-baseline',
297
+ propagateExitCode: true,
298
+ errorPrefix: '[dead-exports-baseline] ❌ Fatal error',
299
+ usage: {
300
+ invocation:
301
+ 'node .agents/scripts/update-dead-exports-baseline.js [--production] [--baseline <path>] [--knip-output <path>]',
302
+ summary:
303
+ 'Scan → write one dead-export baseline. Regenerates the rows `check-dead-exports.js` ratchets against, so the snapshot is produced rather than hand-edited.',
304
+ flags: [
305
+ [
306
+ '--production',
307
+ 'Write the production-pass baseline (baselines/dead-exports-production.json) instead of the default one.',
308
+ ],
309
+ ['--baseline <path>', 'Write to this path instead of the mode default.'],
310
+ [
311
+ '--knip-output <path>',
312
+ 'Read a saved knip JSON report instead of running knip.',
313
+ ],
314
+ ],
315
+ notes: [
316
+ 'Fails closed: when knip cannot run or its report cannot be parsed, the CLI exits 1 and writes nothing — the checker is advisory on that failure, the producer must not be.',
317
+ 'There is no scope flag: knip scores the whole graph, so a diff-scoped refresh is not expressible. Run `npm run dead-exports:update` to refresh both passes.',
318
+ 'Exit codes:\n 0 baseline written\n 1 knip unavailable, unparseable, or version unresolvable',
319
+ ],
320
+ },
321
+ });
@@ -18,7 +18,7 @@ allowed_tools:
18
18
  - **No gate may be skipped.** Failing lint means fix lint, not disable the rule; a failing test means fix the code, not `.skip` or delete the test. Gates are ordered shift-left so cheap checks fail first, and CI failure output is fed back verbatim with the directive to reproduce and fix locally before re-pushing.
19
19
  - **Introducing a gate that asserts on pre-existing state** (doc-drift, lint-vocabulary, dependency-cycle, missing-coverage) MUST land green at merge: either advisory-first (report-only until the backlog is burned down) or with the populated baseline committed in the same change that turns the gate on. Never wire a gate into `requiredChecks` that lands red on latent findings nobody authored.
20
20
  - **Refresh a baseline only when the change is deliberate** — a rename/move, an operator-approved complexity bump, a signed-off perf delta, an intentional API-surface change. Never refresh to paper over an unintentional regression; fix the regression instead.
21
- - **Pick the refresh that matches why you are refreshing** — the two are different scopes, and the wrong one silently no-ops. `npm run <kind>:update` is **diff-scoped**: it re-scores only files changed in `origin/main..HEAD` and preserves every other row verbatim. That is what you want after changing code. `npm run <kind>:reanchor` is the same script with `--full-scope`: it re-scores every file in every target dir. That is what you want when the baseline is **stale** — a scorer-version bump, a coverage-shape change, accumulated drift — because a diff-scoped run would leave the stale rows exactly as they were and the staleness finding would re-fire. Kinds: `coverage`, `crap`, `maintainability`, `duplication`; dead-exports and lighthouse have no npm scriptregenerate the rows and edit `baselines/dead-exports*.json` / `baselines/lighthouse.json` directly. Run either on the **Story branch**, not on `main`.
21
+ - **Pick the refresh that matches why you are refreshing** — the two are different scopes, and the wrong one silently no-ops. `npm run <kind>:update` is **diff-scoped**: it re-scores only files changed in `origin/main..HEAD` and preserves every other row verbatim. That is what you want after changing code. `npm run <kind>:reanchor` is the same script with `--full-scope`: it re-scores every file in every target dir. That is what you want when the baseline is **stale** — a scorer-version bump, a coverage-shape change, accumulated drift — because a diff-scoped run would leave the stale rows exactly as they were and the staleness finding would re-fire. Kinds with both scopes: `coverage`, `crap`, `maintainability`, `duplication`. Dead-exports has a producer but no scope axisknip scores the whole graph, so `npm run dead-exports:update` (both passes, via `.agents/scripts/update-dead-exports-baseline.js`) is the only sanctioned way to write `baselines/dead-exports.json` / `baselines/dead-exports-production.json`; never hand-edit those files. Lighthouse still has no producer — edit `baselines/lighthouse.json` directly. Run any of them on the **Story branch**, not on `main`.
22
22
  - Verify the refresh diff is scoped to the relevant `baselines/<kind>.json` (plus cosmetic `package-lock.json` churn only). If unrelated files appear, STOP — the refresh is contaminated. Stage baseline files **explicitly** (`git add baselines/<kind>.json`); never `git add -A` in a refresh commit.
23
23
  - Commit-subject contract: a **Conventional-Commits** subject `chore(baselines): refresh <kind> snapshot for <reason>` — never an ad-hoc leading token like `baseline-refresh:` (commitlint and the planner validator reject it). The body is **mandatory** and non-empty: what changed, why the new floor is correct, and the Story that triggered it.
24
24
  - Add the machine-readable trailer `baseline-refresh: true` (git-trailer `Key: value` style) and `Story: #<storyId>` to the body whenever observability classification matters. Never pass `--no-verify`; the `commit-msg` hook (commitlint) MUST run and pass.
@@ -101,7 +101,7 @@ own as the parseable marker for any future reader.)
101
101
  | Maintainability | `npm run maintainability:update` | `npm run maintainability:reanchor` |
102
102
  | Coverage | `npm run coverage:update` | `npm run coverage:reanchor` |
103
103
  | Duplication | `npm run duplication:update` | `npm run duplication:reanchor` |
104
- | Dead-exports | edit `baselines/dead-exports.json` / `baselines/dead-exports-production.json` (rows are `(file, symbol)`; `check-dead-exports.js --json` prints the current rows) | same — no scope flag applies |
104
+ | Dead-exports | `npm run dead-exports:update` (regenerates both passes; never hand-edit the JSON) | same — no scope flag applies |
105
105
  | Lighthouse | edit `baselines/lighthouse.json` | same — no scope flag applies |
106
106
 
107
107
  1. **Run the matching command for your reason** — left column after a code
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-08-02T11:35:53.522Z",
2
+ "generatedAt": "2026-08-06T10:26:18.702Z",
3
3
  "generator": "generate-skills-index.js@1",
4
4
  "skills": [
5
5
  {
@@ -42,16 +42,6 @@
42
42
  "allowedTools": null,
43
43
  "vendor": null
44
44
  },
45
- {
46
- "name": "diagnose-friction",
47
- "tier": "core",
48
- "category": "core",
49
- "path": ".agents/skills/core/diagnose-friction/SKILL.md",
50
- "description": "Wrap a shell command with diagnostic capture. On failure, print static suggestions and append a structured `friction` record to the per-Story signals.ndjson stream. Use whenever a script in the orchestration loop invokes a tool whose failure shape we want the analyzer to attribute.",
51
- "policyCapsuleBullets": 9,
52
- "allowedTools": ["Bash", "Read"],
53
- "vendor": null
54
- },
55
45
  {
56
46
  "name": "documentation-and-adrs",
57
47
  "tier": "core",
@@ -148,7 +138,7 @@
148
138
  "category": "qa",
149
139
  "path": ".agents/skills/stack/qa/playwright/SKILL.md",
150
140
  "description": "Robust E2E browser testing with Playwright. Use when writing browser-driven tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse `storageState` for auth, and enable trace-on-first-retry for CI debugging.",
151
- "policyCapsuleBullets": 7,
141
+ "policyCapsuleBullets": 8,
152
142
  "allowedTools": null,
153
143
  "vendor": "playwright"
154
144
  },
@@ -19,3 +19,51 @@ vendor: playwright
19
19
  - Write tests independent of one another so they run in parallel; clean up shared state in fixtures, not afterwards.
20
20
  - Enable `trace: 'on-first-retry'` (or `'retain-on-failure'`) so CI failures are debuggable in the Trace Viewer.
21
21
  - Use a unique data set per test run, or tear down state explicitly, to prevent cross-test contamination.
22
+ - Never let Playwright own the lifetime of a dev server it did not start: boot the server out-of-band, point the suite at the running origin, and set `reuseExistingServer` so `webServer` only probes readiness.
23
+
24
+ ## Running a `webServer`-backed suite outside CI
25
+
26
+ Playwright's `webServer` block **watches the process it spawned**. That
27
+ assumption holds for a dev server that stays in the foreground, and breaks for
28
+ any manager that daemonizes one — the foreground process exits `0` while the
29
+ server keeps serving, Playwright reads the exit as a crash, and the run aborts
30
+ before a single test executes:
31
+
32
+ ```text
33
+ Process from config.webServer exited early
34
+ ```
35
+
36
+ Read that line as a **lifetime-ownership mismatch, not a flake**. It reproduces
37
+ on every invocation, clean tree or not, and no amount of retrying, tree-cleaning
38
+ or timeout-raising changes it. Agent sandboxes and IDE harnesses commonly manage
39
+ dev servers this way (`Dev server already running at … (pid N)`), so an agent
40
+ meets this far more often than a developer does.
41
+
42
+ ### Attach, don't boot
43
+
44
+ Invert the ownership instead of fighting it — the manager owns the process,
45
+ Playwright owns only the probe:
46
+
47
+ 1. **Boot the server out-of-band** through whatever manages it, and confirm it
48
+ is serving. Its lifetime is now the manager's concern, not the runner's.
49
+ 2. **Point the suite at the already-running origin** — set the config's
50
+ `baseURL` (or the `webServer.url` the block probes) to that origin, via the
51
+ project's own environment seam rather than an edit to committed config.
52
+ 3. **Set `reuseExistingServer: true`** so Playwright probes the URL, finds it
53
+ live, and never spawns or supervises a process of its own.
54
+
55
+ This is the same convention the QA harness already encodes as
56
+ `qa.environments[].baseUrl`: attach to a running origin, never boot one. A suite
57
+ run this way exercises identical browser behavior — only the process supervision
58
+ differs.
59
+
60
+ ### When no attachable origin exists
61
+
62
+ Some apps genuinely cannot be reached this way — the server is unreachable from
63
+ the sandbox, or the suite depends on a build step the sandbox cannot run. Do
64
+ **not** burn a timebox rediscovering that. Record the observed signature, state
65
+ which of the three steps above failed, and escalate on the first encounter:
66
+ that evidence is exactly what the `unreproducible-tier` verdict in
67
+ [`ci-remediation.md`](../../../../rules/ci-remediation.md) requires, and it is
68
+ the only verdict that lets an unrunnable tier route somewhere other than a dead
69
+ end.
@@ -7,10 +7,10 @@ description: Audit the repository's main documentation for staleness, semantic d
7
7
  You are a Staff Engineer & Documentation Steward verifying the repository's prose
8
8
  documentation is **up to date and complete**. Prose rots silently: commands get
9
9
  renamed, scripts move, workflows change shape, version/topology claims go stale.
10
- The deterministic gates (`check-doc-links.js`, `check-lifecycle-doc-drift.js`,
11
- `validate-docs-freshness.js`) catch broken links, generator drift, and
12
- per-delivery freshness they cannot tell whether the prose still describes how
13
- the code actually behaves. That semantic verification is this lens's job. The
10
+ The deterministic gates (`check-doc-links.js` and the generators' `--check`
11
+ mode) catch broken links and generator drift — they cannot tell whether the
12
+ prose still describes how the code actually behaves. That semantic
13
+ verification is this lens's job. The
14
14
  shared lens machinery — read-only constraint, scope interpretation, report
15
15
  envelope + finding-block skeleton, severity scale, self-cross-check, and
16
16
  execution strategy — lives in
@@ -78,7 +78,6 @@ are cheap, exact, and de-duplicate the easy findings:
78
78
 
79
79
  ```bash
80
80
  node .agents/scripts/check-doc-links.js
81
- node .agents/scripts/check-lifecycle-doc-drift.js
82
81
  node .agents/scripts/generate-config-docs.js --check
83
82
  node .agents/scripts/generate-lifecycle-docs.js --check
84
83
  node .agents/scripts/generate-workflows-doc.js --check
@@ -87,7 +86,7 @@ node .agents/scripts/resolve-doc-tiers.js --json
87
86
 
88
87
  Fold the results in as findings:
89
88
 
90
- - **Checker failures** (broken links, lifecycle drift) become individual
89
+ - **Checker failures** (broken links, generator drift) become individual
91
90
  findings with `Category: Link Integrity` (or `Generator Drift` for the
92
91
  lifecycle gate), citing the checker output verbatim.
93
92
  - **Generator dirtiness** (any `--check` reporting stale output, including
@@ -227,8 +227,8 @@ runs FIRST and widens the net across open + closed issues; the exact
227
227
  was reworded but whose *location* is unchanged still confirms against the Issue
228
228
  that already tracks that location, because the audit filers stamp a
229
229
  location-based `audit-semantic-keys` footer alongside the `audit-fingerprints`
230
- footer. Close-time filings from the
231
- [`audit-results-graduator`](../scripts/lib/feedback-loop/audit-results-graduator.js)
230
+ footer. Filings from the
231
+ [`retro-proposals-graduator`](../scripts/lib/feedback-loop/retro-proposals-graduator.js)
232
232
  carry the same canonical `audit-fingerprints` footer, so a sweep recognizes a
233
233
  graduator-filed issue and never re-files it.
234
234
 
@@ -229,18 +229,17 @@ which path produced it.
229
229
  > stage before synthesising the report. It derives its per-dimension prompts
230
230
  > from the *lens* markdown at run time — the lens stays the single source of
231
231
  > truth. This is a performance optimization over path 1, **not** a separate
232
- > contract: strategy selection lives in
233
- > [`../../scripts/lib/dynamic-workflow/capability.js`](../../scripts/lib/dynamic-workflow/capability.js)
234
- > (`selectAuditStrategy`), and it is not covered by the No-Shim / hard-cutover
235
- > rule in [`../../rules/git-conventions.md`](../../rules/git-conventions.md)
236
- > because there is one report contract and only the execution strategy varies —
237
- > the same capability-degradation pattern the protocol endorses for live-docs
238
- > fallback. Force a path for testing with `MANDREL_AUDIT_STRATEGY=sequential`
239
- > or `MANDREL_AUDIT_STRATEGY=orchestrated`; exercise the real disable signals
240
- > with `CLAUDE_CODE_DISABLE_WORKFLOWS=1` or `disableWorkflows: true` in
241
- > `.claude/settings.json`. On the orchestrated path the analysis subagents are
242
- > granted only read/search tools (`Read`, `Grep`, `Glob`) — the single write is
243
- > the final report artifact.
232
+ > contract, and it is not covered by the No-Shim / hard-cutover rule in
233
+ > [`../../rules/git-conventions.md`](../../rules/git-conventions.md) because
234
+ > there is one report contract and only the execution strategy varies — the
235
+ > same capability-degradation pattern the protocol endorses for live-docs
236
+ > fallback. **The host owns the choice.** Mandrel ships no in-repo strategy
237
+ > selector and no force-override env var: Claude Code launches the saved
238
+ > workflow when it can, and you get path 1 or 2 above when it cannot.
239
+ > Suppress the orchestrated path with `CLAUDE_CODE_DISABLE_WORKFLOWS=1`
240
+ > or `disableWorkflows: true` in `.claude/settings.json`. On the orchestrated
241
+ > path the analysis subagents are granted only read/search tools (`Read`,
242
+ > `Grep`, `Glob`) — the single write is the final report artifact.
244
243
 
245
244
  ## Parallel tooling {#parallel-tooling}
246
245
 
@@ -95,17 +95,18 @@ suppress the noise.
95
95
 
96
96
  ## When a number changes
97
97
 
98
- Update three places **in the same commit**:
99
-
100
- 1. `delivery.quality.codingGuardrails.<key>` in
101
- [`agentrc-reference.json`](../../docs/agentrc-reference.json).
102
- 2. The matching schema bound in
103
- [`schemas/agentrc.schema.json`](../../schemas/agentrc.schema.json) and the
104
- AJV mirror in
105
- [`scripts/lib/config-settings-schema.js`](../../scripts/lib/config-settings-schema.js).
106
- 3. The threshold cell or sentence in this helper.
107
-
108
- The drift test (`tests/config-schema-mirror-drift.test.js`) catches schema
109
- divergence between the JSON mirror and the AJV runtime; the helper-prose
110
- drift is caught by `audit-clean-code` and `agent-protocol` linking back here
111
- rather than restating the numbers.
98
+ Update two places **in the same commit**:
99
+
100
+ 1. `CODING_GUARDRAILS_DEFAULTS` in
101
+ [`scripts/lib/config/quality.js`](../../scripts/lib/config/quality.js).
102
+ The schema literal in
103
+ [`config-settings-schema-quality.js`](../../scripts/lib/config-settings-schema-quality.js)
104
+ imports it, and `npm run docs:gen` propagates the number into
105
+ [`agentrc-reference.json`](../../docs/agentrc-reference.json), the shipped
106
+ JSON-Schema mirror, and the `configuration.md` key table.
107
+ 2. The threshold cell or sentence in this helper.
108
+
109
+ The generator-fidelity test (`tests/config-schema-mirror-drift.test.js`)
110
+ catches a stale generated artifact; the helper-prose drift is caught by
111
+ `audit-clean-code` and `agent-protocol` linking back here rather than
112
+ restating the numbers.
@@ -293,14 +293,9 @@ For every finding, provide:
293
293
 
294
294
  Findings that Step 4.5 remediated on `[HEAD_REF]` MUST be rendered under a
295
295
  dedicated **`## Fixed on-branch`** heading, **not** in the severity groups
296
- above. This is the contract seam that keeps remediated findings from
297
- spawning ghost follow-up issues: the
298
- [audit-results graduator](../../scripts/lib/feedback-loop/audit-results-graduator.js)
299
- (the sole canonical reader of the unified comment)
300
- skips every entry inside this section (both because a fixed entry is
301
- rendered with a **✅ prefix** — so it carries no leading severity emoji the
302
- parser would match — and because the parser has an explicit
303
- Fixed-on-branch section guard).
296
+ above. This keeps a remediated finding legible as remediated to every reader
297
+ of the unified comment — human or downstream — rather than reading as an
298
+ outstanding severity-grouped finding.
304
299
 
305
300
  Render each fixed finding as a `✅`-prefixed line naming its original
306
301
  severity, the file path in backticks, and the remediating commit SHA, e.g.:
@@ -118,7 +118,8 @@ engine runs, never what runs — gates, PR, and terminal envelope are identical.
118
118
  **Read the mode; never infer it from shape.** Before spawning
119
119
  anything, read the Story's `dispatchMode` from the resolver envelope
120
120
  (`stories[].dispatchMode`, produced by `resolveStoryDispatchMode` in
121
- `lib/orchestration/complexity-gate.js`). A Story with `dispatchMode: "inline"`
121
+ `lib/orchestration/complexity-gate.js`, which decides on the resolved set size
122
+ alone — it does not read the Story body). A Story with `dispatchMode: "inline"`
122
123
  executes [`deliver-story.md`](deliver-story.md) **inline in this session** — no
123
124
  `story-worker` sub-agent boot and no fresh acceptance-critic sub-agents
124
125
  (sub-agent boots are the dominant deliver-phase token cost at trivial scope) —
@@ -139,19 +139,18 @@ and the `rules/security-baseline.md` MUSTs all run exactly as for a
139
139
  full-ceremony Story. The lite route's `preserves` field is the machine-readable
140
140
  record of those non-negotiables; there is no lite-specific gate bypass.
141
141
 
142
- **Deliver derives the route from the Story body's shape and the
143
- dispatch mode from the run.** Persist stamps a lite cohort's Stories with the
144
- `route::lite` label as a _human-visible hint only_ (and ledgers the authored
145
- verdict — recorded reason plus per-Story shape evidence — on the
146
- `story-plan-state` checkpoint); the label is never the control signal.
147
- `/deliver` computes the route from the fetched Story body via
148
- `resolveStoryDispatchMode` (`lib/orchestration/complexity-gate.js`) the same
149
- shape taxonomy `deriveChangeLevel` applies to the landed diff at close:
150
- `changes[]` count, acceptance count, creates-vs-refactors mix, sensitive-path
151
- classes. A footprint intersecting a sensitive-path class derives `full` —
152
- sensitivity wins, and the Story keeps its fresh acceptance critic.
153
-
154
- That derived route sets ceremony. It does **not** set the dispatch mode,
142
+ **Ceremony comes from the landed diff; the dispatch mode comes from the
143
+ run.** Persist stamps a lite cohort's Stories with the `route::lite` label as a
144
+ _human-visible hint only_ (and ledgers the authored verdict — recorded reason
145
+ plus per-Story shape evidence — on the `story-plan-state` checkpoint); the
146
+ label is never the control signal. Ceremony is resolved from the **derived
147
+ change level** (`deriveChangeLevel` over the computed change set digest § 3),
148
+ not from a body-shape read: a footprint intersecting a sensitive-path class
149
+ derives `high`, so the Story keeps its fresh acceptance critic. The light path
150
+ is the one caller that reads the authored body's shape, through
151
+ `deriveStoryShape` (`lib/orchestration/complexity-gate.js`).
152
+
153
+ That derived level sets ceremony. It does **not** set the dispatch mode,
155
154
  because `inline` names one indivisible resource — the router's own session —
156
155
  and only run topology can say whether it is free: a **single-Story run**
157
156
  executes inline, and every Story of a multi-Story run dispatches as a
@@ -200,8 +199,8 @@ critic (the redundant pre-pass buys no measurable quality and roughly
200
199
  triples the acceptance-block cost). `acceptance-eval.js` is the
201
200
  deterministic **scorer** of that one authored verdict — schema validation,
202
201
  round cap, proceed / redraft / block — not an independent additional pass
203
- over the criteria. The M4-B floor holds: one verdict per cluster, the
204
- cluster count owned by `acceptance-clusters.js` alone.
202
+ over the criteria. The M4-B floor holds: one verdict per cluster, with the
203
+ cluster count owned by the dispatching caller and never by routing.
205
204
 
206
205
  **One round = N cluster critics → ONE merged verdict → ONE gate call.** The
207
206
  clusters are how a round is _authored_; they are not how it is _scored_.
@@ -279,7 +278,9 @@ floor forces `fresh`). Review depth reads the same derived level via
279
278
  `review-depth.js` inside close, so the two decisions cannot disagree.
280
279
 
281
280
  **Inline-dispatch override.** When the Story dispatches
282
- `inline` (`resolveStoryDispatchMode` → `inline`, i.e. a single-Story run), run
281
+ `inline` (`resolveStoryDispatchMode` → `inline`, which is exactly a
282
+ single-Story run — the function reads the resolved set size and nothing
283
+ else), run
283
284
  every acceptance critic **inline** — do not spawn fresh-context critic
284
285
  sub-agents regardless of what the profile would otherwise resolve. The self-eval rigor
285
286
  (scoring each `acceptance[]` item against the one computed change set, with
@@ -393,6 +394,16 @@ judgment that help text cannot carry.
393
394
  wants the PR left at `agent::closing` for a human land (or a wrapper that
394
395
  will invoke `single-story-confirm-merge.js` itself). Reports `pending` —
395
396
  the work is not done, nothing is broken, and one named command finishes it.
397
+ - `--override-review-block "<reason>"` — when the Story-scope review's
398
+ **critical** blocker is one you have read and judged wrong (a false positive,
399
+ or a finding the ratchet correctly exempts). It is the only sanctioned way
400
+ past that halt: reach for it instead of merging the PR by hand, because a
401
+ hand-merge bypasses the gate and records nothing. The reason is mandatory and
402
+ is written to three places (Story comment, PR comment, a
403
+ `review-block-overridden` friction signal), and the terminal envelope reports
404
+ `gates.codeReview: "overridden"` rather than `"passed"`. If you find yourself
405
+ reaching for it twice for the same shape of finding, the gate is
406
+ miscalibrated — fix the gate, not the run.
396
407
  - `--max-wait-seconds <n>` — from a headless caller with no host
397
408
  tool-invocation ceiling, to keep single-block semantics
398
409
  without editing the consumer's config.