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.
- package/.agents/README.md +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +13 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- 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
|
/**
|