mandrel 2.31.0 → 2.33.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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -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 +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  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 +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -1,14 +1,13 @@
1
1
  /**
2
- * bootstrap/manifest — single mutation-manifest source for the
3
- * consent-first phased install (Story #3521, Feature #3515, Epic #3438).
2
+ * bootstrap/manifest — single mutation-manifest source for the install
3
+ * ledger (Story #3521, Feature #3515, Epic #3438).
4
4
  *
5
5
  * `buildMutationManifest(ctx)` enumerates every mutation the bootstrap
6
- * pipeline can perform as a flat, declarative array. Each entry carries the
7
- * five fields the consent-first install screen needs to preview a change
8
- * before any write lands:
6
+ * pipeline can perform as a flat, declarative array. Each entry carries five
7
+ * fields:
9
8
  *
10
- * - `phaseGroup` — one of the four independently-approvable groups
11
- * (`ide-wiring`, `repo-config`, `github-admin`, `quality-gates`).
9
+ * - `phaseGroup` — one of the four groups (`ide-wiring`, `repo-config`,
10
+ * `github-admin`, `quality-gates`).
12
11
  * - `target` — the file path or remote resource the mutation touches.
13
12
  * - `action` — the verb describing the mutation (`create`, `merge`,
14
13
  * `update`, `run`, `configure`).
@@ -17,16 +16,18 @@
17
16
  * (e.g. delete a created file) vs. a remote-admin mutation that is not
18
17
  * a simple local revert.
19
18
  *
20
- * The manifest is the SINGLE source of truth: `applyProjectBootstrap`'s
21
- * no-write preview is derived from `buildMutationManifest` so the preview
22
- * the operator approves and the execution that follows enumerate the exact
23
- * same set of mutations. There is no second, drift-prone list.
19
+ * Story #3690 replaced the consent-first phased-approval install screen with
20
+ * a plain summary+confirm loop, and Story #5007 removed the vestigial
21
+ * machinery it left behind (the no-write `previewMutationManifest` renderer
22
+ * and the `approvedGroups` set-threading). What survives is load-bearing:
23
+ * `recordLedger` filters these entries into the install ledger, and
24
+ * `mandrel uninstall` walks them — the `phaseGroup` string is the ledger's
25
+ * grouping key, not an approval gate.
24
26
  *
25
27
  * The manifest describes *intended* mutations deterministically from the
26
28
  * project root; it does not read or mutate any file. Whether a given
27
29
  * mutation is a no-op on a particular clone (the file already carries the
28
- * wiring) is decided at execution time by the idempotent `ensure*` steps
29
- * the manifest always lists the full surface so the preview is complete.
30
+ * wiring) is decided at execution time by the idempotent `ensure*` steps.
30
31
  *
31
32
  * @module bootstrap/manifest
32
33
  */
@@ -34,9 +35,9 @@
34
35
  import path from 'node:path';
35
36
 
36
37
  /**
37
- * The four independently-approvable phase groups. The consent-first install
38
- * flow gates each group behind its own opt-in, so every manifest entry MUST
39
- * carry exactly one of these values.
38
+ * The four phase groups. Ledger/uninstall metadata only (Story #5007): every
39
+ * manifest entry MUST carry exactly one of these values so the install ledger
40
+ * can group and the uninstaller can select what a run actually landed.
40
41
  *
41
42
  * @type {Readonly<{ IDE_WIRING: 'ide-wiring', REPO_CONFIG: 'repo-config',
42
43
  * GITHUB_ADMIN: 'github-admin', QUALITY_GATES: 'quality-gates' }>}
@@ -271,27 +272,3 @@ export function buildMutationManifest(ctx = {}) {
271
272
 
272
273
  return entries;
273
274
  }
274
-
275
- /**
276
- * Render the manifest as a no-write preview report grouped by phase group.
277
- * Pure helper — derives entirely from {@link buildMutationManifest}, so the
278
- * preview and the executing pipeline enumerate one identical source.
279
- *
280
- * The returned shape is `{ preview: true, groups: { <phaseGroup>: entry[] },
281
- * entries: entry[] }`: callers that want the flat list read `entries`, and
282
- * the consent-first screen reads `groups` to render one approvable section
283
- * per phase group (only groups with at least one entry appear).
284
- *
285
- * @param {object} [ctx] — same context as {@link buildMutationManifest}.
286
- * @returns {{ preview: true, groups: Record<string, MutationManifestEntry[]>,
287
- * entries: MutationManifestEntry[] }}
288
- */
289
- export function previewMutationManifest(ctx = {}) {
290
- const entries = buildMutationManifest(ctx);
291
- const groups = {};
292
- for (const entry of entries) {
293
- if (!groups[entry.phaseGroup]) groups[entry.phaseGroup] = [];
294
- groups[entry.phaseGroup].push(entry);
295
- }
296
- return { preview: true, groups, entries };
297
- }
@@ -20,7 +20,7 @@ import { isCommandExcluded } from '../command-header.js';
20
20
  import { detectPackageManager as detectPm } from '../detect-package-manager.js';
21
21
  import { LEDGER_RELATIVE_PATH } from './install-ledger.js';
22
22
  import { ensureIssueForms } from './issue-forms-template.js';
23
- import { PHASE_GROUPS, previewMutationManifest } from './manifest.js';
23
+ import { PHASE_GROUPS } from './manifest.js';
24
24
  import { applyQualityBootstrap } from './quality-bootstrap.js';
25
25
 
26
26
  export const SYNC_COMMAND = 'node .agents/scripts/sync-claude-commands.js';
@@ -436,8 +436,7 @@ export function ensureGitignore(ctx) {
436
436
  * project (Story #4227). Derived from the Story-body SSOT so a human-filed
437
437
  * ticket round-trips through `story-body.parse()`. Idempotent and additive,
438
438
  * mirroring `ensureGitignore`: byte-identical forms are `unchanged`,
439
- * operator-edited forms are preserved (`custom-skip`). Honours `ctx.preview`
440
- * (no writes) like the other phases.
439
+ * operator-edited forms are preserved (`custom-skip`).
441
440
  *
442
441
  * Returns the per-form action envelope keyed by ticket type.
443
442
  *
@@ -446,13 +445,9 @@ export function ensureGitignore(ctx) {
446
445
  *
447
446
  * @param {object} ctx
448
447
  * @param {string} ctx.projectRoot
449
- * @param {boolean} [ctx.preview]
450
448
  */
451
449
  function ensureIssueFormsPhase(ctx) {
452
- const { forms } = ensureIssueForms({
453
- projectRoot: ctx.projectRoot,
454
- write: !ctx.preview,
455
- });
450
+ const { forms } = ensureIssueForms({ projectRoot: ctx.projectRoot });
456
451
  const outcomes = {};
457
452
  for (const form of forms) {
458
453
  outcomes[form.type] = { action: form.action, path: form.path };
@@ -691,12 +686,11 @@ const fatalParity = (result) =>
691
686
  * bootstrap.
692
687
  *
693
688
  * Each project-side mutation phase carries a `phaseGroup` matching one of
694
- * the consent-first {@link PHASE_GROUPS}. When a phased-approval gate is
695
- * supplied via `ctx.approvedGroups`, a phase whose `phaseGroup` is not in
696
- * the approved set is skipped (recorded as a `phase-group-declined` no-op)
697
- * declining one group never short-circuits the others (Story #3524).
698
- * Phases with no `phaseGroup` (the Node-version precondition and the
699
- * dependency install) are always-run infrastructure, never gated.
689
+ * the {@link PHASE_GROUPS}. Since Story #5007 that string is pure
690
+ * ledger/uninstall metadata every phase runs on every install, and the
691
+ * ledger decides after the fact which groups actually landed. Phases with no
692
+ * `phaseGroup` (the Node-version precondition and the dependency install)
693
+ * are always-run infrastructure.
700
694
  */
701
695
  export const BOOTSTRAP_PHASES = Object.freeze([
702
696
  {
@@ -770,24 +764,6 @@ export const BOOTSTRAP_PHASES = Object.freeze([
770
764
  },
771
765
  ]);
772
766
 
773
- /**
774
- * Decide whether a phase should run given the approved-phase-group gate.
775
- * An always-run infrastructure phase (no `phaseGroup`) runs unconditionally.
776
- * A grouped phase runs only when no gate is supplied (`approvedGroups`
777
- * absent — the un-gated legacy path) or when its group is in the gate.
778
- *
779
- * Exported for unit testing.
780
- *
781
- * @param {BootstrapPhase} phase
782
- * @param {Set<string>|undefined} approvedGroups
783
- * @returns {boolean}
784
- */
785
- export function isPhaseApproved(phase, approvedGroups) {
786
- if (!phase.phaseGroup) return true;
787
- if (!approvedGroups) return true;
788
- return approvedGroups.has(phase.phaseGroup);
789
- }
790
-
791
767
  /**
792
768
  * Throw with the formatted message when the phase is marked fatal and
793
769
  * the result indicates an abort. Pure helper so the driver stays a
@@ -809,12 +785,6 @@ export function throwIfFatal(phase, result) {
809
785
  * the result on `report[phase.name]`, then route fatal phases through
810
786
  * `throwIfFatal`. Returns the accumulated report.
811
787
  *
812
- * When `ctx.approvedGroups` is a `Set`, a grouped phase whose `phaseGroup`
813
- * is not approved is skipped and recorded as
814
- * `{ skipped: true, reason: 'phase-group-declined', phaseGroup }` — it never
815
- * runs, never throws (so a declined `ide-wiring` group also skips its
816
- * fatal `parity` check), and never short-circuits the remaining phases.
817
- *
818
788
  * Exported for tests so phase ordering and fatal behaviour can be
819
789
  * asserted without spawning a full bootstrap.
820
790
  *
@@ -825,14 +795,6 @@ export function throwIfFatal(phase, result) {
825
795
  export async function runPhases(phases, ctx) {
826
796
  const report = {};
827
797
  for (const phase of phases) {
828
- if (!isPhaseApproved(phase, ctx.approvedGroups)) {
829
- report[phase.name] = {
830
- skipped: true,
831
- reason: 'phase-group-declined',
832
- phaseGroup: phase.phaseGroup,
833
- };
834
- continue;
835
- }
836
798
  const result = await phase.run(ctx, report);
837
799
  report[phase.name] = result;
838
800
  throwIfFatal(phase, result);
@@ -844,24 +806,16 @@ export async function runPhases(phases, ctx) {
844
806
  * Compose every step in order. Each returned key is the outcome of one
845
807
  * step so the CLI can render a structured summary.
846
808
  *
847
- * When `ctx.preview` is truthy, the function performs **no writes and no
848
- * network I/O**. Instead it derives the operator-facing change list from
849
- * the single mutation-manifest source ({@link previewMutationManifest}) and
850
- * returns `{ preview: true, groups, entries }` the exact same source the
851
- * consent-first install screen renders. Deriving the preview from
852
- * `buildMutationManifest` (rather than from a parallel hand-maintained list)
853
- * guarantees the preview the operator approves and the execution that
854
- * follows enumerate one identical set of mutations (Story #3521).
809
+ * Story #5007 removed the no-write `ctx.preview` branch: it had zero
810
+ * production callers (bootstrap's own `--dry-run` renders a hand-rolled text
811
+ * plan instead) and it survived only to serve the consent-first install
812
+ * screen Story #3690 replaced with a plain summary+confirm loop.
855
813
  *
856
814
  * @param {object} ctx
857
815
  * @param {string} ctx.projectRoot
858
816
  * @param {string} [ctx.agentRoot]
859
817
  * @param {{ owner: string, repo: string, baseBranch: string,
860
818
  * operatorHandle: string|null }} ctx.answers
861
- * @param {boolean} [ctx.preview] — no-write preview from the manifest.
862
- * @param {Set<string>} [ctx.approvedGroups] — when present, only phases
863
- * whose `phaseGroup` is in this set execute (the consent-first gate from
864
- * Story #3524); always-run infrastructure phases ignore it.
865
819
  * @param {boolean} [ctx.withQuality]
866
820
  * @param {boolean} [ctx.skipGithub]
867
821
  * @param {boolean} [ctx.skipInstall]
@@ -871,6 +825,5 @@ export async function runPhases(phases, ctx) {
871
825
  * @returns {Promise<object>}
872
826
  */
873
827
  export async function applyProjectBootstrap(ctx) {
874
- if (ctx.preview) return previewMutationManifest(ctx);
875
828
  return runPhases(BOOTSTRAP_PHASES, ctx);
876
829
  }
@@ -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.