mandrel 2.31.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 (250) 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/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -8,10 +8,10 @@
8
8
  * runner only — set SKIP_PREFLIGHT=0 to include preflight if desired.
9
9
  */
10
10
 
11
- import { spawnSync } from 'node:child_process';
12
11
  import fs from 'node:fs';
13
12
  import path from 'node:path';
14
13
  import { fileURLToPath } from 'node:url';
14
+ import { spawnChild } from './lib/child-exec.js';
15
15
  import { runAsCli } from './lib/cli-utils.js';
16
16
  import { buildWebhookSafeTestEnv } from './lib/test-env.js';
17
17
  import { parseTapOutput } from './lib/test-profile/parse-tap.js';
@@ -56,13 +56,14 @@ export function parseProfileArgv(argv) {
56
56
  * @param {object} [opts]
57
57
  * @param {string[]} [opts.argv]
58
58
  * @param {string} [opts.cwd]
59
- * @param {typeof spawnSync} [opts.spawn]
59
+ * @param {Function} [opts.spawn] - Injected child runner; defaults to the
60
+ * shared surface's real `spawnSync`.
60
61
  * @param {typeof fs} [opts.fs]
61
62
  */
62
63
  export function runTestProfile({
63
64
  argv = process.argv.slice(2),
64
65
  cwd = ROOT,
65
- spawn = spawnSync,
66
+ spawn,
66
67
  fs: fsLike = fs,
67
68
  } = {}) {
68
69
  const { outDir, topN, testArgv } = parseProfileArgv(argv);
@@ -75,7 +76,8 @@ export function runTestProfile({
75
76
  ];
76
77
 
77
78
  const started = Date.now();
78
- const result = spawn(process.execPath, nodeArgs, {
79
+ const result = spawnChild(process.execPath, nodeArgs, {
80
+ run: spawn,
79
81
  cwd,
80
82
  encoding: 'utf8',
81
83
  env: {
@@ -84,8 +86,6 @@ export function runTestProfile({
84
86
  FORCE_COLOR: '0',
85
87
  NO_COLOR: '1',
86
88
  },
87
- maxBuffer: 64 * 1024 * 1024,
88
- shell: false,
89
89
  });
90
90
  const wallDurationMs = Date.now() - started;
91
91
 
@@ -6,8 +6,8 @@
6
6
  * locally, without epic-scoped MI projection or push semantics.
7
7
  *
8
8
  * Order: audit (SCA) → lint (includes docs:check + the arch-cycles ratchet) →
9
- * full test suite → unified baselines → the dead-exports and context-budget
10
- * ratchets.
9
+ * full test suite → unified baselines → the standalone ratchets
10
+ * (dead-exports ×2, context-budget, cyclomatic, schema-references).
11
11
  *
12
12
  * The `audit` step runs `npm audit --audit-level=high`, matching CI's
13
13
  * "Dependency Vulnerability Audit (SCA)" gate so a local green no longer hides
@@ -27,42 +27,60 @@
27
27
  * full-tree scan in check-dead-exports.js) to a command that already carries
28
28
  * the full test suite.
29
29
  *
30
- * A handful of CI gates cannot be reproduced by this command (action pinning,
31
- * TruffleHog secret scan, the BASELINE_SCOPE=full push-scoped maintainability
32
- * run) those are catalogued in docs/ci-contract.md.
30
+ * Story #5004 closed the last two mirror gaps that were pure omission.
31
+ * `check-cyclomatic.js` (#4923) and `check-schema-references.js` (#4938) were
32
+ * each added to the `baselines` job's standalone-ratchet slot without ever
33
+ * being added here, so a green `verify` still hid both. Like their neighbours
34
+ * they are pure-Node and cost milliseconds.
35
+ *
36
+ * Still NOT mirrored: `check-workflow-citations.js` and
37
+ * `check-baseline-scope.js` run in CI's `baselines` job only —
38
+ * `.agents/rules/known-tooling-behavior.md` entry 2 carries the current
39
+ * coverage table. (`prune-baseline-orphans.js --check` used to sit in that
40
+ * list; it no longer runs in CI at all — the un-attributed duplicate of the
41
+ * scope gate's `extra` direction reds every open PR on inherited rows.) Nor are the CI gates this command structurally cannot
42
+ * reproduce (action pinning, TruffleHog secret scan) — those are catalogued
43
+ * in docs/ci-contract.md. The nightly full-scope re-score
44
+ * (.github/workflows/baseline-drift.yml) is deliberately outside this
45
+ * mirror too: it re-scores the whole tree, which is the cost `verify` exists
46
+ * to avoid paying on every run.
33
47
  */
34
48
 
35
49
  import { spawnSync } from 'node:child_process';
36
50
  import { runAsCli } from './lib/cli-utils.js';
37
51
 
52
+ /**
53
+ * A gate step: `node .agents/scripts/<script>` plus any extra args. Seven of
54
+ * the ten steps share exactly that shape, so spelling it once leaves the list
55
+ * below readable as what it actually is — a gate *order* — instead of a wall
56
+ * of spawn tuples.
57
+ *
58
+ * @param {string} label reported as `failedStep` when the gate exits non-zero
59
+ * @param {string} script basename under `.agents/scripts/`
60
+ * @param {...string} args extra CLI args
61
+ * @returns {{ label: string, cmd: string, args: string[] }}
62
+ */
63
+ const gate = (label, script, ...args) => ({
64
+ label,
65
+ cmd: 'node',
66
+ args: [`.agents/scripts/${script}`, ...args],
67
+ });
68
+
38
69
  const STEPS = [
39
- {
40
- label: 'audit',
41
- cmd: 'npm',
42
- args: ['audit', '--audit-level=high'],
43
- },
70
+ { label: 'audit', cmd: 'npm', args: ['audit', '--audit-level=high'] },
44
71
  { label: 'lint', cmd: 'npm', args: ['run', 'lint'] },
45
72
  { label: 'test', cmd: 'npm', args: ['test'] },
46
- {
47
- label: 'baselines',
48
- cmd: 'node',
49
- args: ['.agents/scripts/check-baselines.js'],
50
- },
51
- {
52
- label: 'dead-exports',
53
- cmd: 'node',
54
- args: ['.agents/scripts/check-dead-exports.js'],
55
- },
56
- {
57
- label: 'dead-exports-production',
58
- cmd: 'node',
59
- args: ['.agents/scripts/check-dead-exports.js', '--production'],
60
- },
61
- {
62
- label: 'context-budget',
63
- cmd: 'node',
64
- args: ['.agents/scripts/check-context-budget.js'],
65
- },
73
+ gate('baselines', 'check-baselines.js'),
74
+ // Ordered ahead of the dead-exports pair deliberately. When a new CLI is
75
+ // missing from knip.json's entry list, both gates fail — but only this one
76
+ // names the cause. Seeing the ratchet's whole-file diff first is what made
77
+ // "accept the diff" look like the fix during Story #5012.
78
+ gate('knip-entries', 'check-knip-entries.js'),
79
+ gate('dead-exports', 'check-dead-exports.js'),
80
+ gate('dead-exports-production', 'check-dead-exports.js', '--production'),
81
+ gate('context-budget', 'check-context-budget.js'),
82
+ gate('cyclomatic', 'check-cyclomatic.js'),
83
+ gate('schema-references', 'check-schema-references.js'),
66
84
  ];
67
85
 
68
86
  export function runVerifySteps({
@@ -44,6 +44,18 @@
44
44
  * [--no-auto-merge]
45
45
  * [--wait-merge | --no-wait-merge]
46
46
  * [--merge-watch-mode <sync|async>]
47
+ * [--override-review-block <reason>]
48
+ *
49
+ * `--override-review-block <reason>` is the one sanctioned way
50
+ * past a code-review CRITICAL blocker. A critical finding halts this script
51
+ * before auto-merge, and until this flag existed there was no override at all —
52
+ * so an operator who had read a finding and judged it wrong could only land by
53
+ * running `gh pr merge` themselves, bypassing the gate with nothing written
54
+ * down. The flag does not weaken the gate; it moves that escape hatch into a
55
+ * mandatory-reason audit trail (Story comment + PR comment + a
56
+ * `review-block-overridden` friction signal) and reports
57
+ * `gates.codeReview: "overridden"` on the terminal envelope. A bare or
58
+ * too-short reason fails during option parsing, before any phase runs.
47
59
  *
48
60
  * `--merge-watch-mode` (Story #4949) overrides `delivery.mergeWatch.mode` for
49
61
  * one invocation, on the same explicit-wins-over-config precedence
@@ -217,6 +229,14 @@ runAsCli(import.meta.url, main, {
217
229
  '--merge-watch-mode <sync|async>',
218
230
  'Override delivery.mergeWatch.mode for this invocation only. `async` caps the merge wait to a short probe window and returns the resumable `pending` terminal instead of holding the foreground slot — pass it on every close of a multi-Story run. An invalid value exits non-zero before any phase runs.',
219
231
  ],
232
+ [
233
+ '--override-review-block <reason>',
234
+ // Deliberately does not spell the merge CLI invocation: the
235
+ // merge-lockout rule in `check-lifecycle-lint.js` forbids that literal
236
+ // in any string outside `phases/auto-merge.js`, and it is right to —
237
+ // the point of this flag is that arming stays on the one code path.
238
+ 'Land despite a Story-scope code-review CRITICAL blocker you have reviewed and judged wrong. The reason is mandatory (≥12 chars) and is recorded on the Story, on the PR, and as a `review-block-overridden` friction signal; the terminal envelope reports `gates.codeReview: "overridden"`. Use this instead of merging the PR by hand with the GitHub CLI — a hand-merge bypasses the gate with no record at all.',
239
+ ],
220
240
  ['--no-evidence', 'Do not reuse or write gate evidence stamps.'],
221
241
  ['--dry-run', 'Report the plan; mutate nothing.'],
222
242
  ],
@@ -54,7 +54,6 @@ import {
54
54
  import { getStoryBranch, gitSpawn, gitSync } from './lib/git-utils.js';
55
55
  import { Logger } from './lib/Logger.js';
56
56
  import { TYPE_LABELS } from './lib/label-constants.js';
57
- import { setActiveStoryEnv } from './lib/observability/active-story-env.js';
58
57
  import { emitTerseResult } from './lib/observability/terse-result.js';
59
58
  import {
60
59
  executeFastForward,
@@ -492,10 +491,9 @@ export function seedStoryBranch({ cwd, storyBranch, baseBranch, progress }) {
492
491
  }
493
492
 
494
493
  /**
495
- * Provision a worktree (or check out the branch in single-tree mode), then
496
- * record the active-story environment markers. Returns the resolved
497
- * `workCwd`, `worktreeCreated`, and `installStatus`. Exported for testing
498
- * (owns the worktree/single-tree routing + setActiveStoryEnv call).
494
+ * Provision a worktree (or check out the branch in single-tree mode).
495
+ * Returns the resolved `workCwd`, `worktreeCreated`, and `installStatus`.
496
+ * Exported for testing (owns the worktree/single-tree routing).
499
497
  *
500
498
  * @param {object} opts
501
499
  * @param {object} opts.runtime
@@ -541,23 +539,6 @@ export async function provisionWorktree({
541
539
  gitSync(cwd, 'checkout', storyBranch);
542
540
  }
543
541
 
544
- try {
545
- // v2 Stories are standalone — no parent Epic. The helper omits
546
- // CC_EPIC_ID from env + file; the trace hook keys its standalone-trace
547
- // branch on CC_EPIC_ID being absent.
548
- setActiveStoryEnv({
549
- storyId,
550
- workCwd,
551
- logger: {
552
- warn: (m) => progress('ENV', `⚠️ ${m}`),
553
- },
554
- });
555
- } catch (err) {
556
- Logger.error(
557
- `[single-story-init] ⚠️ Failed to set active-Story env: ${err?.message ?? err}`,
558
- );
559
- }
560
-
561
542
  return { workCwd, worktreeCreated, installStatus };
562
543
  }
563
544
 
@@ -572,13 +553,11 @@ export async function runSingleStoryInit({
572
553
  injectedConfig,
573
554
  injectedSweep,
574
555
  // Story #3483: lets tests drive the lease preflight deterministically.
575
- // `injectedAcquireLease` swaps the guard; `leaseNow` injects the clock the
576
- // fail-closed liveness check evaluates against (audit #3513). `steal`
577
- // forcibly transfers a foreign claim — the standalone path has no Epic
578
- // heartbeat ledger, so a foreign assignee blocks unless stolen.
556
+ // `injectedAcquireLease` swaps the guard. `steal` forcibly transfers a
557
+ // foreign claim the lease fails closed (audit #3513), so a foreign
558
+ // assignee blocks unless stolen.
579
559
  injectedAcquireLease,
580
560
  steal = false,
581
- leaseNow,
582
561
  injectedVerifyRemote,
583
562
  // Story #4620: swap the git-touching provisioning steps so the
584
563
  // early-flip-then-rollback ordering is unit-testable without a real worktree.
@@ -652,13 +631,12 @@ export async function runSingleStoryInit({
652
631
  `Standalone Story: "${story.title}" → branch ${storyBranch} from ${baseBranch}.`,
653
632
  );
654
633
 
655
- // Story #3483 — lease preflight. Take an exclusive, time-bounded claim on
656
- // the Story ticket before any git mutation so two concurrent standalone
657
- // runs cannot both drive the same Story. The standalone path has no Epic
658
- // heartbeat ledger, so the guard fails closed (audit #3513): a foreign
659
- // assignee is treated as a live claim and aborts init (naming the current
660
- // owner) unless --steal forcibly transfers it. Unclaimed / self-held claims
661
- // proceed. Skipped under --dry-run (no assignee mutation).
634
+ // Story #3483 — lease preflight. Take an exclusive claim on the Story
635
+ // ticket before any git mutation so two concurrent standalone runs cannot
636
+ // both drive the same Story. The guard fails closed (audit #3513): a
637
+ // foreign assignee aborts init (naming the current owner) unless --steal
638
+ // forcibly transfers it. Unclaimed / self-held claims proceed. Skipped
639
+ // under --dry-run (no assignee mutation).
662
640
  let workCwd = cwd;
663
641
  let worktreeCreated = false;
664
642
  let installStatus = { status: 'skipped', reason: 'dry-run' };
@@ -670,7 +648,6 @@ export async function runSingleStoryInit({
670
648
  storyId,
671
649
  config,
672
650
  steal: stealRequested,
673
- now: leaseNow,
674
651
  });
675
652
  progress(
676
653
  'LEASE',
@@ -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-05T00:59:17.768Z",
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",