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
@@ -2,7 +2,7 @@
2
2
  * bootstrap/quality-bootstrap — Story #1401 (Epic #1386)
3
3
  *
4
4
  * Idempotent installer for the stabilized-quality-gates surface area on a
5
- * project clone. Performs four additive actions, each safe to re-run:
5
+ * project clone. Performs five additive actions, each safe to re-run:
6
6
  *
7
7
  * 1. Copies the `code-quality-guardrails.md` helper into the project's
8
8
  * `.agents/workflows/helpers/` (no-op when the helper is already present).
@@ -15,6 +15,11 @@
15
15
  * 4. Seeds `delivery.quality.codingGuardrails` and
16
16
  * `delivery.quality.autoRefresh` defaults in `.agentrc.json` when
17
17
  * the keys are absent. Existing values are preserved.
18
+ * 5. Prunes a committed pre-v2 `baselines/epic/` tree (Story #5007). The
19
+ * v2 model is Story-only — nothing writes, reads, or reaps per-Epic
20
+ * ratchet snapshots — so an upgrading consumer is left carrying a
21
+ * committed directory no gate consults. Absent on every repo that never
22
+ * ran a pre-v2 install, which is the overwhelmingly common case.
18
23
  *
19
24
  * Returns a structured summary so the bootstrap and update workflows can
20
25
  * surface exactly which actions ran and which were no-ops.
@@ -25,6 +30,7 @@
25
30
  * @module bootstrap/quality-bootstrap
26
31
  */
27
32
 
33
+ import { spawnSync as defaultSpawnSync } from 'node:child_process';
28
34
  import fs from 'node:fs';
29
35
  import path from 'node:path';
30
36
  import { getAgentrcDefaults, lookupPath } from '../config/defaults.js';
@@ -349,7 +355,59 @@ export function ensureQualityConfigDefaults(ctx) {
349
355
  }
350
356
 
351
357
  /**
352
- * Run all four steps in order. Composable wrapper used by the bootstrap
358
+ * Repo-root-relative path of the retired committed per-Epic snapshot tree.
359
+ * Module-private: exporting it would add a production-dead row to the
360
+ * dead-exports ratchet for a two-word constant.
361
+ */
362
+ const LEGACY_EPIC_BASELINES_RELPATH = 'baselines/epic';
363
+
364
+ /**
365
+ * Step 5 — Prune a committed pre-v2 `baselines/epic/` tree (Story #5007).
366
+ *
367
+ * Story #1396 committed per-Epic ratchet snapshots under
368
+ * `baselines/epic/<id>/`. The v2 Story-only model retired epics along with
369
+ * every reader of those snapshots, and `check-baselines.js` resolves
370
+ * baselines by fixed filename rather than by directory glob — so the tree is
371
+ * inert, and the only remaining value in the retired layout migration was
372
+ * getting it out of version control.
373
+ *
374
+ * `git rm -r --ignore-unmatch` stages the removal when the path is tracked
375
+ * and is a safe no-op when it is not (fresh clone / untracked leftovers);
376
+ * the on-disk residue is then removed so a re-run reports `absent`. The
377
+ * caller commits the resulting working-tree delta.
378
+ *
379
+ * @param {object} ctx
380
+ * @param {string} ctx.projectRoot
381
+ * @param {typeof defaultSpawnSync} [ctx.spawnImpl] — injectable spawn seam.
382
+ * @returns {{ action: 'absent'|'pruned', path: string, gitStatus?: number|null }}
383
+ */
384
+ export function pruneLegacyEpicBaselines(ctx) {
385
+ const target = path.join(ctx.projectRoot, 'baselines', 'epic');
386
+ if (!fs.existsSync(target)) return { action: 'absent', path: target };
387
+ const spawn = ctx.spawnImpl ?? defaultSpawnSync;
388
+ const rm = spawn(
389
+ 'git',
390
+ [
391
+ 'rm',
392
+ '-r',
393
+ '--quiet',
394
+ '--ignore-unmatch',
395
+ '--',
396
+ LEGACY_EPIC_BASELINES_RELPATH,
397
+ ],
398
+ {
399
+ cwd: ctx.projectRoot,
400
+ encoding: 'utf-8',
401
+ stdio: 'pipe',
402
+ shell: false,
403
+ },
404
+ );
405
+ fs.rmSync(target, { recursive: true, force: true });
406
+ return { action: 'pruned', path: target, gitStatus: rm.status ?? null };
407
+ }
408
+
409
+ /**
410
+ * Run all five steps in order. Composable wrapper used by the bootstrap
353
411
  * and update workflows. Each step's outcome is returned under its own key
354
412
  * so callers can render a per-action summary.
355
413
  *
@@ -357,6 +415,7 @@ export function ensureQualityConfigDefaults(ctx) {
357
415
  * @param {string} ctx.projectRoot
358
416
  * @param {string} [ctx.frameworkRoot]
359
417
  * @param {'framework'|'downstream'} [ctx.variant]
418
+ * @param {typeof defaultSpawnSync} [ctx.spawnImpl]
360
419
  */
361
420
  export function applyQualityBootstrap(ctx) {
362
421
  return {
@@ -364,5 +423,6 @@ export function applyQualityBootstrap(ctx) {
364
423
  hook: ensurePreCommitHook(ctx),
365
424
  scripts: ensureQualityNpmScripts(ctx),
366
425
  config: ensureQualityConfigDefaults(ctx),
426
+ legacyBaselines: pruneLegacyEpicBaselines(ctx),
367
427
  };
368
428
  }
@@ -5,7 +5,7 @@
5
5
  * The original feedback-loop drift shipped silently because an empty
6
6
  * feedback report is indistinguishable from a healthy one. This standing
7
7
  * self-check closes that gap: at retro time it samples the on-disk signal
8
- * substrate the sibling Stories established and surfaces the three ways the
8
+ * substrate the sibling Stories established and surfaces the two ways the
9
9
  * repaired loop can regress without anyone noticing:
10
10
  *
11
11
  * 1. **Schema-invalid signal lines.** It tails the most recent
@@ -13,11 +13,7 @@
13
13
  * the run temp tree and validates each against the canonical
14
14
  * `signal-event.schema.json` (via `validateSignal`, the same validator
15
15
  * the writer uses — no hand-rolled drift).
16
- * 2. **Persisted write-time rejects.** It reads the per-run reject tally
17
- * (`temp/run-<id>/signal-rejects.json`, written by Story #4413's
18
- * signals-writer) so records that were dropped at write time — and thus
19
- * never appear in the stream — are still counted.
20
- * 3. **Un-actioned retro proposals.** It reads the retro mirror
16
+ * 2. **Un-actioned retro proposals.** It reads the retro mirror
21
17
  * (`temp/run-<id>/retro.md`, Story #4418) and flags any actionable
22
18
  * "Proposed issues" item that carries neither a filed-issue reference
23
19
  * (`Filed: [#N](url)`) nor lives under the explicit "One-off /
@@ -26,7 +22,7 @@
26
22
  * Contract:
27
23
  * - Scope `retro`, `autoCorrect: 'refuse-and-print'` — read-only by
28
24
  * construction; the runner refuses `autoFix` under the retro scope.
29
- * - A clean substrate (valid lines, zero rejects, every proposal filed or
25
+ * - A clean substrate (valid lines, every proposal filed or
30
26
  * discarded) yields **zero findings**, preserving the compact retro
31
27
  * shape. Only when a concern is non-zero does `detect` return a single
32
28
  * combined finding naming every non-clean dimension.
@@ -168,25 +164,6 @@ export function sampleStreamInvalidCount(
168
164
  return { sampled: tail.length, invalid };
169
165
  }
170
166
 
171
- /**
172
- * Read the per-run persisted reject count from `signal-rejects.json`.
173
- * Returns 0 when the tally is absent or unreadable.
174
- *
175
- * @param {string} epicDir
176
- * @param {{ readImpl?: typeof readFileSync }} [opts]
177
- * @returns {number}
178
- */
179
- export function readRejectTally(epicDir, { readImpl = readFileSync } = {}) {
180
- try {
181
- const parsed = JSON.parse(
182
- readImpl(path.join(epicDir, 'signal-rejects.json'), 'utf8'),
183
- );
184
- return parsed && Number.isFinite(parsed.count) ? parsed.count : 0;
185
- } catch {
186
- return 0;
187
- }
188
- }
189
-
190
167
  /**
191
168
  * Scan a retro mirror body for actionable proposals that were neither filed
192
169
  * nor discarded. An actionable proposal is a `- **Title**` item under a
@@ -207,7 +184,7 @@ export function scanRetroMirror(retroText) {
207
184
  let inSection = false;
208
185
  let current = null;
209
186
  const flush = () => {
210
- if (current && current.actionable && !current.filed) {
187
+ if (current?.actionable && !current.filed) {
211
188
  unfiled.push(current.title);
212
189
  }
213
190
  current = null;
@@ -235,8 +212,8 @@ export function scanRetroMirror(retroText) {
235
212
 
236
213
  /**
237
214
  * Core detection: locate the Epic temp tree under `baseDir`, sample its
238
- * signal streams, read its reject tally, and scan its retro mirror. Returns
239
- * a single combined finding when any dimension is non-clean, else `null`.
215
+ * signal streams, and scan its retro mirror. Returns a single combined
216
+ * finding when either dimension is non-clean, else `null`.
240
217
  *
241
218
  * @param {string} baseDir
242
219
  * @param {{
@@ -277,8 +254,6 @@ export function detectLoopHealth(
277
254
  sampled += r.sampled;
278
255
  }
279
256
 
280
- const rejectCount = readRejectTally(epicDir, { readImpl });
281
-
282
257
  let retroText = '';
283
258
  try {
284
259
  retroText = readImpl(path.join(epicDir, 'retro.md'), 'utf8');
@@ -287,20 +262,17 @@ export function detectLoopHealth(
287
262
  }
288
263
  const unfiledProposals = scanRetroMirror(retroText);
289
264
 
290
- const signalConcern = invalidCount > 0 || rejectCount > 0;
265
+ const signalConcern = invalidCount > 0;
291
266
  const proposalConcern = unfiledProposals.length > 0;
292
267
  if (!signalConcern && !proposalConcern) return null;
293
268
 
294
269
  const summaryParts = [];
295
270
  const detailLines = [];
296
271
  if (signalConcern) {
297
- summaryParts.push(
298
- `${invalidCount} schema-invalid signal sample(s), ${rejectCount} persisted reject(s)`,
299
- );
272
+ summaryParts.push(`${invalidCount} schema-invalid signal sample(s)`);
300
273
  detailLines.push(
301
274
  `Sampled ${sampled} line(s) across ${streams.length} signals.ndjson stream(s) (last ${maxLines} per stream):`,
302
275
  ` schema-invalid samples: ${invalidCount}`,
303
- ` persisted reject tally (signal-rejects.json): ${rejectCount}`,
304
276
  );
305
277
  }
306
278
  if (proposalConcern) {
@@ -320,7 +292,7 @@ export function detectLoopHealth(
320
292
  summary: `Loop-health (run-${epicId}): ${summaryParts.join('; ')}.`,
321
293
  detail: detailLines.join('\n'),
322
294
  fixCommand:
323
- 'Inspect temp/run-<id>/{signals.ndjson,signal-rejects.json,retro.md}; fix the signal producer or file/discard the surfaced proposals.',
295
+ 'Inspect temp/run-<id>/{signals.ndjson,retro.md}; fix the signal producer or file/discard the surfaced proposals.',
324
296
  autoCorrectable: false,
325
297
  };
326
298
  }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * child-exec.js — the one child-process execution surface (Story #5009).
3
+ *
4
+ * Every synchronous and asynchronous child this framework spawns runs through
5
+ * this module. It owns exactly three policies, and nothing else:
6
+ *
7
+ * 1. **The stdout ceiling.** `MAX_BUFFER_BYTES` is defined here and nowhere
8
+ * else. Node's child-process runners default `maxBuffer` to 1 MB and
9
+ * *kill* the child on overflow — `status: null`, `signal: 'SIGTERM'`,
10
+ * `error.code: 'ENOBUFS'` — so the call fails for a reason that has
11
+ * nothing to do with the command. That failure class has shipped
12
+ * operator-visible breakage more than once (Story #4914's committed
13
+ * baseline read at 1,178,910 bytes; Story #4948's `git push` relaying a
14
+ * 2,166,643-byte `pre-push` envelope, which reddened `phase: push` on
15
+ * every Story close while the gates themselves were green). Both were
16
+ * fixed one call site at a time. This module is why there is no third.
17
+ * 2. **Shell-free argv.** `shell: false` on every invocation, so no argument
18
+ * is ever shell-interpolated (`rules/security-baseline.md` § Output &
19
+ * Rendering). Callers pass argv tokens; they cannot pass a command line.
20
+ * 3. **Result and error normalisation.** {@link spawnCapture} collapses the
21
+ * `spawnSync` return into `{ status, stdout, stderr }` with a non-null
22
+ * status and trimmed streams; {@link formatChildFailure} renders the one
23
+ * failure-message shape so a thrown child error reads the same wherever
24
+ * it came from.
25
+ *
26
+ * Every wrapper takes an optional `run` — the injected child-process runner —
27
+ * so a module keeps its own test seam (`git-base.js`'s `__setSpawnRunner`,
28
+ * `git-utils.js`'s `__setGitRunners`, the `run` / `spawn` parameters threaded
29
+ * through the audit-baselines engine) while still delegating buffer, shell and
30
+ * error policy here. Omit it and the real Node runner is used.
31
+ *
32
+ * **Do not re-import `node:child_process` in new modules.**
33
+ * `tests/enforcement/child-process-imports.test.js` captures the current set
34
+ * of direct importers and fails on any addition outside that allowlist.
35
+ *
36
+ * @module lib/child-exec
37
+ */
38
+
39
+ import { execFile, execFileSync, spawnSync } from 'node:child_process';
40
+ import { promisify } from 'node:util';
41
+
42
+ /** One mebibyte, so the ceilings below read as the units they are quoted in. */
43
+ const MIB = 1024 * 1024;
44
+
45
+ /**
46
+ * The stdout ceiling every child in this framework runs under — the single
47
+ * definition of the constant.
48
+ *
49
+ * Deliberately **not** exported. Every wrapper below applies it as its
50
+ * default, so a call site gets the ceiling by importing the wrapper rather
51
+ * than by importing (and possibly forgetting) a number. The only bound a
52
+ * caller ever names explicitly is one that is deliberately *different* —
53
+ * today that is {@link INTERCEPTOR_MAX_BUFFER_BYTES} and nothing else.
54
+ *
55
+ * 64 MiB is not a fresh guess: it is the bound Stories #4914 and #4948 already
56
+ * settled on independently for the git read and git push paths, and the value
57
+ * `run-test-profile.js`, `audit-baselines/trend.js` and
58
+ * `audit-baselines/weights.js` had hand-copied. `maxBuffer` caps a buffer, it
59
+ * does not reserve one, so a generous ceiling costs nothing on the calls that
60
+ * print two lines.
61
+ *
62
+ * @type {number}
63
+ */
64
+ const MAX_BUFFER_BYTES = 64 * MIB;
65
+
66
+ /**
67
+ * The deliberately *lower* ceiling the friction interceptor spawns arbitrary
68
+ * operator commands under (`diagnose-friction.js`).
69
+ *
70
+ * This one is a reported policy bound, not an overflow guard: when it fires,
71
+ * the interceptor records `executionMaxBuffer` on the friction row and tells
72
+ * the operator to quieten the command rather than split it (Story #4915).
73
+ * Raising it to {@link MAX_BUFFER_BYTES} would change that emitted row, so it
74
+ * stays where it was — but it is defined here, alongside the ceiling it is
75
+ * deliberately different from, rather than hand-copied into the interceptor.
76
+ *
77
+ * @type {number}
78
+ */
79
+ export const INTERCEPTOR_MAX_BUFFER_BYTES = 10 * MIB;
80
+
81
+ /**
82
+ * Merge caller options over this module's fixed policy.
83
+ *
84
+ * `maxBuffer` is applied **last** so a caller's spread cannot silently drop it
85
+ * back to Node's 1 MB default — the only way to change the ceiling is to pass
86
+ * `maxBuffer` explicitly, which the two named constants above exist for.
87
+ *
88
+ * @param {{ encoding: string }} defaults - Runner-specific defaults.
89
+ * @param {object} rest - Caller options (`cwd`, `env`, `stdio`, …).
90
+ * @param {number} maxBuffer - Resolved stdout ceiling.
91
+ * @returns {object}
92
+ */
93
+ function childOptions(defaults, rest, maxBuffer) {
94
+ return { ...defaults, shell: false, ...rest, maxBuffer };
95
+ }
96
+
97
+ /**
98
+ * Run a child synchronously via `execFileSync`, returning whatever the runner
99
+ * returns (a UTF-8 string under the default encoding). **Throws** on a
100
+ * non-zero exit, exactly like `execFileSync` — callers that treat failure as a
101
+ * recoverable state want {@link spawnCapture} instead.
102
+ *
103
+ * @param {string} file - Executable name (never a shell command line).
104
+ * @param {string[]} args - Argv tokens.
105
+ * @param {object} [opts] - `cwd` / `env` / `stdio` / `encoding`, plus:
106
+ * @param {Function} [opts.run] - Injected runner; defaults to `execFileSync`.
107
+ * @param {number} [opts.maxBuffer] - Override the ceiling; defaults to {@link MAX_BUFFER_BYTES}.
108
+ * @returns {string}
109
+ */
110
+ export function execFileCapture(file, args, opts = {}) {
111
+ const { run = execFileSync, maxBuffer = MAX_BUFFER_BYTES, ...rest } = opts;
112
+ return run(file, args, childOptions({ encoding: 'utf8' }, rest, maxBuffer));
113
+ }
114
+
115
+ /** Promisified `execFile` — the default runner for {@link execFileCaptureAsync}. */
116
+ const execFileAsync = promisify(execFile);
117
+
118
+ /**
119
+ * The asynchronous sibling of {@link execFileCapture}. Resolves to
120
+ * `{ stdout, stderr }` and rejects on a non-zero exit.
121
+ *
122
+ * @param {string} file - Executable name.
123
+ * @param {string[]} args - Argv tokens.
124
+ * @param {object} [opts] - As {@link execFileCapture}; `run` defaults to a
125
+ * promisified `execFile`.
126
+ * @returns {Promise<{ stdout: string, stderr: string }>}
127
+ */
128
+ export function execFileCaptureAsync(file, args, opts = {}) {
129
+ const { run = execFileAsync, maxBuffer = MAX_BUFFER_BYTES, ...rest } = opts;
130
+ return run(file, args, childOptions({ encoding: 'utf8' }, rest, maxBuffer));
131
+ }
132
+
133
+ /**
134
+ * Run a child synchronously via `spawnSync`, returning the runner's **raw**
135
+ * result. Never throws on a non-zero exit.
136
+ *
137
+ * Use this when the caller needs the untouched result — untrimmed `stdout`
138
+ * (file contents, TAP output), a `status` of `null` that must stay `null`
139
+ * (`git show`'s 128-vs-killed split), or `result.error`. Callers that only
140
+ * want a normalised `{ status, stdout, stderr }` should use
141
+ * {@link spawnCapture}.
142
+ *
143
+ * @param {string} file - Executable name.
144
+ * @param {string[]} args - Argv tokens.
145
+ * @param {object} [opts] - As {@link execFileCapture}; `run` defaults to `spawnSync`.
146
+ * @returns {import('node:child_process').SpawnSyncReturns<string>}
147
+ */
148
+ export function spawnChild(file, args, opts = {}) {
149
+ const { run = spawnSync, maxBuffer = MAX_BUFFER_BYTES, ...rest } = opts;
150
+ return run(
151
+ file,
152
+ args,
153
+ childOptions({ encoding: 'utf-8', stdio: 'pipe' }, rest, maxBuffer),
154
+ );
155
+ }
156
+
157
+ /**
158
+ * {@link spawnChild} with the result normalised: `status` is coerced to `1`
159
+ * when the child did not exit normally (so no caller can read a `null` status
160
+ * as success — `process.exit(null)` exits 0), and both streams are coerced to
161
+ * trimmed strings.
162
+ *
163
+ * @param {string} file - Executable name.
164
+ * @param {string[]} args - Argv tokens.
165
+ * @param {object} [opts] - As {@link spawnChild}.
166
+ * @returns {{ status: number, stdout: string, stderr: string }}
167
+ */
168
+ export function spawnCapture(file, args, opts = {}) {
169
+ const result = spawnChild(file, args, opts);
170
+ return {
171
+ status: result?.status ?? 1,
172
+ stdout: (result?.stdout ?? '').toString().trim(),
173
+ stderr: (result?.stderr ?? '').toString().trim(),
174
+ };
175
+ }
176
+
177
+ /**
178
+ * The one failure-message shape for a child that exited non-zero.
179
+ *
180
+ * `status` is rendered verbatim — a `null` prints as `status=null`, which is
181
+ * the diagnostic: it means the child was killed (buffer overflow, timeout,
182
+ * signal) rather than having reported an exit code of its own.
183
+ *
184
+ * @param {object} failure
185
+ * @param {string} failure.label - What was being attempted, e.g. `readBaseFromGit: git show main:x`.
186
+ * @param {number|null} failure.status - Raw child exit status.
187
+ * @param {unknown} [failure.stderr] - Raw stderr; coerced and trimmed.
188
+ * @returns {string}
189
+ */
190
+ export function formatChildFailure({ label, status, stderr }) {
191
+ const detail = (stderr ?? '').toString().trim();
192
+ return `${label} failed (status=${status}): ${detail}`;
193
+ }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Replaces the per-CLI hand-rolled flag dispatch with a single
6
6
  * declarative entrypoint that the refactored scripts (story-close,
7
- * epic-deliver, check-baselines, audit-suite/cli) all call. The helper
7
+ * epic-deliver, check-baselines) all call. The helper
8
8
  * covers the flags every dispatcher CLI shares (`--epic`, `--story`,
9
9
  * `--changed-since`, `--json`, `--full-scope`, `--dry-run`)
10
10
  * and — via the `extras` schema entry — caller-defined extras (e.g.
@@ -109,6 +109,59 @@ function tolerantMergeWatchMode(value) {
109
109
  }
110
110
  }
111
111
 
112
+ /**
113
+ * Shortest override reason that can plausibly name a rejected finding. Below
114
+ * this, the flag is being used to silence the gate rather than to record a
115
+ * judgement, which is the failure mode the reason requirement exists to stop.
116
+ */
117
+ const MIN_OVERRIDE_REASON_LENGTH = 12;
118
+
119
+ /**
120
+ * Parse `--override-review-block <reason>` — the sanctioned,
121
+ * logged override of a Story-scope code-review critical blocker.
122
+ *
123
+ * The reason is **mandatory and validated**, because the alternative it
124
+ * replaces is not "no override" but `gh pr merge` run by hand: before this
125
+ * flag, a review blocker the operator had reviewed and rejected left bypassing
126
+ * the gate entirely as the only way to land, and that bypass wrote nothing
127
+ * down anywhere. An override that records why is strictly more auditable than
128
+ * the hand-merge it displaces; an override that records nothing is not, so a
129
+ * bare or blank `--override-review-block` fails closed here — before any phase
130
+ * runs, at no mutation cost — rather than arming a silent one.
131
+ *
132
+ * @param {unknown} value
133
+ * @returns {string|undefined} the trimmed reason, or `undefined` when absent.
134
+ */
135
+ export function parseOverrideReviewBlock(value) {
136
+ if (value == null) return undefined;
137
+ // `parseArgs` with `strict: false` yields `true` for a bare string flag.
138
+ const reason = typeof value === 'string' ? value.trim() : '';
139
+ if (reason.length >= MIN_OVERRIDE_REASON_LENGTH) return reason;
140
+ throw new Error(
141
+ '--override-review-block requires a reason of at least ' +
142
+ `${MIN_OVERRIDE_REASON_LENGTH} characters naming the finding you reviewed ` +
143
+ `and rejected (got ${JSON.stringify(value)}). The reason is posted to the ` +
144
+ 'PR and the Story and recorded as friction telemetry.',
145
+ );
146
+ }
147
+
148
+ /**
149
+ * {@link parseOverrideReviewBlock} degraded to absent instead of throwing, for
150
+ * the tolerant reporting parse. Same contract as
151
+ * {@link tolerantMergeWatchMode}: safe only because a tolerant parse runs no
152
+ * phase.
153
+ *
154
+ * @param {unknown} value
155
+ * @returns {string|undefined}
156
+ */
157
+ function tolerantOverrideReviewBlock(value) {
158
+ try {
159
+ return parseOverrideReviewBlock(value);
160
+ } catch {
161
+ return undefined;
162
+ }
163
+ }
164
+
112
165
  /**
113
166
  * Standardized CLI argument parser for sprint scripts.
114
167
  * Supports options like --epic, --story, --dry-run, --skip-dashboard.
@@ -147,6 +200,11 @@ export function parseSprintArgs(
147
200
  // Absent means "use the config"; see `parseMergeWatchMode` for why an
148
201
  // unrecognized value fails closed instead of degrading to absent.
149
202
  'merge-watch-mode': { type: 'string' },
203
+ // Sanctioned override of a code-review critical blocker.
204
+ // Absent means "the blocker blocks"; see `parseOverrideReviewBlock` for
205
+ // why a bare or too-short reason fails closed instead of arming a silent
206
+ // override.
207
+ 'override-review-block': { type: 'string' },
150
208
  executor: { type: 'string' },
151
209
  cwd: { type: 'string' },
152
210
  'recut-of': { type: 'string' },
@@ -183,6 +241,12 @@ export function parseSprintArgs(
183
241
  mergeWatchMode: tolerant
184
242
  ? tolerantMergeWatchMode(values['merge-watch-mode'])
185
243
  : parseMergeWatchMode(values['merge-watch-mode']),
244
+ // The operator's recorded reason for overriding a review
245
+ // blocker. `undefined` when the flag is absent, which is what keeps the
246
+ // blocker blocking by default.
247
+ overrideReviewBlock: tolerant
248
+ ? tolerantOverrideReviewBlock(values['override-review-block'])
249
+ : parseOverrideReviewBlock(values['override-review-block']),
186
250
  executor: values.executor ?? null,
187
251
  // Resolve worktree cwd from flag or env. Empty string/whitespace → null.
188
252
  cwd:
@@ -327,8 +327,8 @@ export function buildDefaultGates({
327
327
  ...buildTestGateEntry(coverageCaptureActive),
328
328
  {
329
329
  // Gate name kept generic ("format") so the close-orchestrator log line
330
- // and the per-gate phase-timer key don't shift when a repo swaps biome
331
- // for Prettier / dprint via `project.commands.formatCheck`. The
330
+ // doesn't shift when a repo swaps biome for Prettier / dprint via
331
+ // `project.commands.formatCheck`. The
332
332
  // actual command and the remediation hint resolve from config.
333
333
  name: 'format',
334
334
  cmd: formatCmd,
@@ -87,9 +87,9 @@ function applyChangedFileScope({ gate, spawnCwd, log }) {
87
87
  * `<tempRoot>/standalone/stories/story-<id>/validation-evidence.json`
88
88
  * keyspace. v2.0.0 removed the Epic tier and its Epic-keyed keyspace.
89
89
  *
90
- * `onGateStart` is invoked immediately before each gate's runner spawn.
91
- * story-close uses it to drive `phaseTimer.mark(...)` for per-gate
92
- * wall-clock telemetry. Errors thrown from the hook propagate.
90
+ * `onGateStart` is invoked immediately before each gate's runner spawn, so a
91
+ * caller can record per-gate wall-clock telemetry. Errors thrown from the
92
+ * hook propagate.
93
93
  *
94
94
  * Projection advisories (Story #4776): when `baseBranch` and `storyBranch`
95
95
  * are both supplied and every gate passed, the maintainability and CRAP
@@ -38,7 +38,6 @@
38
38
  */
39
39
  export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
40
40
  maxRounds: 2,
41
- clusterCeiling: 4,
42
41
  });
43
42
 
44
43
  /**
@@ -51,22 +50,6 @@ export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
51
50
  */
52
51
  export const ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING = 5;
53
52
 
54
- /**
55
- * Hard, undisableable ceiling on `clusterCeiling` — the max ACs one
56
- * single-delivery acceptance critic scores in a single fresh-context pass
57
- * (Epic #4475, M4-B, design §S2a). It is the **acceptance-dilution guard**:
58
- * single delivery collapses the whole Epic into one session, so the only
59
- * acceptance coverage left is the per-AC-cluster critic fan-out
60
- * (`ceil(totalACs / clusterCeiling)` independent maker-blind passes). A
61
- * pathologically large `clusterCeiling` would collapse that fan-out to a
62
- * single critic scoring every AC at once — exactly the dilution the design
63
- * forecloses — so a configured value above this ceiling is clamped down to
64
- * it. Kept small on purpose.
65
- *
66
- * @type {number}
67
- */
68
- export const ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX = 8;
69
-
70
53
  /**
71
54
  * Clamp a candidate round count into the inviolable `[1, ceiling]` range.
72
55
  * Non-integer / non-finite inputs fall back to the documented default.
@@ -85,43 +68,19 @@ function clampRounds(value, fallback) {
85
68
  return candidate;
86
69
  }
87
70
 
88
- /**
89
- * Clamp a candidate cluster ceiling into the inviolable
90
- * `[1, ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX]` range. Non-integer / non-finite
91
- * inputs fall back to the documented default. Mirrors `clampRounds` — the
92
- * anti-dilution guard cannot be disabled (`clusterCeiling: 0` or a negative
93
- * clamps up to 1; an over-max value clamps down to the hard cap).
94
- *
95
- * @param {unknown} value
96
- * @param {number} fallback
97
- * @returns {number}
98
- */
99
- function clampClusterCeiling(value, fallback) {
100
- const candidate =
101
- typeof value === 'number' && Number.isInteger(value) ? value : fallback;
102
- if (candidate < 1) return 1;
103
- if (candidate > ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX) {
104
- return ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX;
105
- }
106
- return candidate;
107
- }
108
-
109
71
  /**
110
72
  * Read the merged acceptance-eval block. Returns the canonical shape:
111
73
  *
112
74
  * {
113
- * maxRounds: number, // clamped into [1, roundsCeiling]
114
- * ceiling: number, // the undisableable hard cap on rounds
115
- * clusterCeiling: number, // clamped into [1, clusterCeilingMax]
116
- * clusterCeilingMax: number // the undisableable hard cap on cluster size
75
+ * maxRounds: number, // clamped into [1, ceiling]
76
+ * ceiling: number // the undisableable hard cap on rounds
117
77
  * }
118
78
  *
119
- * `maxRounds` is always a positive integer no greater than `ceiling`;
120
- * `clusterCeiling` is always a positive integer no greater than
121
- * `clusterCeilingMax`, regardless of what the resolved config carried.
79
+ * `maxRounds` is always a positive integer no greater than `ceiling`,
80
+ * regardless of what the resolved config carried.
122
81
  *
123
82
  * @param {object | null | undefined} config
124
- * @returns {{ maxRounds: number, ceiling: number, clusterCeiling: number, clusterCeilingMax: number }}
83
+ * @returns {{ maxRounds: number, ceiling: number }}
125
84
  */
126
85
  export function getAcceptanceEval(config) {
127
86
  const user = config?.delivery?.acceptanceEval ?? {};
@@ -129,14 +88,8 @@ export function getAcceptanceEval(config) {
129
88
  user.maxRounds,
130
89
  ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
131
90
  );
132
- const clusterCeiling = clampClusterCeiling(
133
- user.clusterCeiling,
134
- ACCEPTANCE_EVAL_DEFAULTS.clusterCeiling,
135
- );
136
91
  return {
137
92
  maxRounds,
138
93
  ceiling: ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING,
139
- clusterCeiling,
140
- clusterCeilingMax: ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX,
141
94
  };
142
95
  }
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * `project.commands` accessor (Epic #1720 Story #1739 — top-level reshape).
3
3
  *
4
- * The surviving five command keys are `lintBaseline`, `test`,
5
- * `typecheck`, `formatCheck`, `formatWrite`.
4
+ * The surviving four command keys are `test`, `typecheck`,
5
+ * `formatCheck`, `formatWrite`.
6
6
  */
7
7
 
8
8
  export const COMMANDS_DEFAULTS = Object.freeze({
9
- lintBaseline: 'npx eslint . --format json',
10
9
  test: 'npm test',
11
10
  typecheck: null,
12
11
  formatCheck: 'npx biome format .',
@@ -19,12 +18,11 @@ export const COMMANDS_DEFAULTS = Object.freeze({
19
18
  * a bare `{ project }` bag.
20
19
  *
21
20
  * @param {object | null | undefined} config
22
- * @returns {{ lintBaseline: string, test: string, typecheck: string|null, formatCheck: string, formatWrite: string }}
21
+ * @returns {{ test: string, typecheck: string|null, formatCheck: string, formatWrite: string }}
23
22
  */
24
23
  export function getCommands(config) {
25
24
  const commands = config?.project?.commands ?? {};
26
25
  return {
27
- lintBaseline: commands.lintBaseline ?? COMMANDS_DEFAULTS.lintBaseline,
28
26
  test: commands.test ?? COMMANDS_DEFAULTS.test,
29
27
  typecheck:
30
28
  commands.typecheck === undefined