mandrel 2.25.0 → 2.26.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/agents/acceptance-critic.md +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -11
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Usage:
|
|
26
26
|
* node .agents/scripts/resolve-stories.js --ids 101,102
|
|
27
|
+
* node .agents/scripts/resolve-stories.js --ids 101-104 # inclusive range
|
|
27
28
|
* node .agents/scripts/resolve-stories.js --ids 101,102 --pretty
|
|
28
29
|
* node .agents/scripts/resolve-stories.js --ids 101 --no-native # skip the dependencies API
|
|
29
30
|
*
|
|
@@ -65,7 +66,9 @@ blocked_by edges, with every blocker (in-set or foreign) resolved against its
|
|
|
65
66
|
real issue state.
|
|
66
67
|
|
|
67
68
|
Options:
|
|
68
|
-
--ids <csv> Comma-separated Story issue numbers. Required.
|
|
69
|
+
--ids <csv> Comma-separated Story issue numbers. Required. A token may be
|
|
70
|
+
a single id (4922) or an inclusive dash range (4922-4926);
|
|
71
|
+
ranges expand in place and dedupe against the rest.
|
|
69
72
|
--pretty Pretty-print the JSON envelope.
|
|
70
73
|
--no-native Skip the native blocked_by read (body edges only).
|
|
71
74
|
--help Show this help.
|
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
* Node script keeps the env injection portable.
|
|
11
11
|
*
|
|
12
12
|
* Pipeline:
|
|
13
|
+
* 0. Run the full tier's preflight (`runTierPreflight`). Story #4936: this
|
|
14
|
+
* used to be a `pretest:coverage` npm script that CI had to name
|
|
15
|
+
* explicitly, because `.npmrc`'s `ignore-scripts=true` (CWE-1357
|
|
16
|
+
* defence, which stays) suppresses every `pre*` hook. The runner
|
|
17
|
+
* invokes it now, so it executes however the coverage run is started.
|
|
13
18
|
* 1. Run the test suite under `NODE_V8_COVERAGE` so each worker writes
|
|
14
|
-
* raw V8 dumps under `coverage/tmp/`.
|
|
19
|
+
* raw V8 dumps under `coverage/tmp/`. The suite targets are the
|
|
20
|
+
* shared `FULL_TIER_GLOBS` from `lib/test-tiers.js` — the same set
|
|
21
|
+
* `run-tests.js` walks — so the measured surface and `npm test`'s
|
|
22
|
+
* surface are the same set by construction.
|
|
15
23
|
* 2. `c8 report` post-processes the dumps into `coverage/coverage-final.json`
|
|
16
24
|
* plus the printed text table. Include/exclude scope from `.c8rc.cjs`
|
|
17
25
|
* is passed explicitly because `c8 report` does not auto-load the
|
|
@@ -28,13 +36,15 @@
|
|
|
28
36
|
* while producing an identical `coverage-final.json` artifact for the
|
|
29
37
|
* CRAP gate.
|
|
30
38
|
*
|
|
31
|
-
* Test-runner
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* TEST_CONCURRENCY_MAX]`.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* Test-runner flags: the suite spawn reuses `TEST_RUNNER_FLAGS` from
|
|
40
|
+
* `lib/test-runner-contract.js` — the single source of truth for the
|
|
41
|
+
* `node --test` flag set, including the host-derived
|
|
42
|
+
* `--test-concurrency` value clamped to `[TEST_CONCURRENCY_MIN,
|
|
43
|
+
* TEST_CONCURRENCY_MAX]`. Story #4936: sharing the whole flag set (not
|
|
44
|
+
* just the concurrency number) is what stops this runner and
|
|
45
|
+
* `run-tests.js` disagreeing about whether a test can execute at all —
|
|
46
|
+
* `--experimental-test-module-mocks` decides whether `t.mock.module`
|
|
47
|
+
* works, and this is the *required* CI job.
|
|
38
48
|
*/
|
|
39
49
|
|
|
40
50
|
import { spawnSync } from 'node:child_process';
|
|
@@ -46,56 +56,90 @@ import { fileURLToPath } from 'node:url';
|
|
|
46
56
|
import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
|
|
47
57
|
import { C8_CLI } from './lib/c8-cli-path.js';
|
|
48
58
|
import { buildWebhookSafeTestEnv } from './lib/test-env.js';
|
|
49
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
runTierPreflight,
|
|
61
|
+
TEST_RUNNER_FLAGS,
|
|
62
|
+
} from './lib/test-runner-contract.js';
|
|
63
|
+
import { FULL_TIER_GLOBS } from './lib/test-tiers.js';
|
|
50
64
|
|
|
51
65
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
66
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
53
|
-
const COVERAGE_DIR = path.join(ROOT, 'coverage');
|
|
54
67
|
const require = createRequire(import.meta.url);
|
|
55
68
|
const C8_CONFIG = require('../../.c8rc.cjs');
|
|
56
|
-
const V8_TMP = path.join(COVERAGE_DIR, 'tmp');
|
|
57
69
|
|
|
58
70
|
/**
|
|
59
71
|
* Build the `node --test` argv for the coverage suite spawn.
|
|
60
72
|
*
|
|
61
|
-
* Reuses
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
73
|
+
* Reuses two shared constants so the coverage path cannot drift from
|
|
74
|
+
* `run-tests.js`:
|
|
75
|
+
*
|
|
76
|
+
* - `TEST_RUNNER_FLAGS` — the single source of truth for the host-aware,
|
|
77
|
+
* clamped `--test-concurrency` value.
|
|
78
|
+
* - `FULL_TIER_GLOBS` — the single source of truth for *which files the
|
|
79
|
+
* full tier runs*. Story #4922: this script used to restate
|
|
80
|
+
* `tests/**\/*.test.js` on its own, so the 47 colocated `__tests__`
|
|
81
|
+
* suites under `lib/` and `.agents/scripts/` executed under `npm test`
|
|
82
|
+
* but never under the measuring run — the coverage and CRAP numbers
|
|
83
|
+
* were computed over code the coverage run had not executed.
|
|
84
|
+
*
|
|
85
|
+
* Both parameters are injected in tests so the argv can be asserted without
|
|
86
|
+
* touching the OS.
|
|
65
87
|
*
|
|
66
88
|
* @param {object} [opts]
|
|
67
89
|
* @param {readonly string[]} [opts.runnerFlags]
|
|
68
|
-
* @param {string} [opts.
|
|
90
|
+
* @param {readonly string[]} [opts.testGlobs]
|
|
69
91
|
* @returns {string[]}
|
|
70
92
|
*/
|
|
71
93
|
export function buildCoverageTestArgs({
|
|
72
94
|
runnerFlags = TEST_RUNNER_FLAGS,
|
|
73
|
-
|
|
95
|
+
testGlobs = FULL_TIER_GLOBS,
|
|
74
96
|
} = {}) {
|
|
75
|
-
return [...runnerFlags,
|
|
97
|
+
return [...runnerFlags, ...testGlobs];
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
/**
|
|
79
|
-
* Execute the coverage pipeline: run the
|
|
80
|
-
* post-process the dumps with `c8 report`,
|
|
81
|
-
* baseline. Returns the first non-zero exit code
|
|
82
|
-
* (or the baseline check's status when
|
|
101
|
+
* Execute the coverage pipeline: run the full tier's preflight, run the
|
|
102
|
+
* suite under `NODE_V8_COVERAGE`, post-process the dumps with `c8 report`,
|
|
103
|
+
* then gate on the coverage baseline. Returns the first non-zero exit code
|
|
104
|
+
* across the stages (or the baseline check's status when the prior stages
|
|
105
|
+
* pass); a refused preflight short-circuits before anything is spawned or
|
|
106
|
+
* removed.
|
|
83
107
|
*
|
|
108
|
+
* Every collaborator is injected so the pipeline's wiring — above all
|
|
109
|
+
* *that the preflight actually runs, and runs first* — is assertable
|
|
110
|
+
* without spawning the real suite.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} [opts]
|
|
113
|
+
* @param {typeof spawnSync} [opts.spawn]
|
|
114
|
+
* @param {(opts: { tier: string, repoRoot: string }) => number} [opts.preflight]
|
|
115
|
+
* @param {(opts: { repoRoot: string }) => unknown} [opts.cleanup]
|
|
116
|
+
* @param {string} [opts.repoRoot]
|
|
84
117
|
* @returns {number}
|
|
85
118
|
*/
|
|
86
|
-
function runCoveragePipeline(
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
export function runCoveragePipeline({
|
|
120
|
+
spawn = spawnSync,
|
|
121
|
+
preflight = runTierPreflight,
|
|
122
|
+
cleanup = cleanupRepoTestTempArtifacts,
|
|
123
|
+
repoRoot = ROOT,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const coverageDir = path.join(repoRoot, 'coverage');
|
|
126
|
+
const v8Tmp = path.join(coverageDir, 'tmp');
|
|
127
|
+
|
|
128
|
+
const preflightStatus = preflight({ tier: 'full', repoRoot });
|
|
129
|
+
if (preflightStatus !== 0) return preflightStatus;
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
rmSync(coverageDir, { recursive: true, force: true });
|
|
132
|
+
mkdirSync(v8Tmp, { recursive: true });
|
|
133
|
+
|
|
134
|
+
const testRun = spawn(process.execPath, buildCoverageTestArgs(), {
|
|
135
|
+
cwd: repoRoot,
|
|
92
136
|
stdio: 'inherit',
|
|
93
137
|
// GIT_*-scrubbed: under a husky pre-push from a linked worktree the
|
|
94
138
|
// inherited GIT_DIR poisons fixture `git init` runs (#4580).
|
|
95
|
-
env: { ...buildWebhookSafeTestEnv(process.env), NODE_V8_COVERAGE:
|
|
139
|
+
env: { ...buildWebhookSafeTestEnv(process.env), NODE_V8_COVERAGE: v8Tmp },
|
|
96
140
|
});
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
cleanup({ repoRoot });
|
|
99
143
|
|
|
100
144
|
const includeArgs = (C8_CONFIG.include ?? []).flatMap((p) => [
|
|
101
145
|
'--include',
|
|
@@ -106,29 +150,36 @@ function runCoveragePipeline() {
|
|
|
106
150
|
p,
|
|
107
151
|
]);
|
|
108
152
|
|
|
109
|
-
|
|
153
|
+
// `c8 report` does not auto-load `.c8rc.cjs`, so every scope knob it needs
|
|
154
|
+
// is forwarded explicitly from the config object — including `all`, which
|
|
155
|
+
// is what puts a 0 % row on a source file no test ever loaded (Story
|
|
156
|
+
// #4922). Reading it off the config keeps `.c8rc.cjs` the one declaration.
|
|
157
|
+
const allArgs = C8_CONFIG.all ? ['--all'] : [];
|
|
158
|
+
|
|
159
|
+
const reportRun = spawn(
|
|
110
160
|
process.execPath,
|
|
111
161
|
[
|
|
112
162
|
C8_CLI,
|
|
113
163
|
'report',
|
|
114
164
|
'--reporter=json',
|
|
115
165
|
'--reporter=text',
|
|
166
|
+
...allArgs,
|
|
116
167
|
'--temp-directory',
|
|
117
|
-
|
|
168
|
+
v8Tmp,
|
|
118
169
|
...includeArgs,
|
|
119
170
|
...excludeArgs,
|
|
120
171
|
],
|
|
121
|
-
{ cwd:
|
|
172
|
+
{ cwd: repoRoot, stdio: 'inherit', shell: false },
|
|
122
173
|
);
|
|
123
174
|
|
|
124
|
-
const checkRun =
|
|
175
|
+
const checkRun = spawn(
|
|
125
176
|
process.execPath,
|
|
126
177
|
[
|
|
127
|
-
path.join(
|
|
178
|
+
path.join(repoRoot, '.agents', 'scripts', 'check-baselines.js'),
|
|
128
179
|
'--gate',
|
|
129
180
|
'coverage',
|
|
130
181
|
],
|
|
131
|
-
{ cwd:
|
|
182
|
+
{ cwd: repoRoot, stdio: 'inherit' },
|
|
132
183
|
);
|
|
133
184
|
|
|
134
185
|
return testRun.status !== 0
|
|
@@ -28,11 +28,20 @@ const tasks = [
|
|
|
28
28
|
args: ['biome', 'ci', '.'],
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
+
// `docs/**/*.md` sat outside these globs until PR #4970's follow-up,
|
|
32
|
+
// so `npm run lint` reported "0 error(s)" while the close-time
|
|
33
|
+
// code-review lens — which lints the whole changed surface, not just
|
|
34
|
+
// what this driver globs — raised pre-existing `docs/` violations
|
|
35
|
+
// against whichever Story happened to touch the file. Keep `docs/`
|
|
36
|
+
// here so the two surfaces agree. `docs/CHANGELOG.md` is linted too;
|
|
37
|
+
// the generator-owned rules it can never satisfy are exempted by a
|
|
38
|
+
// `markdownlint-disable-file` directive in its own header.
|
|
31
39
|
name: 'markdownlint',
|
|
32
40
|
cmd: 'npx',
|
|
33
41
|
args: [
|
|
34
42
|
'markdownlint-cli2',
|
|
35
43
|
'.agents/**/*.md',
|
|
44
|
+
'docs/**/*.md',
|
|
36
45
|
'*.md',
|
|
37
46
|
'!node_modules/**',
|
|
38
47
|
'!.worktrees/**',
|
|
@@ -79,6 +88,17 @@ const tasks = [
|
|
|
79
88
|
cmd: 'node',
|
|
80
89
|
args: ['.agents/scripts/lint-label-vocabulary.js'],
|
|
81
90
|
},
|
|
91
|
+
{
|
|
92
|
+
// GitHub Actions job-timeout gate (Story #4936). Enumerates every
|
|
93
|
+
// `jobs.<id>` key across `.github/workflows/*.yml` and fails when one
|
|
94
|
+
// sets no `timeout-minutes` (inheriting GitHub's 360-minute default) or
|
|
95
|
+
// sets one above the ceiling. A deadlocked Windows job burned 44 minutes
|
|
96
|
+
// of a runner and withheld the failing required check's logs for the
|
|
97
|
+
// whole time; nothing but this check would have caught the gap.
|
|
98
|
+
name: 'workflow-timeouts',
|
|
99
|
+
cmd: 'node',
|
|
100
|
+
args: ['.agents/scripts/check-workflow-timeouts.js'],
|
|
101
|
+
},
|
|
82
102
|
{
|
|
83
103
|
// Architecture cycle ratchet (Story #3991). Detects directed import
|
|
84
104
|
// cycles under `.agents/scripts/` and fails on any cycle not in the
|
|
@@ -29,53 +29,34 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
32
|
-
import os from 'node:os';
|
|
33
32
|
import path from 'node:path';
|
|
34
33
|
import { fileURLToPath } from 'node:url';
|
|
35
34
|
import { assertNoReservedIdStreams } from './check-test-temp-hygiene.js';
|
|
36
35
|
import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
|
|
37
36
|
import { runAsCli } from './lib/cli-utils.js';
|
|
38
37
|
import { buildWebhookSafeTestEnv } from './lib/test-env.js';
|
|
38
|
+
import {
|
|
39
|
+
resolveTestConcurrency,
|
|
40
|
+
runTierPreflight,
|
|
41
|
+
TEST_CONCURRENCY_MAX,
|
|
42
|
+
TEST_CONCURRENCY_MIN,
|
|
43
|
+
TEST_RUNNER_FLAGS,
|
|
44
|
+
} from './lib/test-runner-contract.js';
|
|
39
45
|
import { listTestFilesForTier, parseTierArgv } from './lib/test-tiers.js';
|
|
40
46
|
|
|
41
47
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
48
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* without touching the OS.
|
|
55
|
-
*
|
|
56
|
-
* @param {number} [parallelism] - defaults to `os.availableParallelism()`
|
|
57
|
-
* @returns {number}
|
|
58
|
-
*/
|
|
59
|
-
export function resolveTestConcurrency(
|
|
60
|
-
parallelism = os.availableParallelism(),
|
|
61
|
-
) {
|
|
62
|
-
return Math.min(
|
|
63
|
-
TEST_CONCURRENCY_MAX,
|
|
64
|
-
Math.max(TEST_CONCURRENCY_MIN, parallelism),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Fixed `node --test` flags applied to every spawn (every chunk).
|
|
70
|
-
* `--test-concurrency` is derived at startup from the host's available
|
|
71
|
-
* parallelism so the value is appropriate for the machine running the suite
|
|
72
|
-
* rather than being pinned to the historical constant of 8.
|
|
73
|
-
*/
|
|
74
|
-
export const TEST_RUNNER_FLAGS = Object.freeze([
|
|
75
|
-
'--experimental-test-module-mocks',
|
|
76
|
-
'--test',
|
|
77
|
-
`--test-concurrency=${resolveTestConcurrency()}`,
|
|
78
|
-
]);
|
|
50
|
+
// The runner contract (flag set, concurrency clamp, tier preflight) is
|
|
51
|
+
// declared once in `lib/test-runner-contract.js` so `run-coverage.js` shares
|
|
52
|
+
// it by construction. Re-exported here because this module is the runner's
|
|
53
|
+
// public face for its own unit tests.
|
|
54
|
+
export {
|
|
55
|
+
resolveTestConcurrency,
|
|
56
|
+
TEST_CONCURRENCY_MAX,
|
|
57
|
+
TEST_CONCURRENCY_MIN,
|
|
58
|
+
TEST_RUNNER_FLAGS,
|
|
59
|
+
};
|
|
79
60
|
|
|
80
61
|
/**
|
|
81
62
|
* Per-spawn character budget for the joined *targets* portion of the argv.
|
|
@@ -167,8 +148,17 @@ export function runTestSuite({
|
|
|
167
148
|
listTargets = listTestFilesForTier,
|
|
168
149
|
maxTargetChars = resolveMaxTargetChars(),
|
|
169
150
|
fixtureStreamGuard = assertNoReservedIdStreams,
|
|
151
|
+
preflight = runTierPreflight,
|
|
170
152
|
} = {}) {
|
|
171
153
|
const { tier, rest } = parseTierArgv(argv);
|
|
154
|
+
|
|
155
|
+
// Preflight first, and abort on refusal — the tier's checks exist to stop
|
|
156
|
+
// the suite starting from a known-bad state, so running them after the
|
|
157
|
+
// spawn would be pointless. Nothing has been created yet, so there is
|
|
158
|
+
// nothing to clean up on this path.
|
|
159
|
+
const preflightStatus = preflight({ tier, repoRoot: cwd });
|
|
160
|
+
if (preflightStatus !== 0) return preflightStatus;
|
|
161
|
+
|
|
172
162
|
const targets = listTargets(tier, cwd);
|
|
173
163
|
const chunks = chunkTestTargets(targets, maxTargetChars);
|
|
174
164
|
|
|
@@ -43,6 +43,16 @@
|
|
|
43
43
|
* [--skip-validation] [--skip-sync]
|
|
44
44
|
* [--no-auto-merge]
|
|
45
45
|
* [--wait-merge | --no-wait-merge]
|
|
46
|
+
* [--merge-watch-mode <sync|async>]
|
|
47
|
+
*
|
|
48
|
+
* `--merge-watch-mode` (Story #4949) overrides `delivery.mergeWatch.mode` for
|
|
49
|
+
* one invocation, on the same explicit-wins-over-config precedence
|
|
50
|
+
* `--max-wait-seconds` uses, and the two compose. It exists because run
|
|
51
|
+
* topology is invisible from inside close: a solo delivery is cheapest waiting
|
|
52
|
+
* in the foreground, while the Nth close of a wave pays that wait as
|
|
53
|
+
* serialized dead time. The config default therefore stays `sync` and the
|
|
54
|
+
* orchestrator — the only party that knows N — passes `async` per close. An
|
|
55
|
+
* unrecognized value fails during option parsing, before any phase runs.
|
|
46
56
|
*
|
|
47
57
|
* Close-and-land is the DEFAULT for every run (Story #4428 introduced it as
|
|
48
58
|
* `--wait-merge`; `delivery.routing.closeAndLand` — default `true` — made it
|
|
@@ -85,7 +95,7 @@
|
|
|
85
95
|
* @see .agents/schemas/story-deliver-terminal.schema.json
|
|
86
96
|
*/
|
|
87
97
|
|
|
88
|
-
import {
|
|
98
|
+
import { parseSprintArgsTolerant } from './lib/cli-args.js';
|
|
89
99
|
import { runAsCli } from './lib/cli-utils.js';
|
|
90
100
|
import { formatCliError } from './lib/error-redactor.js';
|
|
91
101
|
import { Logger } from './lib/Logger.js';
|
|
@@ -151,13 +161,25 @@ export async function runSingleStoryClose(opts) {
|
|
|
151
161
|
* status rather than from a thrown/not-thrown distinction, so `pending`
|
|
152
162
|
* (resumable) is distinguishable from `blocked` (come look) without parsing
|
|
153
163
|
* stdout.
|
|
164
|
+
*
|
|
165
|
+
* The catch parses argv through the **non-throwing** wrapper (Story #4959).
|
|
166
|
+
* It used to call `parseSprintArgs()` — re-invoking the very parser that had
|
|
167
|
+
* just thrown, since `parseMergeWatchMode` made argv parsing fallible. The
|
|
168
|
+
* second throw escaped the handler, so an unparseable argv produced a bare
|
|
169
|
+
* stack trace with no envelope and no friction signal, on the surface whose
|
|
170
|
+
* whole contract is that every invocation emits exactly one envelope. An
|
|
171
|
+
* error handler may not depend on an operation already known to fail.
|
|
172
|
+
*
|
|
173
|
+
* A parse rejection carries no `closePhase`, so the envelope reports `init` —
|
|
174
|
+
* accurate: the runner rejected the flag before any phase ran, and nothing
|
|
175
|
+
* was mutated.
|
|
154
176
|
*/
|
|
155
177
|
async function main() {
|
|
156
178
|
try {
|
|
157
179
|
const outcome = await runSingleStoryClose();
|
|
158
180
|
return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
|
|
159
181
|
} catch (err) {
|
|
160
|
-
const terminal = failedTerminalFor(err,
|
|
182
|
+
const terminal = failedTerminalFor(err, parseSprintArgsTolerant().args);
|
|
161
183
|
if (!terminal) throw err;
|
|
162
184
|
// Mirror runAsCli's default error line (which this catch pre-empts) so the
|
|
163
185
|
// human-facing failure text is unchanged, then emit the envelope.
|
|
@@ -191,6 +213,10 @@ runAsCli(import.meta.url, main, {
|
|
|
191
213
|
['--wait-merge', 'Force the in-close merge wait.'],
|
|
192
214
|
['--no-wait-merge', 'Return as soon as the PR is open; do not wait.'],
|
|
193
215
|
['--max-wait-seconds <n>', 'Per-invocation merge-wait bound.'],
|
|
216
|
+
[
|
|
217
|
+
'--merge-watch-mode <sync|async>',
|
|
218
|
+
'Override delivery.mergeWatch.mode for this invocation only. `async` caps the merge wait to a short probe window and returns the resumable `pending` terminal instead of holding the foreground slot — pass it on every close of a multi-Story run. An invalid value exits non-zero before any phase runs.',
|
|
219
|
+
],
|
|
194
220
|
['--no-evidence', 'Do not reuse or write gate evidence stamps.'],
|
|
195
221
|
['--dry-run', 'Report the plan; mutate nothing.'],
|
|
196
222
|
],
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { parseArgs } from 'node:util';
|
|
40
|
-
import {
|
|
40
|
+
import { parseSprintArgsTolerant } from './lib/cli-args.js';
|
|
41
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
42
42
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
43
43
|
import { formatCliError } from './lib/error-redactor.js';
|
|
@@ -66,6 +66,15 @@ const progress = Logger.createProgress('single-story-confirm-merge', {
|
|
|
66
66
|
stderr: true,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* This CLI's own surface name — the `runAsCli` source, and the `emitter.tool`
|
|
71
|
+
* every friction record it emits carries. Single-homed so the two cannot
|
|
72
|
+
* drift: `emitTerminalFriction` defaults to the CLOSE CLI's name, so a record
|
|
73
|
+
* emitted from here without it is attributed to a CLI that never ran, and the
|
|
74
|
+
* retro roll-up sends the follow-up to the wrong surface.
|
|
75
|
+
*/
|
|
76
|
+
const CLI_SOURCE = 'single-story-confirm-merge';
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* Default `gh` facade for this CLI, bound to the merge wait's spawn-level
|
|
71
80
|
* timeout (Story #4710). This CLI is the resume surface async mode hands the
|
|
@@ -204,7 +213,7 @@ async function logConfirmResult(result, terminal, config) {
|
|
|
204
213
|
},
|
|
205
214
|
});
|
|
206
215
|
emitTerminalEnvelope(terminal, { config });
|
|
207
|
-
await emitTerminalFriction({ envelope: terminal, config });
|
|
216
|
+
await emitTerminalFriction({ envelope: terminal, tool: CLI_SOURCE, config });
|
|
208
217
|
return { success: terminal.status !== 'failed', result, terminal };
|
|
209
218
|
}
|
|
210
219
|
|
|
@@ -512,14 +521,21 @@ async function main() {
|
|
|
512
521
|
const outcome = await runConfirmMerge();
|
|
513
522
|
return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
|
|
514
523
|
} catch (err) {
|
|
515
|
-
|
|
524
|
+
// Non-throwing by construction (Story #4959): this used to call
|
|
525
|
+
// `parseSprintArgs()`, so a rejected `--merge-watch-mode` threw a second
|
|
526
|
+
// time here and escaped the handler — no envelope, no friction. Close
|
|
527
|
+
// carries the identical shape; both landing surfaces behave the same.
|
|
528
|
+
const { args, error: argvError } = parseSprintArgsTolerant();
|
|
529
|
+
const storyId = Number(args.storyId);
|
|
516
530
|
// No story id → a usage error; there is nothing to report an envelope
|
|
517
531
|
// about, so let runAsCli surface it as a plain fatal.
|
|
518
532
|
if (!Number.isInteger(storyId) || storyId <= 0) throw err;
|
|
519
533
|
const terminal = buildTerminalEnvelope({
|
|
520
534
|
storyId,
|
|
521
535
|
status: 'failed',
|
|
522
|
-
phase
|
|
536
|
+
// An argv rejection happens before any phase runs, so it reports at
|
|
537
|
+
// `init` rather than claiming a confirm-merge attempt that never began.
|
|
538
|
+
phase: argvError ? 'init' : 'confirm-merge',
|
|
523
539
|
failure: { reason: String(err?.message ?? err) },
|
|
524
540
|
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
525
541
|
elapsedSeconds: 0,
|
|
@@ -528,13 +544,13 @@ async function main() {
|
|
|
528
544
|
`[single-story-confirm-merge] Fatal error: ${formatCliError(err)}`,
|
|
529
545
|
);
|
|
530
546
|
emitTerminalEnvelope(terminal);
|
|
531
|
-
await emitTerminalFriction({ envelope: terminal });
|
|
547
|
+
await emitTerminalFriction({ envelope: terminal, tool: CLI_SOURCE });
|
|
532
548
|
return exitCodeForTerminal(terminal);
|
|
533
549
|
}
|
|
534
550
|
}
|
|
535
551
|
|
|
536
552
|
runAsCli(import.meta.url, main, {
|
|
537
|
-
source:
|
|
553
|
+
source: CLI_SOURCE,
|
|
538
554
|
propagateExitCode: true,
|
|
539
555
|
usage: {
|
|
540
556
|
invocation:
|