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
@@ -0,0 +1,469 @@
1
+ /**
2
+ * knip-entry-sync.js — derive which top-level `.agents/scripts/*.js` CLIs are
3
+ * actually invoked, and diff that set against `knip.json`'s `entry` array.
4
+ *
5
+ * Why this exists (Story #5001 → the #5012 near-miss):
6
+ *
7
+ * #5001 replaced the blanket `.agents/scripts/*.js!` entry glob with an
8
+ * explicit list, precisely so that a top-level CLI **nothing invokes** stops
9
+ * being declared live by the glob and surfaces as dead. It simultaneously
10
+ * promoted knip's `files` rule to `error`, so whole-file death now produces
11
+ * `{ file, symbol: '*' }` baseline rows.
12
+ *
13
+ * That pairing has a silent failure mode. A CLI added *after* #5001 is not in
14
+ * the hand-written list, so knip reads it as unreachable and emits a whole-file
15
+ * row for it — plus transitive rows for every lib module only that CLI imports.
16
+ * During #5012's delivery this produced 5 false rows (`check-baseline-scope.js`,
17
+ * `prune-baseline-orphans.js`, and their three `lib/baselines/*` modules). The
18
+ * natural remedy — accept the ratchet's diff — would have permanently recorded
19
+ * live, working operator CLIs as expected-dead, blinding the ratchet to their
20
+ * real death later. It was caught only because a base-sync conflict forced a
21
+ * manual read of the diff.
22
+ *
23
+ * The fix is to stop depending on a human remembering to edit `knip.json`.
24
+ * #5001's own wording already names the derivation rule — entries are derived
25
+ * "from what package.json scripts, husky hooks, .github/workflows, and the
26
+ * .agents workflow/skill markdown actually invoke". This module mechanizes
27
+ * exactly that rule and asserts it in both directions:
28
+ *
29
+ * missing — invoked by a real caller but absent from `entry`. This is the
30
+ * #5012 bug: knip will call it dead and the ratchet will offer to record
31
+ * the lie.
32
+ * stale — listed in `entry` but invoked by nothing. This is the blind spot
33
+ * #5001 closed: a declared entry point is immortal, so its death can never
34
+ * surface.
35
+ *
36
+ * Deliberately NOT a filesystem-derived list. Restoring `.agents/scripts/*.js!`
37
+ * — or regenerating the array from `readdirSync` — would make every file live
38
+ * by construction and undo #5001's AC-2. Liveness here means *invoked*, not
39
+ * *present*.
40
+ *
41
+ * Deliberately NOT conferred by documentation prose. `.agents/docs/**` and
42
+ * `docs/**` describe operator-run commands; a paragraph naming a CLI is not a
43
+ * caller. Three CLIs are named only in prose today
44
+ * (`check-baseline-drift.js`, `post-structured-comment.js`,
45
+ * `provision-git-hooks.js`) and #5001 recorded all of them as whole-file dead
46
+ * rows on purpose. Counting prose would silently resurrect them.
47
+ *
48
+ * A CLI that is only *imported* by another script (`cleanup-repo-test-temp.js`,
49
+ * imported by `run-tests.js` and `run-coverage.js`) is correctly neither: it is
50
+ * reachable through the module graph from a real entry, so knip already sees it
51
+ * and it must not be declared an entry point of its own.
52
+ */
53
+
54
+ import fs from 'node:fs';
55
+ import path from 'node:path';
56
+
57
+ // Only `resolveEntrySync` and `renderEntrySyncReport` are public — they are
58
+ // what `check-knip-entries.js` calls. Everything else is module-private and
59
+ // reached by the suite through the `__testing` bundle at the foot of this file,
60
+ // the same seam `source-classifier.js` and five sibling modules use. Exporting
61
+ // the internals individually would put seven test-only symbols on the
62
+ // production dead-export baseline and make private helpers look like API.
63
+
64
+ /** Top-level CLI directory, repo-relative. */
65
+ const SCRIPTS_DIR = path.join('.agents', 'scripts');
66
+
67
+ /**
68
+ * Executable surfaces — a mention of a CLI here is an instruction to run it.
69
+ *
70
+ * `dirs` entries are walked recursively and filtered by `test`. `files` are
71
+ * read directly. Documentation trees are absent by design (see module header).
72
+ *
73
+ * @type {ReadonlyArray<{ kind: 'file' | 'dir', at: string, test?: RegExp }>}
74
+ */
75
+ const INVOCATION_SURFACES = Object.freeze([
76
+ { kind: 'file', at: 'package.json' },
77
+ { kind: 'dir', at: '.husky', test: /^(?!.*[/\\]_[/\\]).*$/ },
78
+ { kind: 'dir', at: path.join('.github', 'workflows'), test: /\.ya?ml$/ },
79
+ { kind: 'dir', at: path.join('.agents', 'workflows'), test: /\.md$/ },
80
+ { kind: 'dir', at: path.join('.agents', 'skills'), test: /\.md$/ },
81
+ { kind: 'dir', at: path.join('.agents', 'agents'), test: /\.md$/ },
82
+ { kind: 'dir', at: path.join('.agents', 'rules'), test: /\.md$/ },
83
+ { kind: 'dir', at: SCRIPTS_DIR, test: /\.js$/ },
84
+ ]);
85
+
86
+ /**
87
+ * Strip line and block comments from JavaScript source, preserving string and
88
+ * template literals so a `'https://…'` or a `` `${x}//y` `` is not mangled.
89
+ *
90
+ * Comments are stripped before scanning `.js` surfaces so a JSDoc paragraph
91
+ * that merely *names* a CLI ("superseded by `node .agents/scripts/foo.js`")
92
+ * cannot confer liveness on it. Several such mentions exist today; every one
93
+ * of them is prose about a script's internals, not a call.
94
+ *
95
+ * Replaces comment bodies with equivalent whitespace rather than deleting
96
+ * them, so byte offsets and line numbers survive for any future caller that
97
+ * wants to report a position.
98
+ *
99
+ * @param {string} source
100
+ * @returns {string}
101
+ */
102
+ function stripJsComments(source) {
103
+ const text = String(source ?? '');
104
+ let out = '';
105
+ let i = 0;
106
+ const blank = (s) => s.replace(/[^\n]/g, ' ');
107
+ while (i < text.length) {
108
+ const two = text.slice(i, i + 2);
109
+ if (two === '//') {
110
+ const end = text.indexOf('\n', i);
111
+ const stop = end === -1 ? text.length : end;
112
+ out += blank(text.slice(i, stop));
113
+ i = stop;
114
+ } else if (two === '/*') {
115
+ const end = text.indexOf('*/', i + 2);
116
+ const stop = end === -1 ? text.length : end + 2;
117
+ out += blank(text.slice(i, stop));
118
+ i = stop;
119
+ } else {
120
+ const ch = text[i];
121
+ if (ch === "'" || ch === '"' || ch === '`') {
122
+ let j = i + 1;
123
+ while (j < text.length) {
124
+ if (text[j] === '\\') {
125
+ j += 2;
126
+ continue;
127
+ }
128
+ if (text[j] === ch) break;
129
+ j += 1;
130
+ }
131
+ const stop = Math.min(j + 1, text.length);
132
+ out += text.slice(i, stop);
133
+ i = stop;
134
+ } else {
135
+ out += ch;
136
+ i += 1;
137
+ }
138
+ }
139
+ }
140
+ return out;
141
+ }
142
+
143
+ /**
144
+ * Recursively list files under `dir`, skipping `node_modules` and `.git`.
145
+ *
146
+ * @param {string} dir absolute path
147
+ * @param {typeof fs} fsImpl
148
+ * @param {string[]} [acc]
149
+ * @returns {string[]} absolute file paths
150
+ */
151
+ function walk(dir, fsImpl, acc = []) {
152
+ let entries;
153
+ try {
154
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
155
+ } catch {
156
+ return acc;
157
+ }
158
+ for (const entry of entries) {
159
+ const full = path.join(dir, entry.name);
160
+ if (entry.isDirectory()) {
161
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
162
+ walk(full, fsImpl, acc);
163
+ } else {
164
+ acc.push(full);
165
+ }
166
+ }
167
+ return acc;
168
+ }
169
+
170
+ /**
171
+ * Read every executable surface under `repoRoot`.
172
+ *
173
+ * `.js` surfaces arrive comment-stripped; every other surface arrives verbatim.
174
+ * Test files are excluded: a test importing or naming a CLI does not make it
175
+ * invoked, which is the same test-only-importer discount `--production` applies
176
+ * to the dead-exports ratchet itself.
177
+ *
178
+ * @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
179
+ * @returns {Array<{ path: string, text: string }>} repo-relative path + content
180
+ */
181
+ function collectInvocationSurfaces({ repoRoot, fsImpl = fs }) {
182
+ const surfaces = [];
183
+ const push = (absolute) => {
184
+ const rel = path.relative(repoRoot, absolute);
185
+ if (rel.includes('__tests__') || /\.test\.[cm]?js$/.test(rel)) return;
186
+ let text;
187
+ try {
188
+ text = fsImpl.readFileSync(absolute, 'utf8');
189
+ } catch {
190
+ return;
191
+ }
192
+ surfaces.push({
193
+ path: rel.split(path.sep).join('/'),
194
+ text: rel.endsWith('.js') ? stripJsComments(text) : text,
195
+ });
196
+ };
197
+
198
+ for (const surface of INVOCATION_SURFACES) {
199
+ const absolute = path.join(repoRoot, surface.at);
200
+ if (surface.kind === 'file') {
201
+ push(absolute);
202
+ continue;
203
+ }
204
+ for (const file of walk(absolute, fsImpl)) {
205
+ const rel = path.relative(repoRoot, file);
206
+ if (surface.test && !surface.test.test(rel)) continue;
207
+ push(file);
208
+ }
209
+ }
210
+ return surfaces;
211
+ }
212
+
213
+ /**
214
+ * Build the two recognizers for one CLI basename.
215
+ *
216
+ * `pathLiteral` — the CLI named by its repo path (`.agents/scripts/foo.js`),
217
+ * in either separator style. This is how package.json scripts, husky hooks,
218
+ * workflow `run:` blocks and workflow markdown spell an invocation.
219
+ *
220
+ * `joinedSpawn` — the CLI named by bare basename as the tail of a
221
+ * `path.join(…, 'scripts', 'foo.js')` construction, which is how
222
+ * `lib/bootstrap/project-bootstrap.js` spawns `check-windows-git-perf.js`
223
+ * into a consumer checkout. Requiring the `'scripts'` sibling argument is
224
+ * what separates a real spawn from the several *data* inventories that list
225
+ * the same basenames as plain array elements (`source-classifier.js`'s
226
+ * `FRAMEWORK_SCRIPT_BASENAMES`, `check-lifecycle-lint.js`'s allowlist) —
227
+ * those are catalogues of names, not calls.
228
+ *
229
+ * @param {string} cli basename, e.g. `check-dead-exports.js`
230
+ * @returns {{ pathLiteral: RegExp, joinedSpawn: RegExp }}
231
+ */
232
+ function buildInvocationPatterns(cli) {
233
+ const escaped = cli.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
234
+ return {
235
+ pathLiteral: new RegExp(`\\.agents[/\\\\]scripts[/\\\\]${escaped}`),
236
+ joinedSpawn: new RegExp(
237
+ `['"\`]scripts['"\`]\\s*,\\s*['"\`]${escaped}['"\`]`,
238
+ ),
239
+ };
240
+ }
241
+
242
+ /**
243
+ * List the top-level CLI basenames actually present in `.agents/scripts/`.
244
+ *
245
+ * @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
246
+ * @returns {string[]} sorted basenames
247
+ */
248
+ function listTopLevelClis({ repoRoot, fsImpl = fs }) {
249
+ let entries;
250
+ try {
251
+ entries = fsImpl.readdirSync(path.join(repoRoot, SCRIPTS_DIR), {
252
+ withFileTypes: true,
253
+ });
254
+ } catch {
255
+ return [];
256
+ }
257
+ return entries
258
+ .filter((e) => e.isFile() && e.name.endsWith('.js'))
259
+ .map((e) => e.name)
260
+ .sort();
261
+ }
262
+
263
+ /**
264
+ * Read the top-level `.agents/scripts/*.js` basenames declared in `knip.json`'s
265
+ * `entry` array. Glob-bearing patterns are ignored — this reads the explicit
266
+ * enumeration only, which is the surface #5001 made authoritative.
267
+ *
268
+ * The trailing `!` is knip's production-mode marker and is load-bearing, not
269
+ * decoration: in a `--production` run knip keeps only the suffixed entries and
270
+ * *negates* the rest (`WorkspaceWorker`'s `hasProductionSuffix` /
271
+ * `hasNoProductionSuffix` split). So an entry written without it leaves the CLI
272
+ * unreachable in exactly the pass that emits whole-file rows — the #5012 shape
273
+ * this gate exists to catch. Reading it as satisfied would point the operator
274
+ * away from the cause, so unsuffixed entries are reported as their own
275
+ * divergence rather than silently normalized.
276
+ *
277
+ * @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
278
+ * @returns {{ entries: string[], unsuffixed: string[], error: string | null }}
279
+ */
280
+ function readKnipEntries({ repoRoot, fsImpl = fs }) {
281
+ const configPath = path.join(repoRoot, 'knip.json');
282
+ let parsed;
283
+ try {
284
+ parsed = JSON.parse(fsImpl.readFileSync(configPath, 'utf8'));
285
+ } catch (error) {
286
+ return {
287
+ entries: [],
288
+ unsuffixed: [],
289
+ error: `cannot read knip.json: ${error.message}`,
290
+ };
291
+ }
292
+ if (!Array.isArray(parsed?.entry)) {
293
+ return {
294
+ entries: [],
295
+ unsuffixed: [],
296
+ error: 'knip.json has no "entry" array',
297
+ };
298
+ }
299
+ const prefix = '.agents/scripts/';
300
+ const declared = parsed.entry
301
+ .filter((e) => typeof e === 'string' && e.startsWith(prefix))
302
+ .filter((e) => !e.includes('*'))
303
+ .map((e) => ({
304
+ cli: e.slice(prefix.length).replace(/!$/, ''),
305
+ suffixed: e.endsWith('!'),
306
+ }))
307
+ .filter((e) => !e.cli.includes('/'));
308
+
309
+ const entries = declared.map((e) => e.cli);
310
+ const unsuffixed = declared.filter((e) => !e.suffixed).map((e) => e.cli);
311
+ return {
312
+ entries: [...new Set(entries)].sort(),
313
+ unsuffixed: [...new Set(unsuffixed)].sort(),
314
+ error: null,
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Resolve the full entry-sync report for a repository.
320
+ *
321
+ * @param {{ repoRoot: string, fsImpl?: typeof fs }} opts
322
+ * @returns {{
323
+ * error: string | null,
324
+ * clis: string[],
325
+ * declared: string[],
326
+ * missing: Array<{ cli: string, invokers: string[] }>,
327
+ * stale: string[],
328
+ * phantom: string[],
329
+ * unsuffixed: string[],
330
+ * }}
331
+ * `missing` — invoked but not declared (the #5012 bug).
332
+ * `stale` — declared but invoked by nothing.
333
+ * `phantom` — declared but no such file on disk (a rename left the list behind).
334
+ * `unsuffixed` — declared without the `!` production marker, which knip's
335
+ * production pass negates rather than honours (the #5012 bug wearing a
336
+ * declared entry).
337
+ */
338
+ export function resolveEntrySync({ repoRoot, fsImpl = fs }) {
339
+ const {
340
+ entries: declared,
341
+ unsuffixed,
342
+ error,
343
+ } = readKnipEntries({ repoRoot, fsImpl });
344
+ const empty = {
345
+ clis: [],
346
+ declared,
347
+ missing: [],
348
+ stale: [],
349
+ phantom: [],
350
+ unsuffixed: [],
351
+ };
352
+ if (error) return { error, ...empty };
353
+
354
+ const clis = listTopLevelClis({ repoRoot, fsImpl });
355
+ if (clis.length === 0) {
356
+ return { error: `no CLIs found under ${SCRIPTS_DIR}`, ...empty };
357
+ }
358
+ const surfaces = collectInvocationSurfaces({ repoRoot, fsImpl });
359
+ const declaredSet = new Set(declared);
360
+
361
+ const missing = [];
362
+ const stale = [];
363
+ for (const cli of clis) {
364
+ const self = `${SCRIPTS_DIR.split(path.sep).join('/')}/${cli}`;
365
+ const { pathLiteral, joinedSpawn } = buildInvocationPatterns(cli);
366
+ const invokers = surfaces
367
+ .filter(
368
+ (s) =>
369
+ s.path !== self &&
370
+ (pathLiteral.test(s.text) ||
371
+ (s.path.endsWith('.js') && joinedSpawn.test(s.text))),
372
+ )
373
+ .map((s) => s.path);
374
+ const isDeclared = declaredSet.has(cli);
375
+ if (invokers.length > 0 && !isDeclared) missing.push({ cli, invokers });
376
+ if (invokers.length === 0 && isDeclared) stale.push(cli);
377
+ }
378
+
379
+ const onDisk = new Set(clis);
380
+ const phantom = declared.filter((cli) => !onDisk.has(cli));
381
+
382
+ return { error: null, clis, declared, missing, stale, phantom, unsuffixed };
383
+ }
384
+
385
+ /**
386
+ * Total divergences across every direction. Single source of the gate's
387
+ * pass/fail arithmetic so a newly added direction cannot be counted in the
388
+ * report but missed by the exit code.
389
+ *
390
+ * @param {ReturnType<typeof resolveEntrySync>} report
391
+ * @returns {number}
392
+ */
393
+ export function countDivergences(report) {
394
+ return (
395
+ report.missing.length +
396
+ report.stale.length +
397
+ report.phantom.length +
398
+ report.unsuffixed.length
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Render the human-readable report. Each divergence names its own remedy —
404
+ * the whole point of the gate is that the *wrong* remedy (accepting the
405
+ * dead-exports ratchet's diff) is the tempting one.
406
+ *
407
+ * @param {ReturnType<typeof resolveEntrySync>} report
408
+ * @returns {string}
409
+ */
410
+ export function renderEntrySyncReport(report) {
411
+ const lines = [];
412
+ for (const { cli, invokers } of report.missing) {
413
+ lines.push(
414
+ `+ ${cli} — invoked by ${invokers.slice(0, 3).join(', ')}${
415
+ invokers.length > 3 ? ` (+${invokers.length - 3} more)` : ''
416
+ }`,
417
+ );
418
+ lines.push(
419
+ ` add ".agents/scripts/${cli}!" to knip.json "entry" — until you do, knip`,
420
+ );
421
+ lines.push(
422
+ ' reads it as unreachable and check-dead-exports will offer a false whole-file row.',
423
+ );
424
+ }
425
+ for (const cli of report.stale) {
426
+ lines.push(
427
+ `- ${cli} — declared in knip.json "entry" but nothing invokes it`,
428
+ );
429
+ lines.push(
430
+ ' remove the entry so its death can surface, or wire up the caller.',
431
+ );
432
+ }
433
+ for (const cli of report.phantom) {
434
+ lines.push(`? ${cli} — declared in knip.json "entry" but not on disk`);
435
+ lines.push(' drop the stale path (the file was renamed or removed).');
436
+ }
437
+ for (const cli of report.unsuffixed) {
438
+ lines.push(`! ${cli} — declared in knip.json "entry" without a "!" suffix`);
439
+ lines.push(
440
+ ` write it as ".agents/scripts/${cli}!" — the production pass negates an`,
441
+ );
442
+ lines.push(
443
+ ' unsuffixed entry, so knip still reads the CLI as unreachable.',
444
+ );
445
+ }
446
+ const total = countDivergences(report);
447
+ lines.push(
448
+ `[knip-entries] clis=${report.clis.length} declared=${report.declared.length} ` +
449
+ `missing=${report.missing.length} stale=${report.stale.length} ` +
450
+ `phantom=${report.phantom.length} unsuffixed=${report.unsuffixed.length} ` +
451
+ `${total > 0 ? '(gate fail)' : '(ok)'}`,
452
+ );
453
+ return lines.join('\n');
454
+ }
455
+
456
+ /**
457
+ * Test-only seam. Not API: these helpers are exercised directly by
458
+ * `tests/check-knip-entries.test.js` so the recognizers can be pinned
459
+ * independently of a full fixture tree.
460
+ */
461
+ export const __testing = Object.freeze({
462
+ buildInvocationPatterns,
463
+ collectInvocationSurfaces,
464
+ INVOCATION_SURFACES,
465
+ listTopLevelClis,
466
+ readKnipEntries,
467
+ SCRIPTS_DIR,
468
+ stripJsComments,
469
+ });
@@ -74,7 +74,7 @@ export const MAX_LEDGER_BYTES = 1024 * 1024;
74
74
 
75
75
  /**
76
76
  * Resolve the ledger path for an Epic (or the standalone stream when
77
- * `epicId` is `null` — the `story-plan.js` / Epic-less healthcheck case).
77
+ * `epicId` is `null` — the v2 Epic-less plan/healthcheck case).
78
78
  *
79
79
  * @param {number|null} epicId
80
80
  * @param {object} [config] Resolved config (threads `project.paths.tempRoot`).
@@ -83,6 +83,16 @@ export const RUNTIME_FRICTION_CATEGORIES = Object.freeze({
83
83
  * ceilings recalibratable from recorded evidence.
84
84
  */
85
85
  LIGHT_SCOPE_REJECTED: 'light-scope-rejected',
86
+ /**
87
+ * An operator overrode a Story-scope code-review critical blocker with
88
+ * `--override-review-block <reason>`. Deliberately its own
89
+ * bucket: this is the one signal that says a gate's verdict was rejected by
90
+ * a human who then shipped anyway, so a rising `occurrences` count is direct
91
+ * evidence the gate is miscalibrated rather than the code being bad. It is
92
+ * *not* `TOOL_DEGRADED` — the tool ran fine and produced a verdict; what
93
+ * failed was the verdict's usefulness.
94
+ */
95
+ REVIEW_BLOCK_OVERRIDDEN: 'review-block-overridden',
86
96
  });
87
97
 
88
98
  /** Cap on free-form reason text copied into a signal's `details`. */
@@ -17,22 +17,20 @@
17
17
  * document (no hand-rolled drift). `strict: false` matches the repo's
18
18
  * other draft-07 validators (see `tests/schemas/signal-schemas.test.js`).
19
19
  *
20
- * A per-Epic reject tally is persisted under the Epic temp tree
21
- * (`temp/run-<eid>/signal-rejects.json`) so a cross-process reader (the
22
- * loop-health check, a follow-on Story) can surface how many records were
23
- * dropped. The tally is read-modify-written best-effort; a lost increment
24
- * under a write race is acceptable for a diagnostic counter.
20
+ * Story #5003 deleted the persisted per-Epic reject tally
21
+ * (`temp/run-<eid>/signal-rejects.json`) and its `readSignalRejectCount`
22
+ * reader: the tally was keyed on a positive `epicId`, and v2 Stories are
23
+ * standalone, so nothing ever wrote a row and nothing could ever read one.
24
+ * Write-time validation itself is untouched.
25
25
  */
26
26
 
27
27
  import { readFileSync } from 'node:fs';
28
- import fs from 'node:fs/promises';
29
28
  import path from 'node:path';
30
29
  import { fileURLToPath } from 'node:url';
31
30
 
32
31
  import Ajv from 'ajv';
33
32
  import addFormats from 'ajv-formats';
34
33
 
35
- import { runArtifactPath } from '../config/temp-paths.js';
36
34
  import { Logger } from '../Logger.js';
37
35
 
38
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -45,8 +43,6 @@ const SCHEMA_PATH = path.resolve(
45
43
  'signal-event.schema.json',
46
44
  );
47
45
 
48
- const REJECT_TALLY_BASENAME = 'signal-rejects.json';
49
-
50
46
  /**
51
47
  * Compile the signal-event schema once. Returns `null` when the schema
52
48
  * cannot be read or compiled — the caller then treats every record as
@@ -126,79 +122,3 @@ export function validateSignal(record) {
126
122
  const message = _validate.errors?.[0]?.message ?? 'schema validation failed';
127
123
  return { valid: false, violatingField: field, message };
128
124
  }
129
-
130
- /**
131
- * Increment the per-Epic reject tally persisted under the Epic temp tree.
132
- * Best-effort and never throws: a missing/corrupt tally file resets to a
133
- * count of 1. When `epicId` is not a positive integer (e.g. a standalone
134
- * friction record with `epicId: null`) the tally write is skipped — there
135
- * is no Epic temp tree to anchor it to.
136
- *
137
- * @param {{ epicId: number|null|undefined, config?: object, field?: string|null }} args
138
- * @returns {Promise<number|null>} the new count, or null when skipped/failed.
139
- */
140
- export async function recordSignalReject({ epicId, config, field }) {
141
- if (!Number.isInteger(epicId) || epicId <= 0) return null;
142
- let target;
143
- try {
144
- target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
145
- } catch {
146
- return null;
147
- }
148
- try {
149
- let prior = { count: 0, lastField: null };
150
- try {
151
- const raw = await fs.readFile(target, 'utf8');
152
- const parsed = JSON.parse(raw);
153
- if (
154
- parsed &&
155
- typeof parsed === 'object' &&
156
- Number.isFinite(parsed.count)
157
- ) {
158
- prior = { count: parsed.count, lastField: parsed.lastField ?? null };
159
- }
160
- } catch {
161
- // Missing or corrupt file → start a fresh tally.
162
- }
163
- const next = {
164
- count: prior.count + 1,
165
- lastField: field ?? prior.lastField ?? null,
166
- updatedAt: new Date().toISOString(),
167
- };
168
- await fs.mkdir(path.dirname(target), { recursive: true });
169
- await fs.writeFile(target, `${JSON.stringify(next)}\n`, 'utf8');
170
- return next.count;
171
- } catch (err) {
172
- Logger.warn(
173
- `signal-validator: failed to persist reject tally for epic-${epicId}: ${
174
- err instanceof Error ? err.message : String(err)
175
- }`,
176
- );
177
- return null;
178
- }
179
- }
180
-
181
- /**
182
- * Read the current per-Epic reject count. Cross-process readable
183
- * counterpart to {@link recordSignalReject}. Returns 0 when the tally
184
- * file is absent or unreadable.
185
- *
186
- * @param {{ epicId: number, config?: object }} args
187
- * @returns {Promise<number>}
188
- */
189
- export async function readSignalRejectCount({ epicId, config }) {
190
- if (!Number.isInteger(epicId) || epicId <= 0) return 0;
191
- let target;
192
- try {
193
- target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
194
- } catch {
195
- return 0;
196
- }
197
- try {
198
- const raw = await fs.readFile(target, 'utf8');
199
- const parsed = JSON.parse(raw);
200
- return parsed && Number.isFinite(parsed.count) ? parsed.count : 0;
201
- } catch {
202
- return 0;
203
- }
204
- }