mandrel 2.53.0 → 2.55.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/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +4 -2
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +158 -7
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +68 -5
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +60 -114
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +19 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/findings/route-finding.js +4 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +241 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +24 -1
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +119 -6
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +59 -38
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +70 -6
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +40 -16
- package/.agents/workflows/helpers/deliver-story-reference.md +7 -4
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +8 -1
- package/.agents/workflows/mandrel-plan.md +4 -7
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +34 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -36,15 +36,80 @@ const BASELINE_MERGE_ATTRIBUTE = `baselines/*.json merge=${BASELINE_MERGE_DRIVER
|
|
|
36
36
|
/** Git config key holding the driver command. */
|
|
37
37
|
export const BASELINE_MERGE_DRIVER_CONFIG_KEY = `merge.${BASELINE_MERGE_DRIVER_NAME}.driver`;
|
|
38
38
|
|
|
39
|
+
/** Script path the driver command invokes, relative to the worktree root. */
|
|
40
|
+
const DRIVER_SCRIPT = '.agents/scripts/merge-baseline.js';
|
|
41
|
+
|
|
42
|
+
/** Git's merge-driver placeholders, in the order the driver's argv expects. */
|
|
43
|
+
const DRIVER_PLACEHOLDERS = '%O %A %B %P';
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
46
|
+
* Build the driver command for a given node binary.
|
|
47
|
+
*
|
|
48
|
+
* The interpreter is the RESOLVED ABSOLUTE path (`process.execPath`), not the
|
|
49
|
+
* bare word `node`, and it is quoted. Git runs a merge driver through the
|
|
50
|
+
* shell, and that shell's `PATH` is whatever launched git — a GUI client, a
|
|
51
|
+
* Finder-launched editor, a `launchd` job — none of which necessarily carry
|
|
52
|
+
* the nvm/volta shim that puts `node` on an interactive shell's `PATH`. A
|
|
53
|
+
* driver that cannot start is not a loud failure: git reports the driver
|
|
54
|
+
* exited non-zero and falls back to leaving the file conflicted, which reads
|
|
55
|
+
* as "baselines conflict again" — the exact symptom the driver exists to
|
|
56
|
+
* remove. Quoting covers the installation whose node lives under a path with
|
|
57
|
+
* a space (`/Users/a b/.nvm/...`).
|
|
58
|
+
*
|
|
59
|
+
* The script path stays RELATIVE: git runs the driver from the worktree root,
|
|
60
|
+
* and that is also where `mandrel sync` materializes `.agents/`, so one
|
|
61
|
+
* command is correct in the main checkout and in every linked worktree.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} [execPath] Absolute path to the node binary.
|
|
64
|
+
* @returns {string}
|
|
42
65
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
function buildBaselineMergeDriverCommand(execPath = process.execPath) {
|
|
67
|
+
return `"${execPath}" ${DRIVER_SCRIPT} ${DRIVER_PLACEHOLDERS}`;
|
|
68
|
+
}
|
|
45
69
|
|
|
46
|
-
/** The
|
|
47
|
-
|
|
70
|
+
/** The driver command this process would install. */
|
|
71
|
+
const BASELINE_MERGE_DRIVER_COMMAND = buildBaselineMergeDriverCommand();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The exact command an operator runs to complete registration. Single-quoted
|
|
75
|
+
* because the command itself carries the double quotes around the node path.
|
|
76
|
+
*/
|
|
77
|
+
export const BASELINE_MERGE_DRIVER_REMEDY = `git config ${BASELINE_MERGE_DRIVER_CONFIG_KEY} '${BASELINE_MERGE_DRIVER_COMMAND}'`;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Split a configured driver command into an executable + argv pair, dropping
|
|
81
|
+
* git's `%O %A %B %P` placeholders.
|
|
82
|
+
*
|
|
83
|
+
* Used by the `mandrel doctor` check to actually RUN the configured command
|
|
84
|
+
* (with `--help`) rather than only assert the config key is non-empty. A key
|
|
85
|
+
* pointing at a node that no longer exists — the ordinary outcome of an nvm
|
|
86
|
+
* version bump after the driver was installed — is set, non-empty, and
|
|
87
|
+
* completely broken, and the pre-Story-#5277 check called that healthy.
|
|
88
|
+
*
|
|
89
|
+
* Tokenising here rather than handing the string to a shell is deliberate:
|
|
90
|
+
* the value is per-clone git config, and executing it through `shell: true`
|
|
91
|
+
* would turn any write to that key into arbitrary command execution
|
|
92
|
+
* (`security-baseline.md` § Output & Rendering — never concatenate into a
|
|
93
|
+
* shell command). Only double quotes are honoured, which is exactly the
|
|
94
|
+
* quoting {@link buildBaselineMergeDriverCommand} emits.
|
|
95
|
+
*
|
|
96
|
+
* @param {string|null|undefined} command
|
|
97
|
+
* @returns {{ file: string, args: string[] }|null} null when there is no
|
|
98
|
+
* runnable first token.
|
|
99
|
+
*/
|
|
100
|
+
export function parseBaselineMergeDriverCommand(command) {
|
|
101
|
+
const tokens = String(command ?? '').match(/"[^"]*"|\S+/g) ?? [];
|
|
102
|
+
const cleaned = tokens
|
|
103
|
+
.map((token) =>
|
|
104
|
+
token.startsWith('"') && token.endsWith('"') && token.length >= 2
|
|
105
|
+
? token.slice(1, -1)
|
|
106
|
+
: token,
|
|
107
|
+
)
|
|
108
|
+
.filter((token) => token.length > 0 && !/^%[A-Za-z]$/.test(token));
|
|
109
|
+
if (cleaned.length === 0) return null;
|
|
110
|
+
const [file, ...args] = cleaned;
|
|
111
|
+
return { file, args };
|
|
112
|
+
}
|
|
48
113
|
|
|
49
114
|
/**
|
|
50
115
|
* Does this `.gitattributes` content route baselines through the driver?
|
|
@@ -53,7 +118,7 @@ export const BASELINE_MERGE_DRIVER_REMEDY = `git config ${BASELINE_MERGE_DRIVER_
|
|
|
53
118
|
* @param {string|null|undefined} gitattributes
|
|
54
119
|
* @returns {boolean}
|
|
55
120
|
*/
|
|
56
|
-
|
|
121
|
+
function declaresBaselineMergeDriver(gitattributes) {
|
|
57
122
|
return String(gitattributes ?? '')
|
|
58
123
|
.split('\n')
|
|
59
124
|
.some((line) => {
|
|
@@ -63,6 +128,55 @@ export function declaresBaselineMergeDriver(gitattributes) {
|
|
|
63
128
|
});
|
|
64
129
|
}
|
|
65
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Is the driver DECLARED by this project and REGISTERED in this clone?
|
|
133
|
+
*
|
|
134
|
+
* Two callers ask this exact question of two different clones — the
|
|
135
|
+
* `mandrel doctor` check, and the pre-push base-sync guard — and both need
|
|
136
|
+
* the same two-part answer, because the two halves fail differently:
|
|
137
|
+
*
|
|
138
|
+
* - **Not declared** → the project never opted in. Both callers stay
|
|
139
|
+
* silent; telling a consumer to register a driver for files they do not
|
|
140
|
+
* route through it is noise. An unreadable `.gitattributes` reads the
|
|
141
|
+
* same way, deliberately.
|
|
142
|
+
* - **Declared, empty command** → the silent case. Git reports nothing at
|
|
143
|
+
* all: it falls back to its own line-based merge, which on a generated
|
|
144
|
+
* baseline either conflicts on the `generatedAt` stamp or splices both
|
|
145
|
+
* sides' rows into a set neither side scored.
|
|
146
|
+
*
|
|
147
|
+
* The `runGit` seam takes argv tokens and returns `{ status, stdout }`, which
|
|
148
|
+
* is the shape both callers' own git surfaces already produce.
|
|
149
|
+
*
|
|
150
|
+
* @param {{
|
|
151
|
+
* projectRoot: string,
|
|
152
|
+
* fsImpl?: typeof fs,
|
|
153
|
+
* runGit: (args: string[]) => { status?: number|null, stdout?: unknown },
|
|
154
|
+
* }} ctx
|
|
155
|
+
* @returns {{ declared: boolean, command: string }}
|
|
156
|
+
*/
|
|
157
|
+
export function probeBaselineMergeDriver({ projectRoot, fsImpl = fs, runGit }) {
|
|
158
|
+
let attributes = '';
|
|
159
|
+
try {
|
|
160
|
+
attributes = fsImpl.readFileSync(
|
|
161
|
+
path.join(projectRoot, '.gitattributes'),
|
|
162
|
+
'utf8',
|
|
163
|
+
);
|
|
164
|
+
} catch {
|
|
165
|
+
attributes = '';
|
|
166
|
+
}
|
|
167
|
+
if (!declaresBaselineMergeDriver(attributes)) {
|
|
168
|
+
return { declared: false, command: '' };
|
|
169
|
+
}
|
|
170
|
+
const configured = runGit([
|
|
171
|
+
'config',
|
|
172
|
+
'--get',
|
|
173
|
+
BASELINE_MERGE_DRIVER_CONFIG_KEY,
|
|
174
|
+
]);
|
|
175
|
+
const command =
|
|
176
|
+
configured?.status === 0 ? String(configured.stdout ?? '').trim() : '';
|
|
177
|
+
return { declared: true, command };
|
|
178
|
+
}
|
|
179
|
+
|
|
66
180
|
/**
|
|
67
181
|
* Add the attribute line, preserving every existing line verbatim.
|
|
68
182
|
*
|
|
@@ -144,32 +258,84 @@ function ensureDriverGitConfig(projectRoot, spawnImpl = spawnCapture) {
|
|
|
144
258
|
}
|
|
145
259
|
|
|
146
260
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
261
|
+
* Read `.gitattributes` without writing it — the `configOnly` half of
|
|
262
|
+
* {@link ensureBaselineMergeDriver}.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} projectRoot
|
|
265
|
+
* @param {typeof fs} fsImpl
|
|
266
|
+
* @returns {{ action: 'already-present'|'absent', path: string }}
|
|
267
|
+
*/
|
|
268
|
+
function probeGitattributesLine(projectRoot, fsImpl) {
|
|
269
|
+
const target = path.join(projectRoot, '.gitattributes');
|
|
270
|
+
let existing = '';
|
|
271
|
+
try {
|
|
272
|
+
existing = fsImpl.readFileSync(target, 'utf8');
|
|
273
|
+
} catch {
|
|
274
|
+
existing = '';
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
action: declaresBaselineMergeDriver(existing)
|
|
278
|
+
? 'already-present'
|
|
279
|
+
: 'absent',
|
|
280
|
+
path: target,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Install the registration. Idempotent: a second run reports
|
|
286
|
+
* `already-present` and changes no bytes.
|
|
287
|
+
*
|
|
288
|
+
* Two modes, because the two callers mean different things by "install":
|
|
289
|
+
*
|
|
290
|
+
* - **Full (default)** — `mandrel init --with-quality` and this repo's own
|
|
291
|
+
* `prepare`. Writes the `.gitattributes` line as well as the config key:
|
|
292
|
+
* the caller has opted the repository into the quality surface, so
|
|
293
|
+
* declaring the attribute is part of what it asked for.
|
|
294
|
+
* - **`configOnly: true`** — consumer `mandrel sync`. Writes ONLY the
|
|
295
|
+
* per-clone config key, and only when `.gitattributes` already declares the
|
|
296
|
+
* attribute. `sync` materializes `.agents/`; it is not an opt-in to the
|
|
297
|
+
* quality surface, and creating a `.gitattributes` in a project that never
|
|
298
|
+
* asked for one would be `sync` changing git's behaviour on files it has no
|
|
299
|
+
* business touching. What it DOES fix is the real gap: the attribute is
|
|
300
|
+
* tracked and therefore ships with the repo, while the config key is
|
|
301
|
+
* per-clone and silently absent in every fresh clone — so the half that
|
|
302
|
+
* cannot travel is installed on the one command every consumer runs.
|
|
149
303
|
*
|
|
150
304
|
* @param {object} ctx
|
|
151
305
|
* @param {string} ctx.projectRoot
|
|
306
|
+
* @param {boolean} [ctx.configOnly]
|
|
152
307
|
* @param {typeof spawnCapture} [ctx.spawnImpl]
|
|
153
308
|
* @param {typeof fs} [ctx.fsImpl]
|
|
154
309
|
* @returns {{
|
|
155
|
-
* action: 'already-present'|'updated',
|
|
310
|
+
* action: 'already-present'|'updated'|'skipped',
|
|
156
311
|
* attributes: string,
|
|
157
312
|
* config: string,
|
|
158
313
|
* path: string,
|
|
159
314
|
* line: string,
|
|
315
|
+
* command: string,
|
|
160
316
|
* }}
|
|
161
317
|
*/
|
|
162
318
|
export function ensureBaselineMergeDriver(ctx) {
|
|
163
|
-
const
|
|
319
|
+
const fsImpl = ctx.fsImpl ?? fs;
|
|
320
|
+
const attributes = ctx.configOnly
|
|
321
|
+
? probeGitattributesLine(ctx.projectRoot, fsImpl)
|
|
322
|
+
: ensureGitattributesLine(ctx.projectRoot, fsImpl);
|
|
323
|
+
const base = {
|
|
324
|
+
attributes: attributes.action,
|
|
325
|
+
path: attributes.path,
|
|
326
|
+
line: BASELINE_MERGE_ATTRIBUTE,
|
|
327
|
+
command: BASELINE_MERGE_DRIVER_COMMAND,
|
|
328
|
+
};
|
|
329
|
+
if (attributes.action === 'absent') {
|
|
330
|
+
return { ...base, action: 'skipped', config: 'skipped' };
|
|
331
|
+
}
|
|
164
332
|
const config = ensureDriverGitConfig(ctx.projectRoot, ctx.spawnImpl);
|
|
165
333
|
const settled =
|
|
166
334
|
attributes.action === 'already-present' &&
|
|
167
335
|
(config.action === 'already-present' || config.action === 'not-a-repo');
|
|
168
336
|
return {
|
|
337
|
+
...base,
|
|
169
338
|
action: settled ? 'already-present' : 'updated',
|
|
170
|
-
attributes: attributes.action,
|
|
171
339
|
config: config.action,
|
|
172
|
-
path: attributes.path,
|
|
173
|
-
line: BASELINE_MERGE_ATTRIBUTE,
|
|
174
340
|
};
|
|
175
341
|
}
|
|
@@ -62,6 +62,22 @@ function parsePositiveInt(value) {
|
|
|
62
62
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The same contract for a flag whose ZERO is meaningful (Story #5266):
|
|
67
|
+
* `--rerun-advisory 0` is an explicit "spend nothing", not an absent flag.
|
|
68
|
+
* A negative or non-numeric value is still treated as absent rather than
|
|
69
|
+
* coerced, so a typo falls back to the config default instead of inventing an
|
|
70
|
+
* allowance that spends the consumer's CI minutes.
|
|
71
|
+
*
|
|
72
|
+
* @param {unknown} value
|
|
73
|
+
* @returns {number|undefined}
|
|
74
|
+
*/
|
|
75
|
+
function parseNonNegativeInt(value) {
|
|
76
|
+
if (value === undefined || value === null) return undefined;
|
|
77
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
78
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
/** The only two merge-watch postures `--merge-watch-mode` accepts. */
|
|
66
82
|
const MERGE_WATCH_MODES = ['sync', 'async'];
|
|
67
83
|
|
|
@@ -200,6 +216,10 @@ export function parseSprintArgs(
|
|
|
200
216
|
// Absent means "use the config"; see `parseMergeWatchMode` for why an
|
|
201
217
|
// unrecognized value fails closed instead of degrading to absent.
|
|
202
218
|
'merge-watch-mode': { type: 'string' },
|
|
219
|
+
// Story #5266 — per-invocation override of `delivery.ci.rerunAdvisory`.
|
|
220
|
+
// Absent means "use the config", whose default is 0: close re-runs
|
|
221
|
+
// nothing and mutates no GitHub state on an advisory red unless asked.
|
|
222
|
+
'rerun-advisory': { type: 'string' },
|
|
203
223
|
// Sanctioned override of a code-review critical blocker.
|
|
204
224
|
// Absent means "the blocker blocks"; see `parseOverrideReviewBlock` for
|
|
205
225
|
// why a bare or too-short reason fails closed instead of arming a silent
|
|
@@ -240,6 +260,12 @@ export function parseSprintArgs(
|
|
|
240
260
|
mergeWatchMode: tolerant
|
|
241
261
|
? tolerantMergeWatchMode(values['merge-watch-mode'])
|
|
242
262
|
: parseMergeWatchMode(values['merge-watch-mode']),
|
|
263
|
+
// Story #5266 — how many times close may re-run a failed ADVISORY run
|
|
264
|
+
// before blocking on it. `undefined` when absent, which is what lets the
|
|
265
|
+
// merge wait fall back to `delivery.ci.rerunAdvisory` (default 0).
|
|
266
|
+
// `parseNonNegativeInt` degrades a junk value to undefined rather than
|
|
267
|
+
// guessing an allowance that would spend the consumer's CI minutes.
|
|
268
|
+
rerunAdvisory: parseNonNegativeInt(values['rerun-advisory']),
|
|
243
269
|
// The operator's recorded reason for overriding a review
|
|
244
270
|
// blocker. `undefined` when the flag is absent, which is what keeps the
|
|
245
271
|
// blocker blocking by default.
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
|
|
11
11
|
import { _internals as baselineReaderInternals } from '../baselines/reader.js';
|
|
12
|
+
import { getChangedFiles } from '../changed-files.js';
|
|
12
13
|
import { getQuality } from '../config/quality.js';
|
|
14
|
+
import { filterFilesUnderTargets } from '../coverage-capture.js';
|
|
13
15
|
import { hasNpmScript, readPackageScripts } from '../npm-scripts.js';
|
|
14
16
|
import { KNOWN_KINDS } from '../orchestration/check-baselines/phases/parse-args.js';
|
|
15
17
|
import {
|
|
@@ -28,6 +30,11 @@ import {
|
|
|
28
30
|
* @property {string[]} args - Arguments passed to `cmd`.
|
|
29
31
|
* @property {string} [hint] - Remediation hint shown on failure.
|
|
30
32
|
* @property {{ baseRef: string }} [changedFileScope] - Optional Story-diff scope.
|
|
33
|
+
* @property {{ reason: string }} [skip] - Pre-decided skip (Story #5278). The
|
|
34
|
+
* runner records the gate as skipped with this reason and never spawns it.
|
|
35
|
+
* Used for the `coverage-capture` gate when the incremental-coverage skip
|
|
36
|
+
* is already known to fire, so the gate list can register a real `npm test`
|
|
37
|
+
* gate in its place instead of the close silently running no test gate.
|
|
31
38
|
* @property {Record<string, string>} [env] - Optional per-gate environment
|
|
32
39
|
* overlay. Merged over `process.env` for this gate's spawned child only.
|
|
33
40
|
* Used to thread the epic baseRef into the `check-baselines` gate via
|
|
@@ -112,12 +119,18 @@ function isCrapGateEnabled(config) {
|
|
|
112
119
|
* has NO working test gate at all. Splitting this out keeps
|
|
113
120
|
* `buildDefaultGates` flat for the CRAP-cyclomatic gate.
|
|
114
121
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
122
|
+
* Story #5278 adds a third way for coverage-capture to stop being the test
|
|
123
|
+
* runner: it is registered, but its own incremental-coverage skip is already
|
|
124
|
+
* known to fire (nothing changed under `crap.targetDirs`), so it will exit 0
|
|
125
|
+
* without running anything. A tests-only Story hits that on every close, and
|
|
126
|
+
* before #5278 the close then recorded a suite it never ran as `passed`.
|
|
127
|
+
*
|
|
128
|
+
* @param {boolean} coverageCaptureRunsSuite - Whether the coverage-capture
|
|
129
|
+
* gate will actually run the suite for this build.
|
|
117
130
|
* @returns {Gate[]}
|
|
118
131
|
*/
|
|
119
|
-
function buildTestGateEntry(
|
|
120
|
-
if (
|
|
132
|
+
function buildTestGateEntry(coverageCaptureRunsSuite) {
|
|
133
|
+
if (coverageCaptureRunsSuite) return [];
|
|
121
134
|
// Story #5173 — `fullSuiteLock` marks the one gate here that spawns a whole
|
|
122
135
|
// suite, so `defaultGateRunner` serializes it behind the host lock. It is
|
|
123
136
|
// set on this entry alone precisely because the two full-suite gates are
|
|
@@ -350,6 +363,78 @@ function splitCommand(commandString) {
|
|
|
350
363
|
return { cmd, args };
|
|
351
364
|
}
|
|
352
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Will the `coverage-capture` gate skip its own capture before running
|
|
368
|
+
* anything? (Story #5278.)
|
|
369
|
+
*
|
|
370
|
+
* `coverage-capture-incremental.js` exits 0 without a suite when no changed
|
|
371
|
+
* file lives under `crap.targetDirs` — the saving that makes incremental mode
|
|
372
|
+
* worth having. The gate list has to know that in advance, because the
|
|
373
|
+
* consequence is not "coverage-capture is cheap today" but "there is no test
|
|
374
|
+
* gate in this close at all": the plain `test` gate is dropped precisely
|
|
375
|
+
* because coverage-capture was going to carry test-failure signalling. A
|
|
376
|
+
* tests-only Story therefore closed green over a red suite.
|
|
377
|
+
*
|
|
378
|
+
* Predicting the skip is safe in one direction only, so every uncertainty
|
|
379
|
+
* resolves to `false` (coverage-capture runs, no extra `test` gate — the
|
|
380
|
+
* pre-#5278 shape): an unresolvable ref, a missing cwd, a git error, or the
|
|
381
|
+
* mode being off. A wrong `false` costs one redundant capture; a wrong `true`
|
|
382
|
+
* would register a `test` gate beside a coverage-capture that also runs the
|
|
383
|
+
* suite, which is the double-spend the credit economy exists to prevent.
|
|
384
|
+
*
|
|
385
|
+
* @param {{
|
|
386
|
+
* config?: object,
|
|
387
|
+
* cwd?: string,
|
|
388
|
+
* baseBranch?: string,
|
|
389
|
+
* getChangedFilesImpl?: typeof getChangedFiles,
|
|
390
|
+
* }} opts
|
|
391
|
+
* @returns {boolean}
|
|
392
|
+
*/
|
|
393
|
+
function predictsIncrementalCaptureSkip({
|
|
394
|
+
config,
|
|
395
|
+
cwd,
|
|
396
|
+
baseBranch,
|
|
397
|
+
getChangedFilesImpl = getChangedFiles,
|
|
398
|
+
}) {
|
|
399
|
+
// No cwd is the module-load `DEFAULT_GATES` case: never spawn git at import
|
|
400
|
+
// time just to answer a question that caller cannot act on.
|
|
401
|
+
if (typeof cwd !== 'string' || cwd.length === 0) return false;
|
|
402
|
+
const { crap } = getQuality(config);
|
|
403
|
+
if (crap?.incrementalCoverage?.skipWhenUnchanged !== true) return false;
|
|
404
|
+
const ref = crap.incrementalCoverage.baseRef || baseBranch;
|
|
405
|
+
if (typeof ref !== 'string' || ref.length === 0) return false;
|
|
406
|
+
try {
|
|
407
|
+
const changed = getChangedFilesImpl({ ref, cwd });
|
|
408
|
+
// Not an array is "the change set is unknown", not "the change set is
|
|
409
|
+
// empty" — and `filterFilesUnderTargets` flattens both to `[]`, so the
|
|
410
|
+
// shape has to be checked here or an unknown diff reads as a skip.
|
|
411
|
+
if (!Array.isArray(changed)) return false;
|
|
412
|
+
return filterFilesUnderTargets(changed, crap.targetDirs).length === 0;
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The `coverage-capture` gate's argv.
|
|
420
|
+
*
|
|
421
|
+
* Story #5278 — `--require-credited` is passed here, and only here, when the
|
|
422
|
+
* consumer has set `delivery.execution.requireCreditedCapture`. The CLI no
|
|
423
|
+
* longer reads that key, so the worker's pre-push deposit invocation always
|
|
424
|
+
* runs while the close gate refuses to pay for a suite the worker should
|
|
425
|
+
* already have banked.
|
|
426
|
+
*
|
|
427
|
+
* @param {object} [config]
|
|
428
|
+
* @returns {string[]}
|
|
429
|
+
*/
|
|
430
|
+
function buildCoverageCaptureArgs(config) {
|
|
431
|
+
const args = ['.agents/scripts/coverage-capture.js'];
|
|
432
|
+
if (config?.delivery?.execution?.requireCreditedCapture === true) {
|
|
433
|
+
args.push('--require-credited');
|
|
434
|
+
}
|
|
435
|
+
return args;
|
|
436
|
+
}
|
|
437
|
+
|
|
353
438
|
/**
|
|
354
439
|
* Build the canonical close-validation gate list.
|
|
355
440
|
*
|
|
@@ -402,7 +487,7 @@ function splitCommand(commandString) {
|
|
|
402
487
|
* none are required, the gate is skipped with a logged reason (via `log`)
|
|
403
488
|
* instead of a blocking failure.
|
|
404
489
|
*
|
|
405
|
-
* @param {{ config?: object, baseBranch?: string, cwd?: string, packageScripts?: Record<string, string>, presentBaselines?: string[]|Set<string>, log?: (message: string) => void }} [opts]
|
|
490
|
+
* @param {{ config?: object, baseBranch?: string, cwd?: string, packageScripts?: Record<string, string>, presentBaselines?: string[]|Set<string>, log?: (message: string) => void, getChangedFilesImpl?: typeof getChangedFiles }} [opts]
|
|
406
491
|
* `config` is the canonical resolved config (`{ project, delivery, ... }`);
|
|
407
492
|
* gate commands resolve from `project.commands` and the CRAP toggle from
|
|
408
493
|
* `delivery.quality.gates.crap.enabled`. `baseBranch` is the close run's
|
|
@@ -424,10 +509,28 @@ export function buildDefaultGates({
|
|
|
424
509
|
packageScripts,
|
|
425
510
|
presentBaselines,
|
|
426
511
|
log,
|
|
512
|
+
getChangedFilesImpl,
|
|
427
513
|
} = {}) {
|
|
428
514
|
const scripts = packageScripts ?? readPackageScripts(cwd);
|
|
429
515
|
const coverageCaptureActive =
|
|
430
516
|
isCrapGateEnabled(config) && hasNpmScript(scripts, 'test:coverage');
|
|
517
|
+
// Story #5278 — a registered coverage-capture gate that is going to take
|
|
518
|
+
// its own incremental skip is not the test runner for this close, so the
|
|
519
|
+
// plain `test` gate comes back beside it and the capture gate registers as
|
|
520
|
+
// a pre-decided skip rather than as a suite that silently did not run.
|
|
521
|
+
const captureSkipPredicted =
|
|
522
|
+
coverageCaptureActive &&
|
|
523
|
+
predictsIncrementalCaptureSkip({
|
|
524
|
+
config,
|
|
525
|
+
cwd,
|
|
526
|
+
baseBranch,
|
|
527
|
+
...(getChangedFilesImpl ? { getChangedFilesImpl } : {}),
|
|
528
|
+
});
|
|
529
|
+
if (captureSkipPredicted) {
|
|
530
|
+
log?.(
|
|
531
|
+
'[close-validation] coverage-capture will take the incremental skip (no changed file under the CRAP target dirs) — registering the plain `test` gate so this close still runs the suite.',
|
|
532
|
+
);
|
|
533
|
+
}
|
|
431
534
|
const typecheck = splitCommand(resolveTypecheckCommand(config));
|
|
432
535
|
const lint = splitCommand(resolveLintCommand(config));
|
|
433
536
|
const formatCheckString = resolveFormatCheckCommand(config);
|
|
@@ -460,7 +563,7 @@ export function buildDefaultGates({
|
|
|
460
563
|
// scoped pair does not shift the close-orchestrator log line, the
|
|
461
564
|
// evidence keyspace, or the parallel-partition membership below.
|
|
462
565
|
{ name: 'lint', cmd: lint.cmd, args: lint.args },
|
|
463
|
-
...buildTestGateEntry(coverageCaptureActive),
|
|
566
|
+
...buildTestGateEntry(coverageCaptureActive && !captureSkipPredicted),
|
|
464
567
|
{
|
|
465
568
|
// Gate name kept generic ("format") so the close-orchestrator log line
|
|
466
569
|
// doesn't shift when a repo swaps biome for Prettier / dprint via
|
|
@@ -479,8 +582,11 @@ export function buildDefaultGates({
|
|
|
479
582
|
{
|
|
480
583
|
name: 'coverage-capture',
|
|
481
584
|
cmd: 'node',
|
|
482
|
-
args:
|
|
585
|
+
args: buildCoverageCaptureArgs(config),
|
|
483
586
|
hint: 'Coverage capture failed — `npm run test:coverage` exited non-zero. Fix failing tests or coverage-threshold breaches, then re-run close.',
|
|
587
|
+
...(captureSkipPredicted
|
|
588
|
+
? { skip: { reason: 'incremental-no-crap-changes' } }
|
|
589
|
+
: {}),
|
|
484
590
|
},
|
|
485
591
|
]
|
|
486
592
|
: []),
|
|
@@ -153,15 +153,19 @@ function isBiomeNoFilesProcessed(output) {
|
|
|
153
153
|
*
|
|
154
154
|
* @param {string} cmd
|
|
155
155
|
* @param {string[]} args
|
|
156
|
-
* @param {{ cwd: string, signal?: AbortSignal, gateName?: string, log?: (m: string) => void, env?: Record<string, string>, tolerateNoFilesProcessed?: boolean, fullSuiteLock?: boolean }} opts
|
|
156
|
+
* @param {{ cwd: string, signal?: AbortSignal, gateName?: string, log?: (m: string) => void, env?: Record<string, string>, tolerateNoFilesProcessed?: boolean, fullSuiteLock?: boolean, skipIfSatisfied?: () => {status: number}|undefined }} opts
|
|
157
157
|
* @returns {Promise<{ status: number }>}
|
|
158
158
|
*/
|
|
159
159
|
export function defaultGateRunner(cmd, args, opts = {}) {
|
|
160
160
|
if (!opts.fullSuiteLock) return spawnGate(cmd, args, opts);
|
|
161
161
|
// `log` is passed through as-is: `withFullSuiteLockAsync` supplies its own
|
|
162
162
|
// no-op default, so a second fallback here would be an untestable branch.
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
// `skipIfSatisfied` (Story #5278) is the caller's post-wait re-probe: after
|
|
164
|
+
// queueing behind another full suite, the gate re-asks whether its evidence
|
|
165
|
+
// has since been deposited and returns that verdict instead of spawning.
|
|
166
|
+
return withFullSuiteLockAsync(
|
|
167
|
+
{ cwd: opts.cwd, log: opts.log, skipIfSatisfied: opts.skipIfSatisfied },
|
|
168
|
+
() => spawnGate(cmd, args, opts),
|
|
165
169
|
);
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* surfaces actionable hints on failure.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { gitSpawn } from '../git-utils.js';
|
|
11
12
|
import {
|
|
12
13
|
recordPass as defaultRecordPass,
|
|
13
14
|
shouldSkip as defaultShouldSkip,
|
|
15
|
+
treeFingerprint as defaultTreeFingerprint,
|
|
14
16
|
hashCommandConfig,
|
|
15
17
|
} from '../validation-evidence.js';
|
|
16
18
|
import {
|
|
@@ -25,6 +27,20 @@ import { defaultGetHeadSha } from './projections/head-sha.js';
|
|
|
25
27
|
/** @typedef {import('./gates.js').Gate} Gate */
|
|
26
28
|
|
|
27
29
|
function applyChangedFileScope({ gate, spawnCwd, log }) {
|
|
30
|
+
// Story #5278 — a skip the gate list already decided (the coverage-capture
|
|
31
|
+
// gate whose incremental skip is known to fire). Honoured before anything
|
|
32
|
+
// else so the gate is recorded as `skipped` with its real reason rather
|
|
33
|
+
// than spawned to discover the same thing minutes later.
|
|
34
|
+
if (gate.skip) {
|
|
35
|
+
log(`[close-validation] ⏭ ${gate.name} skipped (${gate.skip.reason})`);
|
|
36
|
+
return {
|
|
37
|
+
gate,
|
|
38
|
+
cmd: gate.cmd,
|
|
39
|
+
args: gate.args,
|
|
40
|
+
skip: true,
|
|
41
|
+
skipReason: gate.skip.reason,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
28
44
|
if (!gate.changedFileScope) {
|
|
29
45
|
return { gate, cmd: gate.cmd, args: gate.args, skip: false };
|
|
30
46
|
}
|
|
@@ -116,6 +132,7 @@ function applyChangedFileScope({ gate, spawnCwd, log }) {
|
|
|
116
132
|
* useEvidence?: boolean,
|
|
117
133
|
* evidenceClock?: () => number,
|
|
118
134
|
* getHeadSha?: (cwd: string) => string|null,
|
|
135
|
+
* getTreeFingerprint?: (cwd: string) => string|null,
|
|
119
136
|
* recordPass?: typeof defaultRecordPass,
|
|
120
137
|
* shouldSkip?: typeof defaultShouldSkip,
|
|
121
138
|
* }} opts
|
|
@@ -137,6 +154,8 @@ export async function runCloseValidation({
|
|
|
137
154
|
useEvidence = true,
|
|
138
155
|
evidenceClock = () => Date.now(),
|
|
139
156
|
getHeadSha = (resolvedCwd) => defaultGetHeadSha(resolvedCwd),
|
|
157
|
+
getTreeFingerprint = (resolvedCwd) =>
|
|
158
|
+
defaultTreeFingerprint(resolvedCwd, gitSpawn),
|
|
140
159
|
recordPass = defaultRecordPass,
|
|
141
160
|
shouldSkip = defaultShouldSkip,
|
|
142
161
|
} = {}) {
|
|
@@ -153,6 +172,13 @@ export async function runCloseValidation({
|
|
|
153
172
|
// Story #1120.
|
|
154
173
|
const spawnCwd = worktreePath ?? cwd;
|
|
155
174
|
const headSha = evidenceActive ? getHeadSha(spawnCwd) : null;
|
|
175
|
+
// Story #5278 — one tree fingerprint for the whole run, not one per gate.
|
|
176
|
+
// Every gate here reads the same working tree, so an identical tree means
|
|
177
|
+
// identical inputs for all of them; a per-gate scope would be narrower but
|
|
178
|
+
// would have to model each gate's read set, and being wrong about that
|
|
179
|
+
// grants a skip the gate did not earn. A gate that carries its own
|
|
180
|
+
// `inputFingerprint` still wins.
|
|
181
|
+
const treeSha = evidenceActive ? getTreeFingerprint(spawnCwd) : null;
|
|
156
182
|
|
|
157
183
|
// Helper closures so the parallel and serial passes share evidence
|
|
158
184
|
// bookkeeping bit-for-bit.
|
|
@@ -166,7 +192,7 @@ export async function runCloseValidation({
|
|
|
166
192
|
gateName: gate.name,
|
|
167
193
|
currentSha: headSha,
|
|
168
194
|
configHash,
|
|
169
|
-
inputFingerprint: gate.inputFingerprint ??
|
|
195
|
+
inputFingerprint: gate.inputFingerprint ?? treeSha,
|
|
170
196
|
},
|
|
171
197
|
evidenceStoreOpts,
|
|
172
198
|
);
|
|
@@ -192,7 +218,7 @@ export async function runCloseValidation({
|
|
|
192
218
|
configHash,
|
|
193
219
|
exitCode: 0,
|
|
194
220
|
durationMs,
|
|
195
|
-
inputFingerprint: gate.inputFingerprint ??
|
|
221
|
+
inputFingerprint: gate.inputFingerprint ?? treeSha,
|
|
196
222
|
},
|
|
197
223
|
evidenceStoreOpts,
|
|
198
224
|
);
|
|
@@ -214,7 +240,7 @@ export async function runCloseValidation({
|
|
|
214
240
|
*
|
|
215
241
|
* @returns {Promise<{ status: number }>}
|
|
216
242
|
*/
|
|
217
|
-
const dispatchGate = async (gate, signal) => {
|
|
243
|
+
const dispatchGate = async (gate, signal, configHash) => {
|
|
218
244
|
log(
|
|
219
245
|
`[close-validation] ▶ ${gate.name}${worktreePath ? ` (cwd=${worktreePath})` : ''}`,
|
|
220
246
|
);
|
|
@@ -226,6 +252,20 @@ export async function runCloseValidation({
|
|
|
226
252
|
log,
|
|
227
253
|
signal,
|
|
228
254
|
...(gate.env ? { env: gate.env } : {}),
|
|
255
|
+
// Story #5278 — only the full-suite gate can end up *waiting* on the
|
|
256
|
+
// host lock, and only it is expensive enough for the wait to change the
|
|
257
|
+
// answer: whoever we queued behind may have deposited this gate's
|
|
258
|
+
// evidence while we sat there. Re-asking the same question the runner
|
|
259
|
+
// asked before the wait is the whole mechanism; a `{ status: 0 }`
|
|
260
|
+
// stands in for the spawn.
|
|
261
|
+
...(gate.fullSuiteLock && configHash
|
|
262
|
+
? {
|
|
263
|
+
skipIfSatisfied: () =>
|
|
264
|
+
evidenceVerdict(gate, configHash).skip
|
|
265
|
+
? { status: 0 }
|
|
266
|
+
: undefined,
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
229
269
|
// Story #5173 — forwarded unconditionally (never a conditional spread
|
|
230
270
|
// like the two below): `defaultGateRunner` already treats a falsy value
|
|
231
271
|
// as "no lock", so a branch here would only add a decision point to the
|
|
@@ -263,7 +303,10 @@ export async function runCloseValidation({
|
|
|
263
303
|
return;
|
|
264
304
|
}
|
|
265
305
|
if (execution.skip) {
|
|
266
|
-
skipped.push({
|
|
306
|
+
skipped.push({
|
|
307
|
+
gate,
|
|
308
|
+
reason: execution.skipReason ?? 'no-changed-files',
|
|
309
|
+
});
|
|
267
310
|
return;
|
|
268
311
|
}
|
|
269
312
|
const configHash = hashCommandConfig({
|
|
@@ -287,6 +330,7 @@ export async function runCloseValidation({
|
|
|
287
330
|
tolerateNoFilesProcessed: execution.tolerateNoFilesProcessed,
|
|
288
331
|
},
|
|
289
332
|
ac.signal,
|
|
333
|
+
configHash,
|
|
290
334
|
);
|
|
291
335
|
} catch (err) {
|
|
292
336
|
result = { status: 1, error: err };
|
|
@@ -395,7 +439,10 @@ async function runSerialGates(
|
|
|
395
439
|
return;
|
|
396
440
|
}
|
|
397
441
|
if (execution.skip) {
|
|
398
|
-
skipped.push({
|
|
442
|
+
skipped.push({
|
|
443
|
+
gate,
|
|
444
|
+
reason: execution.skipReason ?? 'no-changed-files',
|
|
445
|
+
});
|
|
399
446
|
continue;
|
|
400
447
|
}
|
|
401
448
|
const configHash = hashCommandConfig({
|
|
@@ -409,12 +456,16 @@ async function runSerialGates(
|
|
|
409
456
|
continue;
|
|
410
457
|
}
|
|
411
458
|
const startedAt = evidenceActive ? evidenceClock() : 0;
|
|
412
|
-
const result = await dispatchGate(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
459
|
+
const result = await dispatchGate(
|
|
460
|
+
{
|
|
461
|
+
...gate,
|
|
462
|
+
cmd: execution.cmd,
|
|
463
|
+
args: execution.args,
|
|
464
|
+
tolerateNoFilesProcessed: execution.tolerateNoFilesProcessed,
|
|
465
|
+
},
|
|
466
|
+
undefined,
|
|
467
|
+
configHash,
|
|
468
|
+
);
|
|
418
469
|
if (result.status !== 0) {
|
|
419
470
|
failGate(
|
|
420
471
|
gate,
|