mandrel 2.52.0 → 2.54.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 +4 -5
- package/.agents/docs/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -0
- package/.agents/scripts/check-audit-attribution.js +245 -0
- package/.agents/scripts/check-pinned-override-notes.js +102 -0
- package/.agents/scripts/coverage-capture.js +36 -21
- package/.agents/scripts/lib/audit-attribution.js +112 -0
- package/.agents/scripts/lib/close-validation/commands.js +27 -1
- package/.agents/scripts/lib/close-validation/gates.js +33 -14
- package/.agents/scripts/lib/config/commands.js +14 -12
- package/.agents/scripts/lib/config-settings-schema-delivery.js +6 -0
- package/.agents/scripts/lib/config-settings-schema.js +9 -2
- package/.agents/scripts/lib/coverage-capture.js +70 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/epic-container.js +27 -0
- package/.agents/scripts/lib/orchestration/epic-rollup.js +23 -4
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +56 -6
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +40 -6
- package/.agents/scripts/lib/pinned-override-notes.js +100 -0
- package/.agents/scripts/resolve-stories.js +9 -3
- package/.agents/workflows/helpers/deliver-digest.md +4 -6
- package/.agents/workflows/helpers/deliver-reference.md +5 -2
- package/.agents/workflows/helpers/plan-reference.md +1 -1
- package/docs/CHANGELOG.md +20 -0
- package/package.json +5 -5
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* close-validation/commands.js — Command resolution + formatter file policy.
|
|
3
3
|
*
|
|
4
4
|
* Owns the `project.commands.*` resolution helpers used by the close-
|
|
5
|
-
* validation gates (typecheck / formatCheck / formatWrite), the Story-diff
|
|
5
|
+
* validation gates (typecheck / lint / formatCheck / formatWrite), the Story-diff
|
|
6
6
|
* changed-file listing for the format gate, and the formatter
|
|
7
7
|
* file-eligibility policy (Story #3410).
|
|
8
8
|
*/
|
|
@@ -17,6 +17,15 @@ import { getCommands } from '../config/commands.js';
|
|
|
17
17
|
*/
|
|
18
18
|
const TYPECHECK_FALLBACK = 'npm run typecheck';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Fallback lint command. The gate is mandatory, and this string is the exact
|
|
22
|
+
* command the gate spawned before `project.commands.lint` existed — an
|
|
23
|
+
* unconfigured consumer must keep getting byte-identical argv, because the
|
|
24
|
+
* gate's `commandConfigHash` is computed over it and any drift would silently
|
|
25
|
+
* invalidate every previously recorded lint evidence record.
|
|
26
|
+
*/
|
|
27
|
+
const LINT_FALLBACK = 'npm run lint';
|
|
28
|
+
|
|
20
29
|
/** Default formatter command when `project.commands.formatCheck` is unset. */
|
|
21
30
|
export const FORMAT_CHECK_FALLBACK = 'npx biome format .';
|
|
22
31
|
|
|
@@ -72,6 +81,23 @@ export function resolveTypecheckCommand(config) {
|
|
|
72
81
|
return resolveCommandWithFallback(config, 'typecheck', TYPECHECK_FALLBACK);
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the lint command. Reads `project.commands.lint`; falls back to
|
|
86
|
+
* `npm run lint`. The framework-wide `COMMANDS_DEFAULTS.lint` is `null` — as
|
|
87
|
+
* for typecheck — because the fallback belongs to this mandatory gate rather
|
|
88
|
+
* than to the shared accessor.
|
|
89
|
+
*
|
|
90
|
+
* A consumer sets this to point the gate at the scoped pair its hooks already
|
|
91
|
+
* run: the close-time gate stays real (the diff is still linted) while CI
|
|
92
|
+
* keeps owning whole-repo drift. Exported for testing.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ project?: { commands?: object } } | null | undefined} config
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function resolveLintCommand(config) {
|
|
98
|
+
return resolveCommandWithFallback(config, 'lint', LINT_FALLBACK);
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
/**
|
|
76
102
|
* Resolve the format-check command. Reads `project.commands.formatCheck`;
|
|
77
103
|
* falls back to `npx biome format .` so existing repos keep working byte-
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
FORMAT_CHECK_FALLBACK,
|
|
18
18
|
resolveFormatCheckCommand,
|
|
19
19
|
resolveFormatWriteCommand,
|
|
20
|
+
resolveLintCommand,
|
|
20
21
|
resolveTypecheckCommand,
|
|
21
22
|
} from './commands.js';
|
|
22
23
|
|
|
@@ -335,6 +336,20 @@ function buildBaselinesGateEntries({ decision, kinds, env }) {
|
|
|
335
336
|
];
|
|
336
337
|
}
|
|
337
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Split a resolved command string into the `{ cmd, args }` pair a gate entry
|
|
341
|
+
* carries. Whitespace-separated, so `npm run lint` yields the argv the gate
|
|
342
|
+
* spawned before any of these commands were configurable — which is what
|
|
343
|
+
* keeps an unconfigured consumer's `commandConfigHash` unchanged.
|
|
344
|
+
*
|
|
345
|
+
* @param {string} commandString
|
|
346
|
+
* @returns {{ cmd: string, args: string[] }}
|
|
347
|
+
*/
|
|
348
|
+
function splitCommand(commandString) {
|
|
349
|
+
const [cmd, ...args] = commandString.split(/\s+/).filter(Boolean);
|
|
350
|
+
return { cmd, args };
|
|
351
|
+
}
|
|
352
|
+
|
|
338
353
|
/**
|
|
339
354
|
* Build the canonical close-validation gate list.
|
|
340
355
|
*
|
|
@@ -347,8 +362,12 @@ function buildBaselinesGateEntries({ decision, kinds, env }) {
|
|
|
347
362
|
* absent, coverage-capture is dropped and the `test` gate is restored so
|
|
348
363
|
* there is always a working test gate.
|
|
349
364
|
*
|
|
350
|
-
* `typecheck`
|
|
351
|
-
* `project.commands.typecheck`
|
|
365
|
+
* `typecheck` and `lint` are mandatory; consumers may customise either
|
|
366
|
+
* command via `project.commands.typecheck` / `project.commands.lint`
|
|
367
|
+
* (defaults `npm run typecheck` / `npm run lint`). Customising `lint` is how
|
|
368
|
+
* a consumer whose hooks and CI already lint the diff stops paying for a
|
|
369
|
+
* third whole-repo pass at close: `lint` runs in the parallel partition, so
|
|
370
|
+
* a slow whole-repo lint sets the floor for that whole phase.
|
|
352
371
|
*
|
|
353
372
|
* Story #2210 retired the legacy per-kind in-process regression gates
|
|
354
373
|
* (`check-maintainability`, `check-crap`, `check-mutation`) and their
|
|
@@ -409,14 +428,10 @@ export function buildDefaultGates({
|
|
|
409
428
|
const scripts = packageScripts ?? readPackageScripts(cwd);
|
|
410
429
|
const coverageCaptureActive =
|
|
411
430
|
isCrapGateEnabled(config) && hasNpmScript(scripts, 'test:coverage');
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
.split(/\s+/)
|
|
415
|
-
.filter(Boolean);
|
|
431
|
+
const typecheck = splitCommand(resolveTypecheckCommand(config));
|
|
432
|
+
const lint = splitCommand(resolveLintCommand(config));
|
|
416
433
|
const formatCheckString = resolveFormatCheckCommand(config);
|
|
417
|
-
const
|
|
418
|
-
.split(/\s+/)
|
|
419
|
-
.filter(Boolean);
|
|
434
|
+
const format = splitCommand(formatCheckString);
|
|
420
435
|
const formatWriteString = resolveFormatWriteCommand(config);
|
|
421
436
|
const formatChangedFileScope =
|
|
422
437
|
formatCheckString === FORMAT_CHECK_FALLBACK
|
|
@@ -436,11 +451,15 @@ export function buildDefaultGates({
|
|
|
436
451
|
return [
|
|
437
452
|
{
|
|
438
453
|
name: 'typecheck',
|
|
439
|
-
cmd:
|
|
440
|
-
args:
|
|
454
|
+
cmd: typecheck.cmd,
|
|
455
|
+
args: typecheck.args,
|
|
441
456
|
hint: TYPECHECK_HINT,
|
|
442
457
|
},
|
|
443
|
-
|
|
458
|
+
// Gate name kept generic ("lint") for the same reason the format gate's
|
|
459
|
+
// is: the command resolves from config, so a consumer pointing it at a
|
|
460
|
+
// scoped pair does not shift the close-orchestrator log line, the
|
|
461
|
+
// evidence keyspace, or the parallel-partition membership below.
|
|
462
|
+
{ name: 'lint', cmd: lint.cmd, args: lint.args },
|
|
444
463
|
...buildTestGateEntry(coverageCaptureActive),
|
|
445
464
|
{
|
|
446
465
|
// Gate name kept generic ("format") so the close-orchestrator log line
|
|
@@ -448,8 +467,8 @@ export function buildDefaultGates({
|
|
|
448
467
|
// `project.commands.formatCheck`. The
|
|
449
468
|
// actual command and the remediation hint resolve from config.
|
|
450
469
|
name: 'format',
|
|
451
|
-
cmd:
|
|
452
|
-
args:
|
|
470
|
+
cmd: format.cmd,
|
|
471
|
+
args: format.args,
|
|
453
472
|
hint: buildFormatHint(formatWriteString),
|
|
454
473
|
...(formatChangedFileScope
|
|
455
474
|
? { changedFileScope: formatChangedFileScope }
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `project.commands` accessor (Epic #1720 Story #1739 — top-level reshape).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* `
|
|
4
|
+
* The command keys are `test`, `typecheck`, `lint`, `formatCheck`,
|
|
5
|
+
* `formatWrite`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const COMMANDS_DEFAULTS = Object.freeze({
|
|
9
9
|
test: 'npm test',
|
|
10
10
|
typecheck: null,
|
|
11
|
+
lint: null,
|
|
11
12
|
formatCheck: 'npx biome format .',
|
|
12
13
|
formatWrite: 'npx biome format --write .',
|
|
13
14
|
});
|
|
@@ -18,17 +19,18 @@ export const COMMANDS_DEFAULTS = Object.freeze({
|
|
|
18
19
|
* a bare `{ project }` bag.
|
|
19
20
|
*
|
|
20
21
|
* @param {object | null | undefined} config
|
|
21
|
-
* @returns {{ test: string, typecheck: string|null, formatCheck: string, formatWrite: string }}
|
|
22
|
+
* @returns {{ test: string, typecheck: string|null, lint: string|null, formatCheck: string, formatWrite: string }}
|
|
22
23
|
*/
|
|
23
24
|
export function getCommands(config) {
|
|
24
25
|
const commands = config?.project?.commands ?? {};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
// `??` is uniform across every key: the two nullable ones (`typecheck`,
|
|
27
|
+
// `lint`) default to `null` themselves, so coalescing an explicit `null`
|
|
28
|
+
// onto the default returns that same `null` — the per-key `=== undefined`
|
|
29
|
+
// ladder this replaces drew a distinction that had no observable effect.
|
|
30
|
+
return Object.fromEntries(
|
|
31
|
+
Object.keys(COMMANDS_DEFAULTS).map((key) => [
|
|
32
|
+
key,
|
|
33
|
+
commands[key] ?? COMMANDS_DEFAULTS[key],
|
|
34
|
+
]),
|
|
35
|
+
);
|
|
34
36
|
}
|
|
@@ -45,6 +45,12 @@ const EXECUTION_SCHEMA = {
|
|
|
45
45
|
'Serialize full-suite spawns (`npm test` / `npm run test:coverage`) behind a host-level advisory lock, so two concurrent deliveries on one checkout do not run two suites against the same cores. Best-effort: a wait that expires spawns anyway, so the lock can never fail a delivery. Set false — or export `MANDREL_FULL_SUITE_LOCK=0` for one invocation — to disable.',
|
|
46
46
|
default: true,
|
|
47
47
|
},
|
|
48
|
+
requireCreditedCapture: {
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
description:
|
|
51
|
+
'Refuse a full-suite coverage capture that no committed stamp covers, instead of paying for it. Default false — the run is announced (a warning naming the crediting invocation, emitted before the spawn) and then executed, which is the pre-existing behaviour. Set true when the whole-suite cost is large enough that a close should stop at zero seconds rather than absorb it silently.',
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
48
54
|
},
|
|
49
55
|
additionalProperties: false,
|
|
50
56
|
};
|
|
@@ -128,9 +128,10 @@ const PATHS_SCHEMA = {
|
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* `project.commands` — names of the test/typecheck/format commands the
|
|
131
|
+
* `project.commands` — names of the test/typecheck/lint/format commands the
|
|
132
132
|
* close-validation chain spawns. `typecheck` accepts `null` to mean
|
|
133
|
-
* "disabled"
|
|
133
|
+
* "disabled"; `lint` accepts `null` to mean "use the framework default",
|
|
134
|
+
* because that gate is mandatory and cannot be switched off. `validate` and `build` were dropped (no production consumers).
|
|
134
135
|
*/
|
|
135
136
|
const COMMANDS_SCHEMA = {
|
|
136
137
|
type: 'object',
|
|
@@ -149,6 +150,12 @@ const COMMANDS_SCHEMA = {
|
|
|
149
150
|
'Static type-check command. `null` disables the gate for projects with no type layer; the empty string is rejected so a typo cannot silently disable it.',
|
|
150
151
|
default: COMMANDS_DEFAULTS.typecheck,
|
|
151
152
|
},
|
|
153
|
+
lint: {
|
|
154
|
+
...NULLABLE_NONEMPTY_SAFE_STRING,
|
|
155
|
+
description:
|
|
156
|
+
'Lint command run as a close-validation gate. `null` (the default) uses `npm run lint`; the gate is mandatory, so unlike `typecheck` this key cannot disable it. Point it at the scoped command your hooks already run to stop paying for a third whole-repo lint at close — the gate still lints the diff, and CI still owns whole-repo drift. Like every command here it must be a single argv (no `;`, `&&`, pipes or substitution), so wrap a multi-linter pair in one npm script and name that.',
|
|
157
|
+
default: COMMANDS_DEFAULTS.lint,
|
|
158
|
+
},
|
|
152
159
|
formatCheck: {
|
|
153
160
|
...SAFE_STRING,
|
|
154
161
|
minLength: 1,
|
|
@@ -375,6 +375,76 @@ export function describeFreshness(freshness, targetDirs) {
|
|
|
375
375
|
return `${reason} — no scorable source file found under [${dirs}]; if that does not name this project's sources, fix quality.gates.crap.targetDirs`;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
/**
|
|
379
|
+
* The invocation that deposits capture credit for a Story branch. Named in
|
|
380
|
+
* the uncredited-capture announcement so a reader of a close log sees the
|
|
381
|
+
* command that would have avoided the cost, not just the cost.
|
|
382
|
+
*/
|
|
383
|
+
const CREDITING_INVOCATION =
|
|
384
|
+
'node <main-repo>/.agents/scripts/coverage-capture.js --cwd <workCwd>';
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Announce — and, when the consumer requires credit, refuse — a full-suite
|
|
388
|
+
* capture that no committed stamp covers.
|
|
389
|
+
*
|
|
390
|
+
* Every caller invokes this immediately before it would spawn the suite, and
|
|
391
|
+
* the ordering is the whole point. The capture itself is the most expensive
|
|
392
|
+
* thing a close does; discovering that it ran uncredited is only actionable
|
|
393
|
+
* while it is still ahead of you, not once it is visible as a `durationMs`
|
|
394
|
+
* in `validation-evidence.json` twelve minutes later.
|
|
395
|
+
*
|
|
396
|
+
* The probe is read-only by construction: every caller has already decided
|
|
397
|
+
* to capture by the time it runs, and it writes nothing — so it never takes
|
|
398
|
+
* the full-suite host lock and can never itself be the reason a close waits.
|
|
399
|
+
*
|
|
400
|
+
* @param {{
|
|
401
|
+
* requireCredited?: boolean,
|
|
402
|
+
* logger: { info: Function, warn: Function, error: Function },
|
|
403
|
+
* }} opts `requireCredited` mirrors `delivery.execution.requireCreditedCapture`
|
|
404
|
+
* (default false → announce and run).
|
|
405
|
+
* @returns {number | null} A non-zero exit code the caller MUST return
|
|
406
|
+
* without spawning the suite, or `null` to proceed with the capture.
|
|
407
|
+
*/
|
|
408
|
+
function announceUncreditedCapture({ requireCredited = false, logger }) {
|
|
409
|
+
const preamble =
|
|
410
|
+
'no credited capture stamp covers this change set — ' +
|
|
411
|
+
`the full suite is about to run. Deposit credit before the push with: ${CREDITING_INVOCATION}`;
|
|
412
|
+
if (requireCredited) {
|
|
413
|
+
logger.error(
|
|
414
|
+
`[coverage-capture] ✖ ${preamble} (delivery.execution.requireCreditedCapture is set, so this run is refused instead of paid for).`,
|
|
415
|
+
);
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
logger.warn(`[coverage-capture] ⚠ ${preamble}`);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Compose the uncredited-capture probe over a capture runner, so the
|
|
424
|
+
* announcement is structurally inseparable from the spawn it describes.
|
|
425
|
+
*
|
|
426
|
+
* This mirrors `lockedCapture`, and for the same reason: there are two
|
|
427
|
+
* capture paths (full-scope and incremental) and neither should have to
|
|
428
|
+
* remember the policy. Wrapping the runner they share means a third path
|
|
429
|
+
* added later inherits the probe for free, and that the warning can never be
|
|
430
|
+
* emitted for a capture that does not happen — or omitted for one that does.
|
|
431
|
+
*
|
|
432
|
+
* It composes OUTSIDE `lockedCapture`, so a refusal costs nothing: the host
|
|
433
|
+
* lock is never acquired for a run that is about to be declined.
|
|
434
|
+
*
|
|
435
|
+
* @param {(opts: object) => number} runCaptureFn The (possibly already
|
|
436
|
+
* lock-wrapped) capture runner.
|
|
437
|
+
* @param {{ requireCredited?: boolean, logger: object }} policy
|
|
438
|
+
* @returns {(opts?: object) => number} A runner returning the capture's exit
|
|
439
|
+
* code, or a non-zero refusal code without having spawned anything.
|
|
440
|
+
*/
|
|
441
|
+
export function creditedCapture(runCaptureFn, { requireCredited, logger }) {
|
|
442
|
+
return (captureOpts = {}) => {
|
|
443
|
+
const refusal = announceUncreditedCapture({ requireCredited, logger });
|
|
444
|
+
return refusal === null ? runCaptureFn(captureOpts) : refusal;
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
378
448
|
/**
|
|
379
449
|
* Narrow `changedFiles` to the subset that lives under one of `targetDirs`.
|
|
380
450
|
*
|