mandrel 2.36.0 → 2.37.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/docs/configuration.md +25 -0
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/drain-pending-cleanup.js +18 -1
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/cli-args.js +0 -2
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +46 -6
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/tickets.js +109 -16
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +0 -2
- package/.agents/scripts/single-story-init.js +5 -1
- package/docs/CHANGELOG.md +32 -0
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +7 -3
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- package/.agents/scripts/lib/orchestration/recut.js +0 -56
|
@@ -292,7 +292,9 @@ export function computeExitCode(merged, miExit, crapExit) {
|
|
|
292
292
|
*
|
|
293
293
|
* The last header used to hardcode `c=8`, which quietly lied to any consumer
|
|
294
294
|
* that had tuned `codingGuardrails.cyclomaticFlag`. It now names the value the
|
|
295
|
-
* count was actually taken against, read off the merge result
|
|
295
|
+
* count was actually taken against, read off the merge result — through the
|
|
296
|
+
* same `normalizeFlag` the merge itself uses, rather than the second, hand-
|
|
297
|
+
* inlined copy of that coercion this function used to carry.
|
|
296
298
|
*
|
|
297
299
|
* Pure — accepts pre-computed merge rows and returns a multi-line string. The
|
|
298
300
|
* table renders even on a clean diff so operators see the "no drift" signal.
|
|
@@ -300,33 +302,40 @@ export function computeExitCode(merged, miExit, crapExit) {
|
|
|
300
302
|
* @param {{ rows: Array<{ file: string, miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>, totals: { miRegressions: number, crapViolations: number }, cyclomaticFlag?: number }} merged
|
|
301
303
|
* @returns {string}
|
|
302
304
|
*/
|
|
305
|
+
/**
|
|
306
|
+
* The table's body: one row per regressed file, or the single placeholder
|
|
307
|
+
* line that keeps the "no drift" signal visible on a clean diff.
|
|
308
|
+
*
|
|
309
|
+
* Split out of `renderTable` so that function keeps a flat shape — the
|
|
310
|
+
* empty-vs-populated branch and the row loop together carried it above the
|
|
311
|
+
* per-method CRAP contract the pre-push preview enforces.
|
|
312
|
+
*
|
|
313
|
+
* @param {Array<{ file: string, miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>} rows
|
|
314
|
+
* @returns {string[]}
|
|
315
|
+
*/
|
|
316
|
+
function tableBodyLines(rows) {
|
|
317
|
+
if (rows.length === 0) return ['| _(no per-file regressions)_ | — | — | — |'];
|
|
318
|
+
return rows.map(
|
|
319
|
+
(row) =>
|
|
320
|
+
`| ${row.file} | -${row.miDrop.toFixed(2)} | +${row.worstCrapDelta.toFixed(2)} | ${row.newOverCeilingMethods} |`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
303
324
|
export function renderTable(merged) {
|
|
304
|
-
const flag =
|
|
305
|
-
? Number(merged.cyclomaticFlag)
|
|
306
|
-
: DEFAULT_CYCLOMATIC_FLAG;
|
|
325
|
+
const flag = normalizeFlag(merged?.cyclomaticFlag);
|
|
307
326
|
const header = [
|
|
308
327
|
'file',
|
|
309
328
|
'MI delta',
|
|
310
329
|
'worst CRAP delta',
|
|
311
330
|
`new-method count over c=${flag}`,
|
|
312
331
|
];
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
} else {
|
|
319
|
-
for (const row of merged.rows) {
|
|
320
|
-
lines.push(
|
|
321
|
-
`| ${row.file} | -${row.miDrop.toFixed(2)} | +${row.worstCrapDelta.toFixed(2)} | ${row.newOverCeilingMethods} |`,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
lines.push('');
|
|
326
|
-
lines.push(
|
|
332
|
+
return [
|
|
333
|
+
`| ${header.join(' | ')} |`,
|
|
334
|
+
`| ${header.map(() => '---').join(' | ')} |`,
|
|
335
|
+
...tableBodyLines(merged.rows),
|
|
336
|
+
'',
|
|
327
337
|
`Totals: MI regressions=${merged.totals.miRegressions} · CRAP violations=${merged.totals.crapViolations}`,
|
|
328
|
-
);
|
|
329
|
-
return lines.join('\n');
|
|
338
|
+
].join('\n');
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
/**
|
|
@@ -354,6 +363,96 @@ function resolveCyclomaticFlag({ cwd, stderr }) {
|
|
|
354
363
|
}
|
|
355
364
|
}
|
|
356
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Invoke one preview runner, degrading a thrown failure into the same
|
|
368
|
+
* `{ exitCode: 1, envelope: null }` shape a real gate failure produces.
|
|
369
|
+
*
|
|
370
|
+
* Both runners degraded identically before, in two hand-copied `catch`
|
|
371
|
+
* arms; folding them into one helper removes the copy and keeps `runCli` a
|
|
372
|
+
* pipeline rather than a pair of inlined error handlers. The emitted message
|
|
373
|
+
* is byte-identical to the arm it replaces — `label` supplies the `MI` /
|
|
374
|
+
* `CRAP` prefix.
|
|
375
|
+
*
|
|
376
|
+
* @param {(args: object) => Promise<{exitCode: number, envelope: object|null}>} runner
|
|
377
|
+
* @param {{cwd: string, staged: boolean, changedSinceRef: string|null}} args
|
|
378
|
+
* @param {'MI'|'CRAP'} label
|
|
379
|
+
* @param {{ write: (s: string) => void }} stderr
|
|
380
|
+
* @returns {Promise<{exitCode: number, envelope: object|null}>}
|
|
381
|
+
*/
|
|
382
|
+
function runGateSafely(runner, args, label, stderr) {
|
|
383
|
+
return runner(args).catch((err) => {
|
|
384
|
+
stderr.write(
|
|
385
|
+
`[quality:preview] ${label} runner failed: ${err?.message ?? err}\n`,
|
|
386
|
+
);
|
|
387
|
+
return { exitCode: 1, envelope: null };
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Write the run's report — the `--json` envelope, or the human-readable
|
|
393
|
+
* table plus any gate diagnostics and the non-zero-exit summary.
|
|
394
|
+
*
|
|
395
|
+
* Split out of `runCli` (Story #5109): the rendering half carried five of
|
|
396
|
+
* that function's decision points, which put it over the per-method CRAP
|
|
397
|
+
* contract the pre-push preview enforces. Output bytes are unchanged in both
|
|
398
|
+
* modes. The exit code stays with the caller — this function only reports.
|
|
399
|
+
*
|
|
400
|
+
* @param {{
|
|
401
|
+
* json: boolean,
|
|
402
|
+
* staged: boolean,
|
|
403
|
+
* ref: string|null,
|
|
404
|
+
* miResult: {exitCode: number, envelope: object|null},
|
|
405
|
+
* crapResult: {exitCode: number, envelope: object|null},
|
|
406
|
+
* merged: ReturnType<typeof mergeEnvelopes>,
|
|
407
|
+
* stdout: { write: (s: string) => void },
|
|
408
|
+
* stderr: { write: (s: string) => void },
|
|
409
|
+
* }} args
|
|
410
|
+
* @returns {void}
|
|
411
|
+
*/
|
|
412
|
+
function emitReport({
|
|
413
|
+
json,
|
|
414
|
+
staged,
|
|
415
|
+
ref,
|
|
416
|
+
miResult,
|
|
417
|
+
crapResult,
|
|
418
|
+
merged,
|
|
419
|
+
stdout,
|
|
420
|
+
stderr,
|
|
421
|
+
}) {
|
|
422
|
+
const miExit = miResult.exitCode;
|
|
423
|
+
const crapExit = crapResult.exitCode;
|
|
424
|
+
if (json) {
|
|
425
|
+
stdout.write(
|
|
426
|
+
`${JSON.stringify(
|
|
427
|
+
{
|
|
428
|
+
ref: staged ? null : ref,
|
|
429
|
+
staged,
|
|
430
|
+
mi: { exit: miExit, envelope: miResult.envelope },
|
|
431
|
+
crap: { exit: crapExit, envelope: crapResult.envelope },
|
|
432
|
+
merged,
|
|
433
|
+
},
|
|
434
|
+
null,
|
|
435
|
+
2,
|
|
436
|
+
)}\n`,
|
|
437
|
+
);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
stdout.write('\n--- quality:preview ---\n');
|
|
441
|
+
stdout.write(
|
|
442
|
+
staged
|
|
443
|
+
? 'scope=staged (git diff --cached)\n\n'
|
|
444
|
+
: `scope=diff ref=${ref}\n\n`,
|
|
445
|
+
);
|
|
446
|
+
stdout.write(`${renderTable(merged)}\n`);
|
|
447
|
+
const diagnostics = renderDiagnostics([miResult, crapResult]);
|
|
448
|
+
if (diagnostics) stdout.write(`\n${diagnostics}\n`);
|
|
449
|
+
if (miExit !== 0 || crapExit !== 0) {
|
|
450
|
+
stderr.write(
|
|
451
|
+
`\n[quality:preview] gate exits: mi=${miExit} crap=${crapExit}\n`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
357
456
|
/**
|
|
358
457
|
* Top-level CLI entry: invoke both per-kind preview runners, merge, render,
|
|
359
458
|
* and exit with the right code. Exposed as `runCli` so tests can drive the
|
|
@@ -381,62 +480,55 @@ export async function runCli({
|
|
|
381
480
|
const staged = parseStagedFlag(argv);
|
|
382
481
|
const ref = staged ? null : (parseChangedSinceArg(argv) ?? 'HEAD');
|
|
383
482
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
483
|
+
// Story #5109 — the two gates run **one after the other**, not under a
|
|
484
|
+
// `Promise.all`. Each scores its batch with its own `runOnPool` budget
|
|
485
|
+
// sized to `os.availableParallelism()`, so overlapping them oversubscribed
|
|
486
|
+
// the host by 2x and stacked two escomplex heaps: a 58-file preview peaked
|
|
487
|
+
// at 1.0-1.2 GB RSS for 3.9 s of CPU. Serialising them bounds the preview
|
|
488
|
+
// to one `availableParallelism` of workers and one heap at a time. The two
|
|
489
|
+
// runners share no state and neither reads the other's envelope, so the
|
|
490
|
+
// emitted envelopes — and therefore the merged table and the exit code —
|
|
491
|
+
// are identical either way; only the peak cost differs.
|
|
492
|
+
//
|
|
493
|
+
// Each runner gets its own options literal rather than one shared object,
|
|
494
|
+
// so serialising them cannot introduce a coupling the concurrent form
|
|
495
|
+
// did not have.
|
|
496
|
+
const miResult = await runGateSafely(
|
|
497
|
+
runMi,
|
|
498
|
+
{ cwd, staged, changedSinceRef: ref },
|
|
499
|
+
'MI',
|
|
500
|
+
stderr,
|
|
501
|
+
);
|
|
502
|
+
const crapResult = await runGateSafely(
|
|
503
|
+
runCrap,
|
|
504
|
+
{ cwd, staged, changedSinceRef: ref },
|
|
505
|
+
'CRAP',
|
|
506
|
+
stderr,
|
|
507
|
+
);
|
|
508
|
+
|
|
402
509
|
// Story #4923 — the over-ceiling column counts against the *resolved*
|
|
403
510
|
// `codingGuardrails.cyclomaticFlag`, not the literal that used to be written
|
|
404
511
|
// into `mergeEnvelopes` and the column header.
|
|
405
512
|
const cyclomaticFlag = resolveCyclomaticFlag({ cwd, stderr });
|
|
406
|
-
const merged = mergeEnvelopes(
|
|
513
|
+
const merged = mergeEnvelopes(miResult.envelope, crapResult.envelope, {
|
|
514
|
+
cyclomaticFlag,
|
|
515
|
+
});
|
|
407
516
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
null,
|
|
419
|
-
2,
|
|
420
|
-
)}\n`,
|
|
421
|
-
);
|
|
422
|
-
} else {
|
|
423
|
-
stdout.write('\n--- quality:preview ---\n');
|
|
424
|
-
stdout.write(
|
|
425
|
-
staged
|
|
426
|
-
? 'scope=staged (git diff --cached)\n\n'
|
|
427
|
-
: `scope=diff ref=${ref}\n\n`,
|
|
428
|
-
);
|
|
429
|
-
stdout.write(`${renderTable(merged)}\n`);
|
|
430
|
-
const diagnostics = renderDiagnostics([miResult, crapResult]);
|
|
431
|
-
if (diagnostics) stdout.write(`\n${diagnostics}\n`);
|
|
432
|
-
if (miExit !== 0 || crapExit !== 0) {
|
|
433
|
-
stderr.write(
|
|
434
|
-
`\n[quality:preview] gate exits: mi=${miExit} crap=${crapExit}\n`,
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
517
|
+
emitReport({
|
|
518
|
+
json,
|
|
519
|
+
staged,
|
|
520
|
+
ref,
|
|
521
|
+
miResult,
|
|
522
|
+
crapResult,
|
|
523
|
+
merged,
|
|
524
|
+
stdout,
|
|
525
|
+
stderr,
|
|
526
|
+
});
|
|
438
527
|
|
|
439
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
exitCode: computeExitCode(merged, miResult.exitCode, crapResult.exitCode),
|
|
530
|
+
merged,
|
|
531
|
+
};
|
|
440
532
|
}
|
|
441
533
|
|
|
442
534
|
// cli-opt-out: Windows-aware main-guard with leading-slash drive-letter normalisation; mirrors quality-watch.js so the diagnostic surface stays consistent across the gate suite.
|
|
@@ -69,11 +69,14 @@ export function runTestProfile({
|
|
|
69
69
|
const { outDir, topN, testArgv } = parseProfileArgv(argv);
|
|
70
70
|
fsLike.mkdirSync(outDir, { recursive: true });
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// The reporter is a *builder option*, not a suffix. Appended after the
|
|
73
|
+
// file targets it was two more positionals to Node, the default reporter
|
|
74
|
+
// ran, and every profile parsed `Timed entries parsed: 0` off a full
|
|
75
|
+
// suite run. `buildNodeTestArgs` places it in flag position.
|
|
76
|
+
const nodeArgs = buildNodeTestArgs({
|
|
77
|
+
extraArgs: testArgv,
|
|
78
|
+
reporter: 'tap',
|
|
79
|
+
});
|
|
77
80
|
|
|
78
81
|
const started = Date.now();
|
|
79
82
|
const result = spawnChild(process.execPath, nodeArgs, {
|
|
@@ -16,16 +16,18 @@
|
|
|
16
16
|
* to opt back in for the rare case where a contract test deliberately
|
|
17
17
|
* exercises a sandbox endpoint.
|
|
18
18
|
*
|
|
19
|
-
* Windows arg-length safety:
|
|
20
|
-
*
|
|
21
|
-
* glob will not do)
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
19
|
+
* Windows arg-length safety: every tier enumerates explicit test-file
|
|
20
|
+
* targets. `quick` / `integration` always did (they exclude specific slow
|
|
21
|
+
* suites, so a single glob will not do), and `full` joined them in Story
|
|
22
|
+
* #5111 — `node --test` has no negative pattern, so "everything except
|
|
23
|
+
* `tests/e2e/**`" is only sayable as a file set. With ~700+ targets the
|
|
24
|
+
* joined command line crosses the Windows `CreateProcess` ~32 767-char
|
|
25
|
+
* `lpCommandLine` ceiling, and `spawnSync` throws `ENAMETOOLONG` before a
|
|
26
|
+
* single test runs. To stay safe on every platform the runner partitions the
|
|
27
|
+
* targets into chunks whose joined length stays well under that ceiling
|
|
28
|
+
* (`MAX_TARGET_CHARS`) and spawns one `node --test` process per chunk,
|
|
29
|
+
* aggregating the exit codes. On POSIX the far larger
|
|
30
|
+
* `POSIX_MAX_TARGET_CHARS` budget collapses every tier back into one spawn.
|
|
29
31
|
*/
|
|
30
32
|
|
|
31
33
|
import { spawnSync } from 'node:child_process';
|
|
@@ -126,18 +128,38 @@ export function chunkTestTargets(targets, maxChars = MAX_TARGET_CHARS) {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
/**
|
|
131
|
+
* Build the argv for one `node --test` invocation.
|
|
132
|
+
*
|
|
133
|
+
* **Flag position is load-bearing.** Node stops treating tokens as options
|
|
134
|
+
* at the first positional, so every runner flag — the fixed
|
|
135
|
+
* `TEST_RUNNER_FLAGS`, the optional `--test-reporter`, and any caller
|
|
136
|
+
* `extraArgs` — has to sit *before* the test-file targets. Appending
|
|
137
|
+
* `--test-reporter tap` after the targets (what `run-test-profile.js` used
|
|
138
|
+
* to do) made Node read the two tokens as two more file patterns: the
|
|
139
|
+
* default reporter ran, no TAP was emitted, and the profiler parsed zero
|
|
140
|
+
* timed entries out of a full suite run. `reporter` is therefore an option
|
|
141
|
+
* of this builder rather than something a caller concatenates on the end.
|
|
142
|
+
*
|
|
129
143
|
* @param {object} [opts]
|
|
130
|
-
* @param {string[]} [opts.extraArgs]
|
|
144
|
+
* @param {string[]} [opts.extraArgs] Extra runner arguments (flag position).
|
|
131
145
|
* @param {'full' | 'quick' | 'integration'} [opts.tier]
|
|
132
146
|
* @param {string} [opts.repoRoot]
|
|
147
|
+
* @param {string} [opts.reporter] `--test-reporter` value, e.g. `'tap'`.
|
|
148
|
+
* @returns {string[]}
|
|
133
149
|
*/
|
|
134
150
|
export function buildNodeTestArgs({
|
|
135
151
|
extraArgs = [],
|
|
136
152
|
tier = 'full',
|
|
137
153
|
repoRoot = ROOT,
|
|
154
|
+
reporter,
|
|
138
155
|
} = {}) {
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
return [
|
|
157
|
+
...(reporter
|
|
158
|
+
? [...TEST_RUNNER_FLAGS, '--test-reporter', reporter]
|
|
159
|
+
: TEST_RUNNER_FLAGS),
|
|
160
|
+
...extraArgs,
|
|
161
|
+
...listTestFilesForTier(tier, repoRoot),
|
|
162
|
+
];
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
export function runTestSuite({
|
|
@@ -169,7 +191,13 @@ export function runTestSuite({
|
|
|
169
191
|
for (const chunk of chunks) {
|
|
170
192
|
const testRun = spawn(
|
|
171
193
|
process.execPath,
|
|
172
|
-
|
|
194
|
+
// `rest` carries the documented `node --test` pass-throughs
|
|
195
|
+
// (`--test-name-pattern`, `--test-only`) — `parseTierArgv` has already
|
|
196
|
+
// rejected anything else. They precede the targets for the same reason
|
|
197
|
+
// `buildNodeTestArgs` orders them that way: Node stops parsing options
|
|
198
|
+
// at the first positional, so a flag after a target is silently read as
|
|
199
|
+
// another file pattern.
|
|
200
|
+
[...TEST_RUNNER_FLAGS, ...rest, ...chunk],
|
|
173
201
|
{ cwd, stdio: 'inherit', env },
|
|
174
202
|
);
|
|
175
203
|
if (testRun.error) {
|
|
@@ -199,7 +227,44 @@ export function runTestSuite({
|
|
|
199
227
|
return status;
|
|
200
228
|
}
|
|
201
229
|
|
|
230
|
+
/**
|
|
231
|
+
* `--help` contract for the runner — the pre-rendered shape
|
|
232
|
+
* `lib/cli-usage.js` accepts alongside a spec object, matching how
|
|
233
|
+
* `check-baselines.js` declares its own.
|
|
234
|
+
*
|
|
235
|
+
* Handed to `runAsCli`, which prints it and returns **before** `main` runs.
|
|
236
|
+
* That ordering is the whole point: with no `usage` the flag fell through to
|
|
237
|
+
* `runTestSuite`, which ran the tier preflight, spawned the full ~10 000-test
|
|
238
|
+
* suite, and swept `temp/` — 25+ seconds and a mutated working tree in
|
|
239
|
+
* answer to a question about flags.
|
|
240
|
+
*
|
|
241
|
+
* Exported so the unit test can assert the documented surface without
|
|
242
|
+
* spawning the CLI.
|
|
243
|
+
*/
|
|
244
|
+
export const USAGE = `Usage: node .agents/scripts/run-tests.js [--tier <full|quick|integration|e2e>] [runner args...]
|
|
245
|
+
|
|
246
|
+
Run the test suite: tier preflight, \`node --test\` over the tier's targets,
|
|
247
|
+
then reserved-temp cleanup — which runs even when the suite fails.
|
|
248
|
+
|
|
249
|
+
Flags:
|
|
250
|
+
--tier <full|quick|integration|e2e>
|
|
251
|
+
Tier to run. Default: full — every test file except
|
|
252
|
+
\`tests/e2e/**\`, which only \`--tier e2e\` runs.
|
|
253
|
+
--test-name-pattern <re>, --test-only
|
|
254
|
+
The documented \`node --test\` pass-throughs, forwarded
|
|
255
|
+
verbatim in flag position, ahead of the file targets.
|
|
256
|
+
Any other \`--flag\` is rejected rather than forwarded:
|
|
257
|
+
\`node --test\` reads an unrecognized flag as another
|
|
258
|
+
file pattern, so forwarding one ran nothing and
|
|
259
|
+
still exited 0.
|
|
260
|
+
--help Show this message.
|
|
261
|
+
|
|
262
|
+
Exits with the first non-zero \`node --test\` chunk status, or 2 when the tier
|
|
263
|
+
preflight refuses to start the suite.
|
|
264
|
+
`;
|
|
265
|
+
|
|
202
266
|
runAsCli(import.meta.url, async () => runTestSuite(), {
|
|
203
267
|
source: 'run-tests',
|
|
204
268
|
propagateExitCode: true,
|
|
269
|
+
usage: USAGE,
|
|
205
270
|
});
|
|
@@ -237,8 +237,6 @@ runAsCli(import.meta.url, main, {
|
|
|
237
237
|
// the point of this flag is that arming stays on the one code path.
|
|
238
238
|
'Land despite a Story-scope code-review CRITICAL blocker you have reviewed and judged wrong. The reason is mandatory (≥12 chars) and is recorded on the Story, on the PR, and as a `review-block-overridden` friction signal; the terminal envelope reports `gates.codeReview: "overridden"`. Use this instead of merging the PR by hand with the GitHub CLI — a hand-merge bypasses the gate with no record at all.',
|
|
239
239
|
],
|
|
240
|
-
['--no-evidence', 'Do not reuse or write gate evidence stamps.'],
|
|
241
|
-
['--dry-run', 'Report the plan; mutate nothing.'],
|
|
242
240
|
],
|
|
243
241
|
notes: [
|
|
244
242
|
'Exit codes:\n 0 landed\n 1 blocked or failed\n 3 pending (resumable — run the envelope’s nextCommand)',
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from './lib/orchestration/ticketing.js';
|
|
74
74
|
import { createProvider } from './lib/provider-factory.js';
|
|
75
75
|
import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
|
|
76
|
+
import { resolveSweepLockPath } from './lib/single-story-sweep/sweep-lock.js';
|
|
76
77
|
// `sweepMergedStoryBranches` is imported dynamically below — its transitive
|
|
77
78
|
// graph reaches `picomatch` (via `git-cleanup.js`). Loading it statically
|
|
78
79
|
// would crash module resolution before `assertDepsInstalled()` can emit a
|
|
@@ -292,6 +293,9 @@ export function decideStoryBranchSeed({ localHas, remoteHas }) {
|
|
|
292
293
|
* in `sweep.protected` for the operator).
|
|
293
294
|
* - Cross-session lock: a single lockfile under `tempRoot` prevents
|
|
294
295
|
* two concurrent `/single-story-deliver` invocations from racing.
|
|
296
|
+
* Story #5112 made that lockfile shared with `boot-sweep.js` — the
|
|
297
|
+
* other entry point into the same merged-branch reap — by routing
|
|
298
|
+
* both through `resolveSweepLockPath`.
|
|
295
299
|
*
|
|
296
300
|
* Exported for testing.
|
|
297
301
|
*/
|
|
@@ -307,7 +311,7 @@ export async function reapMergedStoryBranches({
|
|
|
307
311
|
injectedSweep ??
|
|
308
312
|
(await import('./lib/single-story-sweep.js')).sweepMergedStoryBranches;
|
|
309
313
|
const tempRoot = config?.project?.paths?.tempRoot ?? 'temp';
|
|
310
|
-
const lockPath =
|
|
314
|
+
const lockPath = resolveSweepLockPath({ cwd, tempRoot });
|
|
311
315
|
const lockTimeoutMs =
|
|
312
316
|
config.delivery?.worktreeIsolation?.sweepLockMs ?? 60_000;
|
|
313
317
|
try {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,38 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.37.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.36.0...mandrel-v2.37.0) (2026-09-03)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### ⚠ BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* `single-story-close.js` now exits non-zero on `--dry-run` or `--no-evidence` instead of silently ignoring them. Neither flag ever had an effect; a caller passing one was getting a full real close.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
* cut the test suite's child-process budget: seed the e2e consumer once and move tests/e2e to its own tier, replace CLI cold-start spawns with in-process calls, and settle the isolation trade-off ([#5111](https://github.com/dsj1984/mandrel/issues/5111)) ([#5122](https://github.com/dsj1984/mandrel/issues/5122)) ([a4fbd56](https://github.com/dsj1984/mandrel/commit/a4fbd5628b8bc11c918e0ff658fe62baf078a17f))
|
|
28
|
+
* detect high-severity advisories on main between PRs, and bound the TypeScript devDependency range ([#5104](https://github.com/dsj1984/mandrel/issues/5104)) ([#5107](https://github.com/dsj1984/mandrel/issues/5107)) ([af8f1b8](https://github.com/dsj1984/mandrel/commit/af8f1b8ee28300391a3a43cca1d43ece5851ce83))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
* close five concurrency holes: sweep-lock steal and heartbeat, one lock for the merged-branch reap, atomic pending-cleanup manifest, idempotent createIssue retry, additive lease claim ([#5112](https://github.com/dsj1984/mandrel/issues/5112)) ([#5119](https://github.com/dsj1984/mandrel/issues/5119)) ([8651514](https://github.com/dsj1984/mandrel/commit/86515147381cf653c1e41c01f6405f5333826253))
|
|
34
|
+
* **deps:** bump fast-uri to 3.1.7 for four high-severity advisories ([#5103](https://github.com/dsj1984/mandrel/issues/5103)) ([06c37b1](https://github.com/dsj1984/mandrel/commit/06c37b11952fb8079027c0beceef831bed677485))
|
|
35
|
+
* **drain-pending-cleanup:** honour --no-escalate and guard advertised --no-* flags (refs [#5101](https://github.com/dsj1984/mandrel/issues/5101)) ([#5106](https://github.com/dsj1984/mandrel/issues/5106)) ([52ef3cb](https://github.com/dsj1984/mandrel/commit/52ef3cb0c546714900474c0613d78b23bc40a068))
|
|
36
|
+
* restore the suite profiler, make run-tests --help print usage, and stop three tests burning worker slots on real backoff and a live-repo scan ([#5110](https://github.com/dsj1984/mandrel/issues/5110)) ([#5115](https://github.com/dsj1984/mandrel/issues/5115)) ([dbe0742](https://github.com/dsj1984/mandrel/commit/dbe07420a7d8d3c99016968d9c15e45da64d0de2))
|
|
37
|
+
* single-story-close advertises --dry-run and --no-evidence but never wired either: retire both and reject them loudly ([#5100](https://github.com/dsj1984/mandrel/issues/5100)) ([#5102](https://github.com/dsj1984/mandrel/issues/5102)) ([2f4dd52](https://github.com/dsj1984/mandrel/commit/2f4dd52c682bcc3697f67124d6dc574e209ab204))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Performance
|
|
41
|
+
|
|
42
|
+
* stop the baseline gates doing pointless heavy work: lazy TypeScript, small-batch in-process scoring, precompiled globs, diff-scoped cyclomatic, precompiled .agentrc validator ([#5109](https://github.com/dsj1984/mandrel/issues/5109)) ([#5117](https://github.com/dsj1984/mandrel/issues/5117)) ([6b04d5e](https://github.com/dsj1984/mandrel/commit/6b04d5e7c014b4bc052afbef8fd88d85c7a6b00f))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
* collapse the tripled semver comparator and the copy-pasted migration-step scaffold in the published lib/ payload ([#5113](https://github.com/dsj1984/mandrel/issues/5113)) ([#5116](https://github.com/dsj1984/mandrel/issues/5116)) ([ae2b6d6](https://github.com/dsj1984/mandrel/commit/ae2b6d6c40b26ea6d2f94b07698867863011a659))
|
|
48
|
+
* extract one JavaScript comment stripper, bring two cyclomatic-20 CLI mains under the ceiling, and delete the orchestration payload’s dead code ([#5114](https://github.com/dsj1984/mandrel/issues/5114)) ([#5118](https://github.com/dsj1984/mandrel/issues/5118)) ([747d9a5](https://github.com/dsj1984/mandrel/commit/747d9a5bce66a6230fe6f46d132b3e8aacbc5a09))
|
|
49
|
+
|
|
18
50
|
## [2.36.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.35.0...mandrel-v2.36.0) (2026-08-29)
|
|
19
51
|
|
|
20
52
|
|