mandrel 1.58.0 → 1.59.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 (129) hide show
  1. package/.agents/README.md +89 -87
  2. package/.agents/docs/SDLC.md +11 -7
  3. package/.agents/docs/workflows.md +2 -1
  4. package/.agents/schemas/audit-rules.json +20 -0
  5. package/.agents/scripts/acceptance-eval.js +20 -3
  6. package/.agents/scripts/assert-branch.js +1 -3
  7. package/.agents/scripts/bootstrap.js +1 -1
  8. package/.agents/scripts/check-arch-cycles.js +360 -0
  9. package/.agents/scripts/coverage-capture.js +24 -3
  10. package/.agents/scripts/epic-deliver-preflight.js +5 -3
  11. package/.agents/scripts/epic-deliver-prepare.js +12 -4
  12. package/.agents/scripts/epic-execute-record-wave.js +1 -1
  13. package/.agents/scripts/evidence-gate.js +1 -1
  14. package/.agents/scripts/git-rebase-and-resolve.js +1 -1
  15. package/.agents/scripts/hierarchy-gate.js +34 -14
  16. package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
  17. package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
  18. package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
  19. package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
  20. package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
  21. package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
  22. package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
  23. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
  24. package/.agents/scripts/lib/baselines/writer.js +1 -1
  25. package/.agents/scripts/lib/close-validation/commands.js +188 -0
  26. package/.agents/scripts/lib/close-validation/gates.js +235 -0
  27. package/.agents/scripts/lib/close-validation/process.js +101 -0
  28. package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
  29. package/.agents/scripts/lib/close-validation/runner.js +325 -0
  30. package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
  31. package/.agents/scripts/lib/config/quality.js +6 -6
  32. package/.agents/scripts/lib/config-resolver.js +2 -5
  33. package/.agents/scripts/lib/coverage-capture.js +147 -4
  34. package/.agents/scripts/lib/cpu-pool.js +14 -0
  35. package/.agents/scripts/lib/crap-utils.js +6 -11
  36. package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
  37. package/.agents/scripts/lib/git-utils.js +24 -22
  38. package/.agents/scripts/lib/maintainability-engine.js +1 -1
  39. package/.agents/scripts/lib/maintainability-utils.js +4 -187
  40. package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
  41. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
  42. package/.agents/scripts/lib/orchestration/code-review.js +90 -77
  43. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
  44. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
  45. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
  46. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
  47. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
  48. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
  49. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
  50. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
  51. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
  52. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
  53. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
  54. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
  55. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
  56. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
  57. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
  58. package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
  59. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
  60. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
  61. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
  62. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
  63. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
  64. package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
  65. package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
  66. package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
  67. package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
  68. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
  69. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
  70. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
  71. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
  72. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
  73. package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
  74. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
  75. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
  76. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
  77. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
  78. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
  79. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
  80. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
  81. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
  82. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
  83. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
  84. package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
  85. package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
  86. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
  87. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
  88. package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
  89. package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
  90. package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
  91. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  92. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
  93. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
  94. package/.agents/scripts/lib/project-root.js +17 -0
  95. package/.agents/scripts/lib/story-adjacency.js +76 -0
  96. package/.agents/scripts/lib/story-lifecycle.js +1 -1
  97. package/.agents/scripts/lib/transpile.js +93 -0
  98. package/.agents/scripts/lib/wave-runner/tick.js +4 -153
  99. package/.agents/scripts/lib/workers/crap-worker.js +1 -1
  100. package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
  101. package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
  102. package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
  103. package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
  104. package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
  105. package/.agents/scripts/providers/github/tickets.js +110 -6
  106. package/.agents/scripts/run-lint.js +9 -0
  107. package/.agents/scripts/run-tests.js +24 -4
  108. package/.agents/scripts/stories-wave-tick.js +8 -5
  109. package/.agents/scripts/story-init.js +149 -10
  110. package/.agents/scripts/sync-branch-from-base.js +1 -1
  111. package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
  112. package/.agents/workflows/audit-documentation.md +226 -0
  113. package/.agents/workflows/epic-deliver.md +16 -23
  114. package/.agents/workflows/epic-plan.md +1 -1
  115. package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
  116. package/.agents/workflows/helpers/single-story-deliver.md +2 -1
  117. package/.agents/workflows/onboard.md +4 -3
  118. package/.agents/workflows/story-deliver.md +1 -1
  119. package/README.md +13 -8
  120. package/lib/cli/init.js +336 -0
  121. package/package.json +2 -1
  122. package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
  123. package/.agents/scripts/lib/close-validation.js +0 -897
  124. package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
  125. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
  126. package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
  127. package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
  128. package/.agents/scripts/lib/task-utils.js +0 -26
  129. package/.agents/scripts/story-deliver-prepare.js +0 -267
@@ -0,0 +1,188 @@
1
+ /**
2
+ * close-validation/commands.js — Command resolution + formatter file policy.
3
+ *
4
+ * Owns the `project.commands.*` resolution helpers used by the close-
5
+ * validation gates (typecheck / formatCheck / formatWrite), the Story-diff
6
+ * changed-file listing for the format gate, and the formatter
7
+ * file-eligibility policy (Story #3410).
8
+ */
9
+
10
+ import { execFileSync } from 'node:child_process';
11
+ import { diffNameOnly } from '../changed-files.js';
12
+ import { getCommands } from '../config/commands.js';
13
+
14
+ /**
15
+ * Fallback typecheck command — the gate is mandatory by design (Epic-branch
16
+ * type regressions surface in the next Story's pre-push otherwise).
17
+ */
18
+ const TYPECHECK_FALLBACK = 'npm run typecheck';
19
+
20
+ /** Default formatter command when `project.commands.formatCheck` is unset. */
21
+ export const FORMAT_CHECK_FALLBACK = 'npx biome format .';
22
+
23
+ /** Default formatter command in write mode. */
24
+ const FORMAT_WRITE_FALLBACK = 'npx biome format --write .';
25
+
26
+ /**
27
+ * Build the format-gate hint dynamically from the resolved write command so
28
+ * a Prettier-only repo gets `prettier --write` in its hint, not biome.
29
+ */
30
+ export function buildFormatHint(writeCmd) {
31
+ const cmd =
32
+ writeCmd && writeCmd.trim().length > 0 ? writeCmd : FORMAT_WRITE_FALLBACK;
33
+ return `Run \`${cmd}\` to auto-fix formatting drift.`;
34
+ }
35
+
36
+ /**
37
+ * Resolve a string `project.commands.<key>` with a fallback when the
38
+ * value is missing, empty, or the resolver throws on malformed config.
39
+ * Shared engine behind the three resolveX command helpers.
40
+ *
41
+ * @param {{ project?: { commands?: object } } | null | undefined} config
42
+ * Canonical resolved config (or a bare `{ project: { commands } }` bag).
43
+ * @param {string} key
44
+ * @param {string} fallback
45
+ * @returns {string}
46
+ */
47
+ function resolveCommandWithFallback(config, key, fallback) {
48
+ try {
49
+ // `getCommands` reads `config.project.commands` from the canonical
50
+ // resolved config.
51
+ const cmds = getCommands(config);
52
+ const value = cmds[key];
53
+ if (typeof value === 'string' && value.trim().length > 0) {
54
+ return value.trim();
55
+ }
56
+ } catch {
57
+ // Malformed config — fall through to the framework default.
58
+ }
59
+ return fallback;
60
+ }
61
+
62
+ /**
63
+ * Resolve the typecheck command. Reads `project.commands.typecheck`;
64
+ * falls back to `npm run typecheck`. The framework-wide
65
+ * `COMMANDS_DEFAULTS.typecheck` is `null` but this gate is mandatory, so
66
+ * we apply the fallback here. Exported for testing.
67
+ *
68
+ * @param {{ project?: { commands?: object } } | null | undefined} config
69
+ * @returns {string}
70
+ */
71
+ export function resolveTypecheckCommand(config) {
72
+ return resolveCommandWithFallback(config, 'typecheck', TYPECHECK_FALLBACK);
73
+ }
74
+
75
+ /**
76
+ * Resolve the format-check command. Reads `project.commands.formatCheck`;
77
+ * falls back to `npx biome format .` so existing repos keep working byte-
78
+ * for-byte. Exported for testing.
79
+ *
80
+ * @param {{ project?: { commands?: object } } | null | undefined} config
81
+ * @returns {string}
82
+ */
83
+ export function resolveFormatCheckCommand(config) {
84
+ return resolveCommandWithFallback(
85
+ config,
86
+ 'formatCheck',
87
+ FORMAT_CHECK_FALLBACK,
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Resolve the format-write command used by story-close format-autofix (and
93
+ * surfaced in the format-gate hint). Reads `project.commands.formatWrite`;
94
+ * falls back to `npx biome format --write .`. Exported for testing.
95
+ *
96
+ * @param {{ project?: { commands?: object } } | null | undefined} config
97
+ * @returns {string}
98
+ */
99
+ export function resolveFormatWriteCommand(config) {
100
+ return resolveCommandWithFallback(
101
+ config,
102
+ 'formatWrite',
103
+ FORMAT_WRITE_FALLBACK,
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Compute the Story-diff file scope for formatter gates. The default Biome
109
+ * formatter used to run against `.` from inside `.worktrees/story-*`, which
110
+ * lets consumer ignore globs that exclude `.worktrees` self-exclude the whole
111
+ * run. Scoping to changed paths keeps verification real without depending on
112
+ * how consumers spell root ignore patterns.
113
+ *
114
+ * @param {{ cwd: string, baseRef: string }} opts
115
+ * @returns {string[]}
116
+ */
117
+ export function listChangedFilesForFormatGate({ cwd, baseRef }) {
118
+ if (!cwd) throw new Error('listChangedFilesForFormatGate: cwd is required');
119
+ if (!baseRef)
120
+ throw new Error('listChangedFilesForFormatGate: baseRef is required');
121
+ // Bridge execFileSync into the gitSpawn(cwd, ...args) contract so
122
+ // diffNameOnly owns the stdout → path-list conversion.
123
+ const gitSpawn = (_cwd, ...args) => {
124
+ try {
125
+ const stdout = execFileSync('git', args, {
126
+ cwd: _cwd,
127
+ encoding: 'utf8',
128
+ stdio: ['ignore', 'pipe', 'pipe'],
129
+ });
130
+ return { status: 0, stdout, stderr: '' };
131
+ } catch (err) {
132
+ return {
133
+ status: err.status ?? 1,
134
+ stdout: err.stdout ?? '',
135
+ stderr: err.stderr ?? err.message,
136
+ };
137
+ }
138
+ };
139
+ return diffNameOnly({ baseRef, cwd, gitSpawn });
140
+ }
141
+
142
+ /**
143
+ * File extensions Biome's formatter can process. Used to filter the
144
+ * changed-file scope down to the formatter-eligible subset (Story #3410):
145
+ * passing only ineligible paths (e.g. a docs-only Story whose diff is all
146
+ * markdown) makes `biome format <files>` report "No files were processed"
147
+ * and exit 1, failing the gate for a Story that has nothing to format.
148
+ *
149
+ * The set mirrors Biome's handled languages (JS/TS family + JSON + CSS).
150
+ * Markdown, YAML, and other unhandled types are intentionally absent — the
151
+ * default formatter is biome, so the scope is keyed to what biome formats.
152
+ * Consumers who swap the formatter via `project.commands.formatCheck`
153
+ * do not get `changedFileScope` at all (see `buildDefaultGates`), so this
154
+ * filter only ever runs against the default biome command.
155
+ */
156
+ const FORMATTER_ELIGIBLE_EXTENSIONS = new Set([
157
+ 'ts',
158
+ 'tsx',
159
+ 'js',
160
+ 'jsx',
161
+ 'mjs',
162
+ 'cjs',
163
+ 'json',
164
+ 'jsonc',
165
+ 'css',
166
+ ]);
167
+
168
+ /**
169
+ * Whether a changed path is eligible for the default (biome) formatter,
170
+ * decided purely by file extension. Pure function — no I/O. Exported for
171
+ * unit coverage (Story #3410).
172
+ *
173
+ * @param {string} filePath - A repo-relative path (forward-slash normalized).
174
+ * @returns {boolean}
175
+ */
176
+ export function isFormatterEligible(filePath) {
177
+ if (typeof filePath !== 'string') return false;
178
+ const lastSlash = Math.max(
179
+ filePath.lastIndexOf('/'),
180
+ filePath.lastIndexOf('\\'),
181
+ );
182
+ const base = filePath.slice(lastSlash + 1);
183
+ const dot = base.lastIndexOf('.');
184
+ // No extension (dotfile-only or extensionless) → not formatter-eligible.
185
+ if (dot <= 0) return false;
186
+ const ext = base.slice(dot + 1).toLowerCase();
187
+ return FORMATTER_ELIGIBLE_EXTENSIONS.has(ext);
188
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * close-validation/gates.js — Gate construction and partitioning.
3
+ *
4
+ * Owns the canonical close-validation gate list (`buildDefaultGates` /
5
+ * `DEFAULT_GATES`) and the parallel-vs-serial partitioning used by the
6
+ * runner (`INDEPENDENT_GATE_NAMES` / `partitionGates`).
7
+ */
8
+
9
+ import {
10
+ buildFormatHint,
11
+ FORMAT_CHECK_FALLBACK,
12
+ resolveFormatCheckCommand,
13
+ resolveFormatWriteCommand,
14
+ resolveTypecheckCommand,
15
+ } from './commands.js';
16
+
17
+ /**
18
+ * @typedef {Object} Gate
19
+ * @property {string} name - Short label used in progress logs.
20
+ * @property {string} cmd - Executable to run.
21
+ * @property {string[]} args - Arguments passed to `cmd`.
22
+ * @property {string} [hint] - Remediation hint shown on failure.
23
+ * @property {{ baseRef: string }} [changedFileScope] - Optional Story-diff scope.
24
+ * @property {Record<string, string>} [env] - Optional per-gate environment
25
+ * overlay. Merged over `process.env` for this gate's spawned child only.
26
+ * Used to thread the epic baseRef into the `check-baselines` gate via
27
+ * `BASELINE_REF` (Story #3890) so baseline regressions compare against the
28
+ * epic integration branch rather than `origin/main`.
29
+ * @property {(cmd: string, args: string[], opts: { cwd: string, gateName?: string, log?: (m: string) => void, signal?: AbortSignal, env?: Record<string, string> }) => Promise<{ status: number }> | { status: number }} [run]
30
+ * - Optional in-process runner. Story #1973: when present, the gate
31
+ * executes via this callable instead of spawning `cmd`/`args` through
32
+ * the default runner — used for per-kind baseline gates that import
33
+ * `compare(head, base)` directly.
34
+ */
35
+
36
+ const TYPECHECK_HINT =
37
+ 'TypeScript regression — fix type errors on the Story branch before retrying close. If the failure is a stale generated type (e.g. wrangler types), regenerate locally and commit before the close.';
38
+
39
+ function buildChangedFileScope(baseRef) {
40
+ if (!baseRef) return null;
41
+ return { baseRef };
42
+ }
43
+
44
+ /**
45
+ * Derive the per-gate `env` overlay that pins the `check-baselines`
46
+ * regression-compare base to the close run's integration branch
47
+ * (Story #3890).
48
+ *
49
+ * The baselines gate resolves its compare ref through `resolveScope`,
50
+ * whose environment layer reads `BASELINE_REF`. Threading
51
+ * `origin/<epicBranch>` here makes the gate diff head against the epic
52
+ * integration branch instead of the framework-default `origin/main`, so
53
+ * drift that already landed on `main` but is outside the Story's own diff
54
+ * does not surface as a phantom regression. The same convention
55
+ * (`origin/<epicBranch>`) is used by the baseline-attribution and
56
+ * auto-refresh paths, keeping read/compare bases aligned.
57
+ *
58
+ * Returns `null` when no integration branch is supplied (the gate then
59
+ * keeps its existing default-ref / consumer-config behaviour untouched).
60
+ *
61
+ * @param {string|undefined|null} epicBranch
62
+ * @returns {{ BASELINE_REF: string } | null}
63
+ */
64
+ function buildBaselinesGateEnv(epicBranch) {
65
+ if (typeof epicBranch !== 'string' || epicBranch.length === 0) return null;
66
+ return { BASELINE_REF: `origin/${epicBranch}` };
67
+ }
68
+
69
+ /**
70
+ * Resolve whether the CRAP gate is enabled. When enabled, the close-
71
+ * validation graph drops the standalone `test` gate because coverage-
72
+ * capture already runs the suite under c8 instrumentation (Story #1798).
73
+ *
74
+ * Reads the single canonical shape `delivery.quality.gates.crap.enabled`
75
+ * from the resolved config. Defaults to `true` so an omitted setting
76
+ * matches `CRAP_GATE_DEFAULTS.enabled`. We deliberately do NOT round-trip
77
+ * through `getQuality()` here because that resolver expects the unresolved
78
+ * `gates.crap.*` shape.
79
+ *
80
+ * @param {object|undefined|null} config - Canonical resolved config.
81
+ * @returns {boolean}
82
+ */
83
+ function isCrapGateEnabled(config) {
84
+ if (!config || typeof config !== 'object') return true;
85
+ const enabled = config?.delivery?.quality?.gates?.crap?.enabled;
86
+ return typeof enabled === 'boolean' ? enabled : true;
87
+ }
88
+
89
+ /**
90
+ * Conditionally produce the standalone `test` gate entry. Returns an empty
91
+ * array when the CRAP gate is enabled (Story #1798: coverage-capture is the
92
+ * canonical test runner in that mode); returns the legacy single-entry
93
+ * gate otherwise. Splitting this out keeps `buildDefaultGates` flat for
94
+ * the CRAP-cyclomatic gate.
95
+ *
96
+ * @param {object|undefined|null} config - Canonical resolved config.
97
+ * @returns {Gate[]}
98
+ */
99
+ function buildTestGateEntry(config) {
100
+ if (isCrapGateEnabled(config)) return [];
101
+ return [{ name: 'test', cmd: 'npm', args: ['test'] }];
102
+ }
103
+
104
+ /**
105
+ * Build the canonical close-validation gate list.
106
+ *
107
+ * Ordering (cheapest fast-fail first): typecheck → lint → [test] →
108
+ * format → coverage-capture → check-baselines. The standalone `test`
109
+ * gate is dropped when `crap.enabled === true` (Story #1798) because
110
+ * coverage-capture carries test-failure signalling under c8 in that
111
+ * mode.
112
+ *
113
+ * `typecheck` is mandatory; consumers may customise the command via
114
+ * `project.commands.typecheck` (default `npm run typecheck`).
115
+ *
116
+ * Story #2210 retired the legacy per-kind in-process regression gates
117
+ * (`check-maintainability`, `check-crap`, `check-mutation`) and their
118
+ * shared in-process runner. The unified `check-baselines` gate is now the
119
+ * single source of truth for per-kind regression enforcement
120
+ * (attribution-wired floor + tolerance + schema).
121
+ * The `epicBranch` parameter threads the close run's integration branch
122
+ * into two gates: the `format` gate's `changedFileScope` (existing) and —
123
+ * since Story #3890 — the `check-baselines` gate's `BASELINE_REF` env, so
124
+ * the baselines regression compare diffs head against the epic integration
125
+ * branch (`origin/<epicBranch>`) rather than the framework-default
126
+ * `origin/main`. Without this, every child Story on an `epic/<id>` branch
127
+ * re-discovered inherited main-vs-epic drift in untouched files as phantom
128
+ * regressions and worked around it by hand-setting `BASELINE_REF`.
129
+ *
130
+ * @param {{ config?: object, epicBranch?: string }} [opts] - `config` is the
131
+ * canonical resolved config (`{ project, delivery, ... }`); gate commands
132
+ * resolve from `project.commands` and the CRAP toggle from
133
+ * `delivery.quality.gates.crap.enabled`. `epicBranch` is the close run's
134
+ * integration branch (`epic/<id>` for Epic-attached Stories, the base
135
+ * branch for standalone Stories).
136
+ * @returns {Gate[]}
137
+ */
138
+ export function buildDefaultGates({ config, epicBranch } = {}) {
139
+ const typecheckCmdString = resolveTypecheckCommand(config);
140
+ const [typecheckCmd, ...typecheckArgs] = typecheckCmdString
141
+ .split(/\s+/)
142
+ .filter(Boolean);
143
+ const formatCheckString = resolveFormatCheckCommand(config);
144
+ const [formatCmd, ...formatArgs] = formatCheckString
145
+ .split(/\s+/)
146
+ .filter(Boolean);
147
+ const formatWriteString = resolveFormatWriteCommand(config);
148
+ const formatChangedFileScope =
149
+ formatCheckString === FORMAT_CHECK_FALLBACK
150
+ ? buildChangedFileScope(epicBranch)
151
+ : null;
152
+ const baselinesGateEnv = buildBaselinesGateEnv(epicBranch);
153
+ return [
154
+ {
155
+ name: 'typecheck',
156
+ cmd: typecheckCmd,
157
+ args: typecheckArgs,
158
+ hint: TYPECHECK_HINT,
159
+ },
160
+ { name: 'lint', cmd: 'npm', args: ['run', 'lint'] },
161
+ ...buildTestGateEntry(config),
162
+ {
163
+ // Gate name kept generic ("format") so the close-orchestrator log line
164
+ // and the per-gate phase-timer key don't shift when a repo swaps biome
165
+ // for Prettier / dprint via `project.commands.formatCheck`. The
166
+ // actual command and the remediation hint resolve from config.
167
+ name: 'format',
168
+ cmd: formatCmd,
169
+ args: formatArgs,
170
+ hint: buildFormatHint(formatWriteString),
171
+ ...(formatChangedFileScope
172
+ ? { changedFileScope: formatChangedFileScope }
173
+ : {}),
174
+ },
175
+ {
176
+ name: 'coverage-capture',
177
+ cmd: 'node',
178
+ args: ['.agents/scripts/coverage-capture.js'],
179
+ hint: 'Coverage capture failed — `npm run test:coverage` exited non-zero. Fix failing tests or coverage-threshold breaches, then re-run close.',
180
+ },
181
+ {
182
+ // Story #2210 — unified `check-baselines` gate is the only path for
183
+ // per-kind regression enforcement. The legacy per-kind in-process
184
+ // gates were retired because their regression-compare semantics are
185
+ // fully subsumed by this gate's attribution-wired floor + tolerance +
186
+ // schema enforcement, and running both paths in series was redundant
187
+ // and conflict-prone.
188
+ //
189
+ // `check-baselines.js` self-skips per-kind gates whose
190
+ // `enabled === false` is configured, so registering it
191
+ // unconditionally is safe.
192
+ name: 'check-baselines',
193
+ cmd: 'node',
194
+ args: ['.agents/scripts/check-baselines.js', '--format', 'text'],
195
+ hint: 'Unified baselines gate breached. Inspect the JSON report (`node .agents/scripts/check-baselines.js`) to see which kind/component/axis fell below floor; remediate the underlying file(s) or — when the regression is intentional — refresh the relevant baseline through its per-kind update script and commit with a `baseline-refresh:` tagged subject.',
196
+ ...(baselinesGateEnv ? { env: baselinesGateEnv } : {}),
197
+ },
198
+ ];
199
+ }
200
+
201
+ /**
202
+ * Default gate list resolved with no consumer config — uses the
203
+ * `npm run typecheck` fallback for the typecheck gate. Call sites that have a
204
+ * resolved config object in scope (e.g. `story-close.js`) should
205
+ * prefer `buildDefaultGates({ config })` so a configured
206
+ * `project.commands.typecheck` is honoured.
207
+ *
208
+ * @type {Gate[]}
209
+ */
210
+ export const DEFAULT_GATES = buildDefaultGates();
211
+
212
+ /**
213
+ * Gates whose I/O is read-only against the working tree (no shared mutable
214
+ * state, no overlapping ports/sockets). Safe to run concurrently — see
215
+ * `runCloseValidation` for the Promise.all + AbortController plumbing.
216
+ */
217
+ export const INDEPENDENT_GATE_NAMES = new Set(['lint', 'format', 'typecheck']);
218
+
219
+ /**
220
+ * Partition a gate list into the parallel-safe set and the order-sensitive
221
+ * remainder. Order is preserved within each bucket so the serial walk stays
222
+ * cheapest-fast-fail-first (test → coverage-capture → check-baselines).
223
+ *
224
+ * @param {Gate[]} gates
225
+ * @returns {{ independent: Gate[], serial: Gate[] }}
226
+ */
227
+ export function partitionGates(gates) {
228
+ const independent = [];
229
+ const serial = [];
230
+ for (const gate of gates) {
231
+ if (INDEPENDENT_GATE_NAMES.has(gate.name)) independent.push(gate);
232
+ else serial.push(gate);
233
+ }
234
+ return { independent, serial };
235
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * close-validation/process.js — Child-process lifecycle plumbing for gates.
3
+ *
4
+ * Owns the default async gate runner (spawn + line-prefixed stdio piping)
5
+ * and the AbortSignal / exit-code helpers it composes.
6
+ */
7
+
8
+ import { spawn } from 'node:child_process';
9
+
10
+ /**
11
+ * Pipe a child stream's output line-by-line through `emit`, prepending
12
+ * `prefix` to each line. Tail bytes without a trailing newline flush on
13
+ * `end` so the operator never loses the last line of a gate's output.
14
+ */
15
+ function pipePrefixed(stream, prefix, emit) {
16
+ let buf = '';
17
+ stream.setEncoding('utf8');
18
+ stream.on('data', (chunk) => {
19
+ buf += chunk;
20
+ while (true) {
21
+ const nl = buf.indexOf('\n');
22
+ if (nl === -1) break;
23
+ emit(prefix + buf.slice(0, nl));
24
+ buf = buf.slice(nl + 1);
25
+ }
26
+ });
27
+ stream.on('end', () => {
28
+ if (buf.length > 0) emit(prefix + buf);
29
+ });
30
+ }
31
+
32
+ /** Wire the AbortSignal so an abort kills the child. Returns the cleanup fn. */
33
+ export function attachGateAbortHandler(child, signal) {
34
+ if (!signal) return () => {};
35
+ const killChild = () => {
36
+ try {
37
+ child.kill('SIGTERM');
38
+ } catch {
39
+ /* race: already exited */
40
+ }
41
+ };
42
+ if (signal.aborted) {
43
+ killChild();
44
+ return () => {};
45
+ }
46
+ signal.addEventListener('abort', killChild, { once: true });
47
+ return () => signal.removeEventListener('abort', killChild);
48
+ }
49
+
50
+ /** SIGTERM (no exit code) on abort → non-zero so the gate counts as failed. */
51
+ export function gateExitCode(code, sig) {
52
+ if (typeof code === 'number') return code;
53
+ return sig ? 143 : 1;
54
+ }
55
+
56
+ /**
57
+ * Default async gate runner — used by `runCloseValidation` when no `runner`
58
+ * is injected. Spawns the gate via `child_process.spawn`, prefixes every
59
+ * stdout/stderr line with `[gate-name] ` (so concurrent gates don't bleed
60
+ * into each other in the operator's terminal), and resolves only when the
61
+ * child exits.
62
+ *
63
+ * Honours `opts.signal`: a TERM is delivered to the child the moment the
64
+ * signal fires, so a sibling gate's failure aborts the rest of the wave
65
+ * promptly. The promise still resolves (rather than rejecting) on abort —
66
+ * `runCloseValidation` sees a non-zero status and folds it into the
67
+ * already-recorded first-failure.
68
+ *
69
+ * @param {string} cmd
70
+ * @param {string[]} args
71
+ * @param {{ cwd: string, signal?: AbortSignal, gateName?: string, log?: (m: string) => void, env?: Record<string, string> }} opts
72
+ * @returns {Promise<{ status: number }>}
73
+ */
74
+ export function defaultGateRunner(cmd, args, opts = {}) {
75
+ const { cwd, signal, gateName, log, env } = opts;
76
+ const child = spawn(cmd, args, {
77
+ cwd,
78
+ shell: process.platform === 'win32',
79
+ stdio: ['ignore', 'pipe', 'pipe'],
80
+ // Per-gate env overlay (Story #3890): merged over the inherited
81
+ // environment so a gate-scoped `BASELINE_REF` reaches the spawned
82
+ // `check-baselines` child without mutating the parent process env.
83
+ ...(env ? { env: { ...process.env, ...env } } : {}),
84
+ });
85
+ const prefix = gateName ? `[${gateName}] ` : '';
86
+ const emit =
87
+ typeof log === 'function' ? log : (m) => process.stdout.write(`${m}\n`);
88
+ pipePrefixed(child.stdout, prefix, emit);
89
+ pipePrefixed(child.stderr, prefix, emit);
90
+ const detach = attachGateAbortHandler(child, signal);
91
+ return new Promise((resolve) => {
92
+ child.on('exit', (code, sig) => {
93
+ detach();
94
+ resolve({ status: gateExitCode(code, sig) });
95
+ });
96
+ child.on('error', () => {
97
+ detach();
98
+ resolve({ status: 1 });
99
+ });
100
+ });
101
+ }
@@ -15,11 +15,11 @@
15
15
  * pre-push time.
16
16
  */
17
17
 
18
+ import { getBaseline } from '../../baselines/maintainability-baseline-io.js';
18
19
  import { diffNameOnly } from '../../changed-files.js';
19
20
  import { cachedGitFetchSync } from '../../git/cached-fetch.js';
20
21
  import { gitSpawn as defaultGitSpawn } from '../../git-utils.js';
21
22
  import { calculateForSource } from '../../maintainability-engine.js';
22
- import { getBaseline } from '../../maintainability-utils.js';
23
23
  import { MISSING_ARG_REASONS, validateProjectionInputs } from './inputs.js';
24
24
 
25
25
  /**