mandrel 2.58.0 → 2.60.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 (124) hide show
  1. package/.agents/README.md +17 -12
  2. package/.agents/agents/acceptance-critic.md +24 -43
  3. package/.agents/agents/story-worker.md +18 -19
  4. package/.agents/docs/SDLC.md +12 -13
  5. package/.agents/docs/agentrc-reference.json +1 -2
  6. package/.agents/docs/configuration.md +29 -46
  7. package/.agents/docs/quality-gates.md +9 -5
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/instructions.md +5 -7
  10. package/.agents/rules/ci-remediation.md +41 -8
  11. package/.agents/rules/known-tooling-behavior.md +65 -15
  12. package/.agents/runtime-deps.json +7 -2
  13. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  14. package/.agents/schemas/agentrc.schema.json +6 -11
  15. package/.agents/schemas/crap-baseline.schema.json +1 -1
  16. package/.agents/schemas/crap-report.schema.json +1 -1
  17. package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
  18. package/.agents/scripts/README.md +11 -1
  19. package/.agents/scripts/acceptance-eval.js +25 -27
  20. package/.agents/scripts/ceremony-derive.js +15 -10
  21. package/.agents/scripts/check-context-budget.js +148 -228
  22. package/.agents/scripts/check-schema-references.js +5 -3
  23. package/.agents/scripts/check-workflow-citations.js +33 -147
  24. package/.agents/scripts/coverage-capture.js +7 -4
  25. package/.agents/scripts/deliver-light.js +41 -100
  26. package/.agents/scripts/deliver-run.js +631 -0
  27. package/.agents/scripts/file-ci-gap.js +59 -11
  28. package/.agents/scripts/install-matrix-assert.js +48 -3
  29. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
  30. package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
  31. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
  32. package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
  33. package/.agents/scripts/lib/changed-files.js +30 -0
  34. package/.agents/scripts/lib/config/delivery-routing.js +5 -4
  35. package/.agents/scripts/lib/config/explain.js +1 -3
  36. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
  37. package/.agents/scripts/lib/config-resolver.js +1 -0
  38. package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
  39. package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
  40. package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
  41. package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
  42. package/.agents/scripts/lib/crap-engine.js +2 -2
  43. package/.agents/scripts/lib/crap-utils.js +21 -5
  44. package/.agents/scripts/lib/doc-tiers.js +4 -2
  45. package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
  46. package/.agents/scripts/lib/escomplex-kernel.js +298 -0
  47. package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
  48. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
  49. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  50. package/.agents/scripts/lib/gh-exec.js +160 -0
  51. package/.agents/scripts/lib/maintainability-engine.js +3 -3
  52. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  53. package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
  54. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
  55. package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
  56. package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
  57. package/.agents/scripts/lib/orchestration/plan-context.js +44 -50
  58. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
  59. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
  60. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
  61. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
  62. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
  63. package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
  64. package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
  65. package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
  66. package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
  67. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
  68. package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
  69. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
  70. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
  71. package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
  72. package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
  73. package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
  74. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
  75. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
  76. package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
  77. package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
  78. package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
  79. package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
  80. package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
  81. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
  82. package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
  83. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  84. package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
  85. package/.agents/scripts/lib/story-body/story-body.js +83 -29
  86. package/.agents/scripts/lib/templates/decomposer-prompts.js +28 -33
  87. package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
  88. package/.agents/scripts/merge-baseline.js +4 -5
  89. package/.agents/scripts/plan-context.js +117 -28
  90. package/.agents/scripts/plan-persist.js +79 -39
  91. package/.agents/scripts/plan-run-epilogue.js +11 -8
  92. package/.agents/scripts/pr-watch-with-update.js +9 -2
  93. package/.agents/scripts/run-verify.js +13 -6
  94. package/.agents/scripts/single-story-init.js +7 -57
  95. package/.agents/scripts/stories-wave-tick.js +160 -26
  96. package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
  97. package/.agents/skills/skills.index.json +2 -12
  98. package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
  99. package/.agents/workflows/audit-to-stories.md +14 -11
  100. package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
  101. package/.agents/workflows/helpers/code-review.md +4 -2
  102. package/.agents/workflows/helpers/deliver-digest.md +31 -24
  103. package/.agents/workflows/helpers/deliver-light.md +92 -101
  104. package/.agents/workflows/helpers/deliver-reference.md +116 -100
  105. package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
  106. package/.agents/workflows/helpers/deliver-story.md +17 -18
  107. package/.agents/workflows/helpers/plan-reference.md +82 -60
  108. package/.agents/workflows/mandrel-deliver.md +47 -31
  109. package/.agents/workflows/mandrel-plan.md +32 -30
  110. package/.agents/workflows/mandrel-update.md +36 -21
  111. package/README.md +3 -3
  112. package/docs/CHANGELOG.md +43 -0
  113. package/lib/cli/registry.js +45 -25
  114. package/lib/cli/update.js +376 -17
  115. package/lib/migrations/index.js +2 -0
  116. package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
  117. package/package.json +8 -2
  118. package/.agents/schemas/model-attribution.schema.json +0 -53
  119. package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
  120. package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
  121. package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
  122. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
  123. package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
  124. package/.agents/skills/core/scope-triage/SKILL.md +0 -48
@@ -35,7 +35,11 @@ import {
35
35
  INTAKE_VERDICTS,
36
36
  REFUSED_VERDICT,
37
37
  } from './lib/orchestration/ci-gap-intake.js';
38
- import { readCiDigest } from './lib/orchestration/ci-rerun-guard.js';
38
+ import {
39
+ RERUN_ALLOWANCE_VERDICTS,
40
+ readCiDigest,
41
+ recordRerunAllowance,
42
+ } from './lib/orchestration/ci-rerun-guard.js';
39
43
  import {
40
44
  STATE_LABELS,
41
45
  transitionTicketState,
@@ -55,7 +59,7 @@ const USAGE = {
55
59
  ],
56
60
  [
57
61
  '--verdict <verdict>',
58
- `One of ${INTAKE_VERDICTS.join(' | ')}. "${REFUSED_VERDICT}" is refused: it routes to Option 1, fix at source.`,
62
+ `One of ${INTAKE_VERDICTS.join(' | ')}. "${REFUSED_VERDICT}" is refused: it routes to Option 1, fix at source. ${RERUN_ALLOWANCE_VERDICTS.join(' / ')} also record the one-rerun allowance on the CI digest.`,
59
63
  ],
60
64
  [
61
65
  '--owner <bucket>',
@@ -138,7 +142,12 @@ function liveIntakePorts({ provider, searchRepo, cwd, logger }) {
138
142
  * @param {object} opts
139
143
  * @returns {string}
140
144
  */
141
- export function renderFrictionComment({ verdict, result, digest }) {
145
+ export function renderFrictionComment({
146
+ verdict,
147
+ result,
148
+ digest,
149
+ rerunAllowance = null,
150
+ }) {
142
151
  const target = result.issue?.url ?? result.issue?.number ?? '(not filed)';
143
152
  const lines = [
144
153
  `### CI gap filed — verdict \`${verdict}\``,
@@ -158,14 +167,31 @@ export function renderFrictionComment({ verdict, result, digest }) {
158
167
  `- **Routing:** deferred from \`${result.routing.deferredFrom}\` (${result.routing.deferralReason}) — filed locally instead.`,
159
168
  );
160
169
  }
161
- lines.push(
162
- '',
163
- 'This verdict does **not** license a re-run of the failed job. Graduate the',
164
- 'intake issue with `/mandrel-plan <issue number>` to turn it into a Story.',
165
- );
170
+ lines.push('', renderRerunLine(rerunAllowance), GRADUATE_LINE);
166
171
  return lines.join('\n');
167
172
  }
168
173
 
174
+ /** The graduation instruction every filing carries, verdict-independent. */
175
+ const GRADUATE_LINE =
176
+ 'Graduate the intake issue with `/mandrel-plan <issue number>` to turn it into a Story.';
177
+
178
+ /**
179
+ * What this filing says about rerunning the failed job (Story #5343). An
180
+ * allowance is stated with its head SHA and its one-shot bound, because the
181
+ * comment is where an operator reads whether the rerun they are about to do
182
+ * is the sanctioned one.
183
+ *
184
+ * @param {{ headSha: string } | null} rerunAllowance
185
+ * @returns {string}
186
+ */
187
+ function renderRerunLine(rerunAllowance) {
188
+ return rerunAllowance
189
+ ? 'This verdict is environmental and proven, so **one** rerun of the failed ' +
190
+ `job is now admitted on head \`${rerunAllowance.headSha}\` — and one only. ` +
191
+ 'A second red after it is real and routes to Option 1.'
192
+ : 'This verdict does **not** license a re-run of the failed job.';
193
+ }
194
+
169
195
  /**
170
196
  * File the CI-gap intake issue for one Story, post the `friction` comment,
171
197
  * and optionally flip the Story to `agent::blocked`.
@@ -198,7 +224,12 @@ export async function runFileCiGap({
198
224
  throw new Error('--story <id> is required (a positive issue number).');
199
225
  }
200
226
  const resolved = config ?? resolveConfig();
201
- const ciDigest = digest ?? readCiDigest({ storyId: sid, tempRoot, cwd });
227
+ // The digest's temp root, resolved exactly as `pr-watch-with-update.js`
228
+ // resolves it when it WRITES the digest — the CLI never passed one, so the
229
+ // default read crashed on `undefined` before it could find the file.
230
+ const digestRoot = tempRoot ?? resolved?.project?.paths?.tempRoot ?? 'temp';
231
+ const ciDigest =
232
+ digest ?? readCiDigest({ storyId: sid, tempRoot: digestRoot, cwd });
202
233
  if (!ciDigest) {
203
234
  throw new Error(
204
235
  `no CI digest for Story #${sid}. The digest is written by \`pr-watch-with-update.js --story ${sid}\` on the first red; without it there is no run link or failure signature to file.`,
@@ -237,9 +268,26 @@ export async function runFileCiGap({
237
268
  now,
238
269
  });
239
270
 
271
+ // Story #5343 — a proven-environmental verdict earns the one same-SHA
272
+ // rerun the watcher's guard will admit. Recorded on the digest, keyed to
273
+ // the head SHA the red was observed on, and never on a dry run.
274
+ const rerunAllowance = dryRun
275
+ ? null
276
+ : recordRerunAllowance({
277
+ storyId: sid,
278
+ verdict,
279
+ tempRoot: digestRoot,
280
+ cwd,
281
+ });
282
+
240
283
  const actions = { commented: false, blocked: false };
241
284
  if (!dryRun && ticketing) {
242
- const body = renderFrictionComment({ verdict, result, digest: ciDigest });
285
+ const body = renderFrictionComment({
286
+ verdict,
287
+ result,
288
+ digest: ciDigest,
289
+ rerunAllowance,
290
+ });
243
291
  try {
244
292
  await upsertStructuredComment(ticketing, sid, 'friction', body);
245
293
  actions.commented = true;
@@ -258,7 +306,7 @@ export async function runFileCiGap({
258
306
  }
259
307
  }
260
308
 
261
- return { storyId: sid, verdict, ...result, actions };
309
+ return { storyId: sid, verdict, ...result, actions, rerunAllowance };
262
310
  }
263
311
 
264
312
  /**
@@ -59,17 +59,62 @@ import path from 'node:path';
59
59
  * `.agents/runtime-deps.json` and provided by the consumer's install of
60
60
  * `mandrel` (npm hoists them into node_modules), but they MUST NOT
61
61
  * appear in the consumer's *declared* package.json dependencies. Kept in sync
62
- * with `.agents/runtime-deps.json` `dependencies` keys.
62
+ * with `.agents/runtime-deps.json` `dependencies` keys — the whole set, so
63
+ * this list documents the closure; {@link SHAREABLE_RUNTIME_DEPS} carries the
64
+ * policy about which of them a consumer may also declare.
63
65
  */
64
66
  const FRAMEWORK_RUNTIME_DEPS = [
67
+ '@babel/parser',
65
68
  'ajv',
66
69
  'ajv-formats',
70
+ 'babel-runtime',
71
+ 'escomplex-plugin-metrics-module',
72
+ 'escomplex-plugin-syntax-babylon',
67
73
  'js-yaml',
68
74
  'minimatch',
69
75
  'picomatch',
70
- 'typhonjs-escomplex',
76
+ 'typhonjs-ast-walker',
77
+ 'typhonjs-escomplex-commons',
71
78
  ];
72
79
 
80
+ /**
81
+ * Runtime dependencies the framework declares but does **not** claim
82
+ * exclusively, so a consumer declaring one is not evidence of a mutated
83
+ * manifest.
84
+ *
85
+ * This check exists to catch a consumer's `package.json` being written into
86
+ * with framework-internal packages. That inference only holds for packages
87
+ * nobody else would plausibly declare. `@babel/parser` and `babel-runtime`
88
+ * fail that test completely: a repository with its own Babel pipeline, AST
89
+ * tooling or legacy transpile output declares them for its own reasons, and
90
+ * failing `manifest-clean` for that would be a false positive on an ordinary
91
+ * consumer rather than a caught mutation.
92
+ *
93
+ * Kept as an explicit list rather than a heuristic: adding a widely-used
94
+ * package to the framework's runtime closure should be a deliberate decision
95
+ * to stop policing it, recorded here.
96
+ */
97
+ const SHAREABLE_RUNTIME_DEPS = ['@babel/parser', 'babel-runtime'];
98
+
99
+ /**
100
+ * Is this framework dependency's presence in a consumer manifest evidence of a
101
+ * mutated manifest?
102
+ *
103
+ * Only for packages the framework claims exclusively. A shareable one is
104
+ * declared by ordinary repositories for their own reasons.
105
+ *
106
+ * @param {string} dep
107
+ * @param {Record<string, string>} declared Consumer's merged declared deps.
108
+ * @returns {boolean}
109
+ */
110
+ function isLeak(dep, declared) {
111
+ if (!(dep in declared)) return false;
112
+ return !SHAREABLE_RUNTIME_DEPS.includes(dep);
113
+ }
114
+
115
+ /** Exported for the manifest-mirror drift assertion. */
116
+ export { FRAMEWORK_RUNTIME_DEPS, SHAREABLE_RUNTIME_DEPS };
117
+
73
118
  /** The verdict marker `mandrel doctor` prints when every check passes. */
74
119
  const DOCTOR_READY_MARKER = '✅ Ready';
75
120
 
@@ -157,7 +202,7 @@ export function checkManifestClean({ consumer, packageName, fs = nodeFs }) {
157
202
  ...(manifest.peerDependencies ?? {}),
158
203
  };
159
204
 
160
- const leaked = FRAMEWORK_RUNTIME_DEPS.filter((dep) => dep in declared);
205
+ const leaked = FRAMEWORK_RUNTIME_DEPS.filter((dep) => isLeak(dep, declared));
161
206
  if (leaked.length > 0) {
162
207
  return {
163
208
  ok: false,
@@ -9,7 +9,7 @@
9
9
  * - Problem Statement (aggregated severity profile)
10
10
  * - Recommended Direction (rollup of recommendations by dimension)
11
11
  * - Key Assumptions (carries the source-report links forward)
12
- * - MVP Scope (the proposed Stories, one bullet per group)
12
+ * - MVP Scope (the findings themselves, flat — Story #5332)
13
13
  * - Key Files (explicit file paths so `/mandrel-plan` authoring has concrete
14
14
  * anchors)
15
15
  * - Not Doing (out-of-scope items by convention)
@@ -19,7 +19,6 @@
19
19
 
20
20
  import { SEVERITIES } from '../findings/severity.js';
21
21
  import { auditLabelFooterForFindings } from './audit-label-taxonomy.js';
22
- import { formatEpicGrouping } from './epic-grouping-directive.js';
23
22
  import {
24
23
  renderFingerprintFooter,
25
24
  renderSemanticKeyFooter,
@@ -93,35 +92,56 @@ function formatRecommendedDirection(findings) {
93
92
  return lines.join('\n');
94
93
  }
95
94
 
96
- function formatMVPScope(groups) {
95
+ /**
96
+ * The findings, flat (Story #5332).
97
+ *
98
+ * This section used to render one numbered bullet per `groupFindings` group
99
+ * under a `## Grouping` directive — a partition the seed had already decided
100
+ * before the planner read a word of it, and at a grain (`groupFindings`'s) the
101
+ * planner's cohesion judgment never got to review. The measured result was a
102
+ * sweep of 44 findings arriving as 18 Stories. The seed now states what was
103
+ * found and lets N reach the planner undecided; container grouping is Gate
104
+ * #3's call at persist, where N is known.
105
+ *
106
+ * @param {object[]} findings
107
+ * @returns {string}
108
+ */
109
+ function formatFindingsList(findings) {
110
+ return findings
111
+ .map((f) => {
112
+ const label = DIMENSION_LABEL[f.dimension] ?? f.dimension;
113
+ const file = f.files?.[0] ? ` (\`${f.files[0]}\`)` : '';
114
+ const severity = f.severity ? `${f.severity} · ` : '';
115
+ return `- **${f.title}** — ${severity}${label}${file}`;
116
+ })
117
+ .join('\n');
118
+ }
119
+
120
+ /**
121
+ * The machine-readable dedup identity, one footer set per group.
122
+ *
123
+ * Deliberately **not** folded into one footer over the whole sweep, and
124
+ * deliberately not attached to a visible bullet. Each group's fingerprint and
125
+ * location-based semantic-key footers are the identity the next sweep matches
126
+ * on (Story #4626), and the `audit::*` labels are the reason it ever looks at
127
+ * the issue at all — an indexed sweep answers exact lookups from the labelled
128
+ * pool without reaching the provider, so a Story missing the labels is
129
+ * invisible however good its fingerprints (Story #5307). They are HTML
130
+ * comments, so they carry no partition to the planner's eye while staying
131
+ * byte-identical to what the standalone-Stories path emits.
132
+ *
133
+ * @param {object[]} groups
134
+ * @returns {string}
135
+ */
136
+ function formatDedupFooters(groups) {
97
137
  return groups
98
- .map((g, idx) => {
99
- const dims = g.dimensions.join(' / ');
100
- const file = g.files[0] ? ` (\`${g.files[0]}\`)` : '';
101
- // Carry each group's fingerprint (and location-based semantic-key)
102
- // footer into the seed so a Story authored from it via `/mandrel-plan` inherits
103
- // the dedup identity — without this the recommended `/mandrel-plan --seed-file`
104
- // path is invisible to the next sweep's dedup (Story #4626). The footers
105
- // are HTML comments, so they never render in the visible one-pager but
106
- // stay machine-readable for the dedup probe.
138
+ .map((g) => {
107
139
  const findings = Array.isArray(g.findings) ? g.findings : [];
108
- const footers = [
140
+ return [
109
141
  renderFingerprintFooter(findings),
110
142
  renderSemanticKeyFooter(findings),
111
- // ...and the `audit::*` labels the dedup corpus is listed by.
112
- //
113
- // Without them a Story the chained planning path files is absent from
114
- // the pool an indexed sweep matches against, and with an index in play
115
- // the exact lookup is answered from that pool without ever reaching
116
- // the provider — so the fingerprint footer above cannot rescue it. The
117
- // two footers are therefore a pair: one carries the identity, the
118
- // other carries the reason the next sweep ever looks at this issue
119
- // (Story #5307).
120
143
  auditLabelFooterForFindings(findings),
121
- ]
122
- .map((f) => ` ${f}`)
123
- .join('\n');
124
- return `${idx + 1}. **${g.title}** — ${dims}${file}\n${footers}`;
144
+ ].join('\n');
125
145
  })
126
146
  .join('\n');
127
147
  }
@@ -165,10 +185,10 @@ export function buildPlanSeedMarkdown({ groups, findings, sourceReports }) {
165
185
  }
166
186
  const problem = formatProblemStatement(findings);
167
187
  const direction = formatRecommendedDirection(findings);
168
- const scope = formatMVPScope(groups);
188
+ const scope = formatFindingsList(findings);
169
189
  const files = formatKeyFiles(groups);
170
190
  const assumptions = formatKeyAssumptions(sourceReports);
171
- const grouping = formatEpicGrouping(groups);
191
+ const dedupFooters = formatDedupFooters(groups);
172
192
 
173
193
  return [
174
194
  '# Idea Seed: Audit Remediation',
@@ -187,16 +207,14 @@ export function buildPlanSeedMarkdown({ groups, findings, sourceReports }) {
187
207
  '',
188
208
  '## MVP Scope',
189
209
  '',
190
- scope || '_(no proposed stories)_',
210
+ scope || '_(no findings)_',
211
+ '',
212
+ dedupFooters,
191
213
  '',
192
214
  '## Key Files',
193
215
  '',
194
216
  files,
195
217
  '',
196
- '## Grouping',
197
- '',
198
- grouping,
199
- '',
200
218
  '## Not Doing',
201
219
  '',
202
220
  '- Findings with severity below the operator-selected threshold.',
@@ -6,7 +6,7 @@
6
6
  * so the Story's opt-in wiring lands as new code, not a same-file expansion
7
7
  * of the pre-existing preview runner.
8
8
  */
9
- import { getChangedFiles } from '../changed-files.js';
9
+ import { getChangedFiles, resolveChangedFilesRef } from '../changed-files.js';
10
10
 
11
11
  /**
12
12
  * Resolve the `incremental` option `scanAndScore` (`crap-utils.js`) expects,
@@ -14,6 +14,10 @@ import { getChangedFiles } from '../changed-files.js';
14
14
  * not be resolved — a resolution failure falls back to full-scope rather
15
15
  * than silently relaxing the gate.
16
16
  *
17
+ * The join's ref comes from `resolveChangedFilesRef` (Story #5365), the same
18
+ * rule capture applies: the `--changed-since` ref the preview was handed wins
19
+ * over a configured `baseRef`, so one hook invocation cannot resolve two.
20
+ *
17
21
  * Gated by `incrementalCoverage.baselineJoin` alone (Story #5173). It MUST
18
22
  * NOT consult `skipWhenUnchanged`: the join loosens what the gate demands,
19
23
  * while the skip only decides whether a capture runs, so a consumer that took
@@ -36,7 +40,7 @@ export function resolveCrapPreviewIncremental({
36
40
  getChangedFilesImpl = getChangedFiles,
37
41
  }) {
38
42
  if (crap.incrementalCoverage?.baselineJoin !== true) return null;
39
- const baseRef = crap.incrementalCoverage.baseRef || diffRef || 'main';
43
+ const baseRef = resolveChangedFilesRef({ crap, ref: diffRef });
40
44
  try {
41
45
  const touchedFiles = new Set(getChangedFilesImpl({ ref: baseRef, cwd }));
42
46
  return { touchedFiles, baselineRows };
@@ -22,7 +22,6 @@
22
22
 
23
23
  import path from 'node:path';
24
24
  import { readBaselineAtRef } from '../../baseline-loader.js';
25
- import { resolveEscomplexVersion } from '../../crap-utils.js';
26
25
  import { loadBaseline } from '../../gates/baseline-store.js';
27
26
  import {
28
27
  loadFile as loadBaselineFile,
@@ -38,11 +37,6 @@ import {
38
37
  * epic-ref callers always know their own path); without one the reader
39
38
  * resolves the configured location for the `crap` kind itself.
40
39
  *
41
- * `escomplexVersion` is back-filled from the running scorer exactly as the
42
- * deleted projection stamped it — the v2 envelope does not carry the field, and
43
- * `escomplex-mismatch` is a fatal axis, so omitting it would fail every
44
- * baseline closed on a value that was never on disk.
45
- *
46
40
  * Returns `null` on any read/parse/schema failure; the preview gate maps that
47
41
  * to "no baseline" and fails open, as it always did.
48
42
  *
@@ -69,7 +63,6 @@ function readCrapBaselineFromTree({ baselinePath, projectRoot } = {}) {
69
63
  }
70
64
  return {
71
65
  kernelVersion: envelope.kernelVersion,
72
- escomplexVersion: resolveEscomplexVersion(),
73
66
  scoringSemantics: envelope.scoringSemantics ?? null,
74
67
  tsTranspilerVersion:
75
68
  typeof envelope.tsTranspilerVersion === 'string'
@@ -135,7 +128,6 @@ export function loadCrapBaseline({
135
128
  // No-epicRef path delegates to readFromTree which already applies the
136
129
  // shape-check + tsTranspilerVersion back-fill, so a tree read returns either
137
130
  // a valid envelope or null. Epic-ref path bypasses that helper — shape-check
138
- // + back-fill happens here.
139
131
  if (!epicRef) return parsed;
140
132
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
141
133
  return null;
@@ -53,11 +53,29 @@ export { loadCrapBaseline } from './_crap-read.js';
53
53
  const __filename = fileURLToPath(import.meta.url);
54
54
 
55
55
  /**
56
- * Resolve the running `typhonjs-escomplex` version by walking up from this
57
- * module's directory and reading the nearest
58
- * `node_modules/typhonjs-escomplex/package.json`. Returns `'0.0.0'` when
59
- * the dependency cannot be found — callers treat that sentinel as
60
- * "unknown environment" and the writer refuses to persist a baseline.
56
+ * Package whose resolved version is stamped as the CRAP scorer identity.
57
+ *
58
+ * `escomplex-plugin-metrics-module` owns the Halstead and cyclomatic math. The
59
+ * displaced `typhonjs-escomplex` shell contributed a parser and a plugin bus
60
+ * and never computed a metric, so stamping it described the shell.
61
+ */
62
+ const SCORER_PACKAGE = 'escomplex-plugin-metrics-module';
63
+
64
+ /**
65
+ * Resolve the running scorer's version by walking up from this module's
66
+ * directory and reading the nearest `node_modules/<SCORER_PACKAGE>/package.json`.
67
+ *
68
+ * The package read is `escomplex-plugin-metrics-module`, which computes the
69
+ * metrics. It was `typhonjs-escomplex` until Story #5336 replaced that shell's
70
+ * parse and dispatch layers; reading a package that no longer exists would
71
+ * yield the sentinel below on every fresh install, and the writer would refuse
72
+ * to persist a baseline. (Locally it can look fine anyway — the walk-up escapes
73
+ * a worktree into the parent checkout, whose `node_modules` may still hold the
74
+ * removed package. CI's fresh clone has no such parent.)
75
+ *
76
+ * Returns `'0.0.0'` when the dependency cannot be found — callers treat that
77
+ * sentinel as "unknown environment" and the writer refuses to persist a
78
+ * baseline.
61
79
  *
62
80
  * @returns {string}
63
81
  */
@@ -68,7 +86,7 @@ export function kernelVersion() {
68
86
  const pkgPath = path.join(
69
87
  dir,
70
88
  'node_modules',
71
- 'typhonjs-escomplex',
89
+ SCORER_PACKAGE,
72
90
  'package.json',
73
91
  );
74
92
  if (fs.existsSync(pkgPath)) {
@@ -695,7 +713,14 @@ export function assessComparisonBasis(compareResult, opts = {}) {
695
713
  * transpiler's sourcemap. `startLine` is half the row identity key, so a
696
714
  * transpiler change makes the rows incomparable rather than merely stale; see
697
715
  * the `ts-transpiler-drift` axis below for the two exemptions that bound it.
698
- * `escomplexVersion` mismatch has always failed closed.
716
+ *
717
+ * There is no `escomplexVersion` axis. One existed, declared `fatal`, and
718
+ * could not fire in either direction: the v2 envelope does not carry the
719
+ * field, so the loaded-envelope pass excluded it as vacuous, and the peer pass
720
+ * back-filled the value from the running scorer before comparing it against
721
+ * the running scorer. A gate that presents as fatal and cannot fail is worse
722
+ * than an absent one, so it was removed rather than repaired — `scoringSemantics`
723
+ * is the axis that actually rejects an incompatible scorer.
699
724
  */
700
725
  /**
701
726
  * The one re-seed recipe every coordinate-invalidating axis ends on. Three
@@ -713,14 +738,6 @@ export const CRAP_COMPAT_AXES = [
713
738
  // missing-baseline and kernel-drift checks live in exactly one place;
714
739
  // each per-kind table composes them in with its own kind label.
715
740
  missingBaselineAxis('CRAP'),
716
- {
717
- name: 'escomplex-mismatch',
718
- severity: 'fatal',
719
- check: ({ baseline, runningEscomplexVersion }) =>
720
- baseline && baseline.escomplexVersion !== runningEscomplexVersion
721
- ? `[CRAP] scorer changed from ${baseline.escomplexVersion} to ${runningEscomplexVersion} — run 'npm run crap:update'`
722
- : null,
723
- },
724
741
  kernelDriftAxis('CRAP'),
725
742
  {
726
743
  name: 'scoring-semantics-drift',
@@ -874,9 +891,9 @@ export function evaluateBaselineCompatibility(ctx) {
874
891
  * predating both questions. The fourth (Story #4969) is about the `method`
875
892
  * half — a baseline still carrying ordinal-keyed anonymous rows.
876
893
  *
877
- * `escomplex-mismatch` and `kernel-drift` stay out — the v2 envelope carries
878
- * no `escomplexVersion`, so that axis would compare `undefined` to `undefined`
879
- * and pass vacuously, which is worse than not running it.
894
+ * `kernel-drift` stays out: it is a warn-level axis, and this pass turns a
895
+ * message into a fail-closed error. (`escomplex-mismatch` was the other
896
+ * exclusion until it was removed outright — see `CRAP_COMPAT_AXES`.)
880
897
  */
881
898
  const LOADED_ENVELOPE_AXES = [
882
899
  'scoring-semantics-drift',
@@ -1,5 +1,35 @@
1
1
  import { createGitInterface } from './git-utils.js';
2
2
 
3
+ /**
4
+ * Resolve the ONE git ref a step of the pre-push chain computes its
5
+ * changed-file set against — the whole rule, stated once (Story #5365).
6
+ *
7
+ * **A ref the caller named wins; `crap.incrementalCoverage.baseRef` is the
8
+ * default for a caller that named none, and `main` the default for neither.**
9
+ * `.husky/pre-push` captures coverage at `--ref origin/main` and then previews
10
+ * CRAP at `--changed-since origin/main`, and the preview only reads its own
11
+ * tree when the artifact under it was captured over the same change set.
12
+ * Letting the configured value outrank the named ref meant a consumer that set
13
+ * `baseRef` captured one scope while the preview scored another — precisely
14
+ * the stale-artifact read the capture-before-preview ordering (Story #5356)
15
+ * closed. This repository sets no `baseRef`, so that divergence was invisible
16
+ * locally and only a consumer who configured one would have paid for it.
17
+ *
18
+ * Callers that name no ref — the close-validation gate, whose argv carries no
19
+ * `--ref` — still get the configured value, so the key keeps the meaning it
20
+ * was added with.
21
+ *
22
+ * Every step that derives that change set calls this: both `coverage-capture`
23
+ * paths and the preview's CRAP baseline join. A new consumer routes through it
24
+ * rather than reading `baseRef` itself.
25
+ *
26
+ * @param {{ crap: object, ref: string | null | undefined }} opts
27
+ * @returns {string}
28
+ */
29
+ export function resolveChangedFilesRef({ crap, ref }) {
30
+ return ref ?? crap?.incrementalCoverage?.baseRef ?? 'main';
31
+ }
32
+
3
33
  /**
4
34
  * Parse the stdout from `git diff --name-only` into a normalized file list.
5
35
  * Trims whitespace, drops blank lines, and converts backslash separators to
@@ -5,10 +5,11 @@
5
5
  * Stage 6 dropped `delivery.routing.singleDelivery` (the v1 epic
6
6
  * single-vs-fan-out kill-switch). Story #5313 dropped
7
7
  * `delivery.routing.freshCriticSampleRate` (the maker-checker sampling
8
- * floor): the standard profile now routes purely off the derived change
9
- * level — high or underivable → fresh critic, low → inline self-eval. v2 has
10
- * one Story delivery path; routing here is only about spawn boot context and
11
- * the ceremony profile.
8
+ * floor), and Story #5343 dropped the derived-level routing that replaced
9
+ * it: `minimal` and `standard` resolve the inline self-eval as the Story's
10
+ * verdict owner whatever the diff touches, and `strict` is the one profile
11
+ * that spawns a fresh-context critic. v2 has one Story delivery path;
12
+ * routing here is only about spawn boot context and the ceremony profile.
12
13
  *
13
14
  * `delivery.routing.roleScopedAgents` is the **kill-switch for the role-scoped
14
15
  * boot contexts** (Epic #4478, M7-B). It defaults to `true`: a converted spawn
@@ -159,11 +159,9 @@ const KEY_MEANINGS = Object.freeze({
159
159
  'delivery.routing.roleScopedAgents':
160
160
  'Whether delivery spawns boot on role-scoped .claude/agents/<role>.md contexts.',
161
161
  'delivery.routing.ceremonyProfile':
162
- 'Acceptance-ceremony depth: minimal (always inline), standard (derived-level routed), or strict (always fresh).',
162
+ 'Who authors the Story acceptance verdict: minimal and standard = the inline self-eval, strict = a fresh maker-blind critic.',
163
163
  'delivery.routing.closeAndLand':
164
164
  'When true, single-story-close lands through merge in one close (opt out with --no-wait-merge).',
165
- 'delivery.feedbackLoop.auditResultsAutoFile':
166
- 'When true, auto-file non-blocking audit findings as follow-up issues.',
167
165
  'delivery.feedbackLoop.retroProposals':
168
166
  'When true, auto-file actionable retro proposals as follow-up issues.',
169
167
  'delivery.quality.formatAutofix.timeoutMs':
@@ -53,7 +53,7 @@ export const INCREMENTAL_COVERAGE_SCHEMA = {
53
53
  type: 'string',
54
54
  minLength: 1,
55
55
  description:
56
- 'Git ref the changed-file set is computed against. Omitted falls back to the gate’s own `--ref` (`main`).',
56
+ 'Default git ref the changed-file set is computed against, for a caller that passes no `--ref`. A `--ref` the caller named wins over this value, so a caller that anchors another gate on the same ref — `.husky/pre-push`, which passes `--ref origin/main` and then previews `--changed-since origin/main` — resolves ONE scope for both steps (Story #5365). Omitted and unpassed, the gate’s own default `main` applies.',
57
57
  },
58
58
  },
59
59
  additionalProperties: false,
@@ -62,6 +62,7 @@ export { resolveListValue } from './config/shared.js';
62
62
  export { validateOrchestrationConfig } from './config/validate-orchestration.js';
63
63
  export {
64
64
  defaultNodeModulesStrategy,
65
+ getWorktreeIsolation,
65
66
  WORKTREE_ISOLATION_DEFAULTS,
66
67
  } from './config/worktree-isolation.js';
67
68
  export { PROJECT_ROOT } from './project-root.js';
@@ -258,12 +258,12 @@ const MERGE_WATCH_SCHEMA = {
258
258
  // context; false falls back to `subagent_type: general-purpose` (the instant
259
259
  // per-consumer revert + the escape for hosts that ignore `.claude/agents/`).
260
260
  // Story #5313 retired `delivery.routing.freshCriticSampleRate` (the
261
- // maker-checker sampling floor): the standard profile now routes purely off
262
- // the derived change level.
261
+ // maker-checker sampling floor). Story #5343 then retired the derived-level
262
+ // routing it left behind: the profile alone decides the verdict owner.
263
263
  const ROUTING_SCHEMA = {
264
264
  type: 'object',
265
265
  description:
266
- 'v2 delivery-spawn routing: role-scoped boot contexts and the ceremony profile. The v1 singleDelivery epic-route kill-switch was removed in Stage 6; the freshCriticSampleRate sampling floor was retired in Story #5313.',
266
+ 'v2 delivery-spawn routing: role-scoped boot contexts and the ceremony profile. The v1 singleDelivery epic-route kill-switch was removed in Stage 6; the freshCriticSampleRate sampling floor was retired in Story #5313 and the derived-level ceremony routing in Story #5343.',
267
267
  properties: {
268
268
  roleScopedAgents: {
269
269
  type: 'boolean',
@@ -275,7 +275,7 @@ const ROUTING_SCHEMA = {
275
275
  type: 'string',
276
276
  enum: ['minimal', 'standard', 'strict'],
277
277
  description:
278
- 'Acceptance-ceremony depth. minimal = always inline critic; strict = always fresh-context critic; standard (default) = routed off the change level derived from the Story diff: high or underivable → fresh, low → inline.',
278
+ 'Acceptance-ceremony depth — who authors the Story acceptance verdict. minimal and standard (default) = the inline self-eval, whatever the diff touches; strict = a fresh-context maker-blind critic. Review depth is a separate decision and still derives `deep` for any sensitive path (review-depth.js).',
279
279
  default: DELIVERY_ROUTING_DEFAULTS.ceremonyProfile,
280
280
  },
281
281
  closeAndLand: {
@@ -452,18 +452,31 @@ const REVIEW_SCHEMA = {
452
452
  };
453
453
 
454
454
  /**
455
- * `delivery.feedbackLoop` — opt-out toggles consumed by the Epic finalize
456
- * listener's auto-file graduators (`lib/feedback-loop/*-graduator.js`, read
457
- * via `graduator-core.js#makeIsAutoFileEnabled`). All default to `true`
458
- * (auto-file on); set any to `false` to suppress auto-filing the
459
- * corresponding non-blocking findings as follow-up issues.
455
+ * `delivery.feedbackLoop` — the **opt-in** toggle consumed by the retro
456
+ * auto-file graduator (`lib/feedback-loop/retro-proposals-graduator.js`, read
457
+ * via `graduator-core.js#makeIsAutoFileEnabled`), plus the friction window.
460
458
  *
461
- * `retroProposals` (Story #4418) governs the retro auto-filer: when true
462
- * (default) the retro's actionable routed proposals are filed as
459
+ * `auditResultsAutoFile` used to sit beside `retroProposals` here. Its
460
+ * graduator was deleted two releases ago, so by Story #5341 — which flipped
461
+ * its default from `true` to `false` on the measured record of what the
462
+ * channel produced — there was nothing left to switch either way. Story #5366
463
+ * removed the key: a toggle with no runtime reader reads as a live control,
464
+ * and a consumer that set it was configuring nothing. The
465
+ * `2.60.0-retire-audit-results-autofile` migration strips it from both config
466
+ * surfaces, because this block is closed to additional properties and a
467
+ * surviving key is a hard validation failure on upgrade.
468
+ *
469
+ * `retroProposals` (Story #4418) governs the retro auto-filer, and defaults to
470
+ * `false` for the same Story #5341 reasons: Story #5324's roll-up carried 116
471
+ * signals and filed nothing, and issues #4653, #4833, #4834 and #4836 are
472
+ * filings that were false or leaked from test fixtures. An auto-filer whose
473
+ * output is dominated by noise costs triage on every run and buys nothing, so
474
+ * a consumer that wants it now asks for it. When `true` the
475
+ * retro's actionable routed proposals are filed as
463
476
  * `meta::<framework-gap|consumer-improvement>` + `friction::<category>`
464
477
  * issues via the graduator pre-parsed-findings seam, and the rendered retro
465
478
  * sections list the filed issue numbers instead of paste-ready `gh` command
466
- * stanzas; set it to `false` to fall back to the command stanzas.
479
+ * stanzas; left `false` it renders the command stanzas.
467
480
  *
468
481
  * `frictionWindowDays` (Story #4850) bounds the run-scope friction recurrence
469
482
  * window by row age. The window deliberately spans every surviving signal
@@ -475,19 +488,13 @@ const REVIEW_SCHEMA = {
475
488
  const FEEDBACK_LOOP_SCHEMA = {
476
489
  type: 'object',
477
490
  description:
478
- 'Opt-out toggles for the close-time auto-file graduators. All default to auto-filing on.',
491
+ 'Opt-in toggle for the close-time retro auto-file graduator, plus the friction recurrence window. Auto-filing defaults to OFF (Story #5341).',
479
492
  properties: {
480
- auditResultsAutoFile: {
481
- type: 'boolean',
482
- description:
483
- 'When true (default), the close-time audit-results graduator auto-files non-blocking audit-results findings as follow-up issues routed by source classification. Set to false to suppress auto-filing; findings remain accessible in the structured comments on the Story.',
484
- default: true,
485
- },
486
493
  retroProposals: {
487
494
  type: 'boolean',
488
495
  description:
489
- 'When true (default), the retro auto-files its actionable routed proposals as meta::<framework-gap|consumer-improvement> + friction::<category> issues via the graduator pre-parsed-findings seam, and the rendered retro sections list the filed issue numbers instead of paste-ready gh command stanzas. Set to false to fall back to the command stanzas.',
490
- default: true,
496
+ 'When true, the retro auto-files its actionable routed proposals as meta::<framework-gap|consumer-improvement> + friction::<category> issues via the graduator pre-parsed-findings seam, and the rendered retro sections list the filed issue numbers instead of paste-ready gh command stanzas. Defaults to false (Story #5341), which renders the command stanzas instead.',
497
+ default: false,
491
498
  },
492
499
  frictionWindowDays: {
493
500
  type: 'integer',