mandrel 2.66.0 → 2.68.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 +2 -2
- package/.agents/agents/story-worker.md +15 -11
- package/.agents/docs/agentrc-reference.json +5 -2
- package/.agents/docs/configuration.md +38 -2
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +2 -1
- package/.agents/rules/git-conventions-reference.md +5 -5
- package/.agents/rules/git-conventions.md +1 -1
- package/.agents/schemas/agentrc.schema.json +20 -2
- package/.agents/schemas/story-deliver-terminal.schema.json +23 -1
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/boot-sweep.js +97 -9
- package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
- package/.agents/scripts/clean-temp.js +54 -0
- package/.agents/scripts/clean-worktrees.js +593 -0
- package/.agents/scripts/coverage-capture.js +65 -9
- package/.agents/scripts/drain-pending-cleanup.js +5 -4
- package/.agents/scripts/evidence-gate.js +106 -8
- package/.agents/scripts/lib/baselines/coverage-refresh-scope.js +60 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +101 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +1 -1
- package/.agents/scripts/lib/baselines/seat-missing.js +228 -0
- package/.agents/scripts/lib/child-exec.js +39 -1
- package/.agents/scripts/lib/clean-temp.js +440 -0
- package/.agents/scripts/lib/close-validation/gates.js +59 -19
- package/.agents/scripts/lib/close-validation/process.js +23 -24
- package/.agents/scripts/lib/close-validation/runner.js +71 -40
- package/.agents/scripts/lib/config/gates/coverage.schema.js +21 -0
- package/.agents/scripts/lib/config/quality.js +7 -1
- package/.agents/scripts/lib/config/temp-paths.js +15 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +12 -3
- package/.agents/scripts/lib/coverage-baseline.js +78 -5
- package/.agents/scripts/lib/coverage-capture-affected.js +345 -0
- package/.agents/scripts/lib/coverage-capture-delta.js +180 -0
- package/.agents/scripts/lib/coverage-capture-fullscope.js +53 -32
- package/.agents/scripts/lib/coverage-capture-incremental.js +49 -26
- package/.agents/scripts/lib/coverage-capture-usage.js +1 -1
- package/.agents/scripts/lib/coverage-capture.js +121 -81
- package/.agents/scripts/lib/full-suite-lock.js +49 -46
- package/.agents/scripts/lib/full-suite-queue.js +83 -8
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +4 -1
- package/.agents/scripts/lib/orchestration/code-review.js +15 -3
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
- package/.agents/scripts/lib/orchestration/review-deposit.js +219 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +11 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +29 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +124 -73
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +38 -20
- package/.agents/scripts/lib/orchestration/single-story-close/phases/lock-wait-pending.js +8 -2
- package/.agents/scripts/lib/orchestration/single-story-close/review-overlap.js +161 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +47 -7
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +8 -16
- package/.agents/scripts/lib/process-group.js +1 -1
- package/.agents/scripts/lib/single-story-sweep.js +2 -2
- package/.agents/scripts/lib/supervised-suite.js +247 -0
- package/.agents/scripts/lib/temp-removal.js +110 -0
- package/.agents/scripts/lib/temp-retention.js +122 -73
- package/.agents/scripts/lib/wave-runner/cross-run-overlap.js +120 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +5 -1
- package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
- package/.agents/scripts/quality-preview.js +112 -14
- package/.agents/scripts/single-story-init.js +120 -17
- package/.agents/scripts/stories-wave-tick.js +47 -0
- package/.agents/scripts/story-review-compute.js +207 -0
- package/.agents/scripts/update-coverage-baseline.js +15 -10
- package/.agents/scripts/update-crap-baseline.js +12 -2
- package/.agents/scripts/update-maintainability-baseline.js +12 -2
- package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
- package/.agents/workflows/clean-temp.md +67 -0
- package/.agents/workflows/clean-worktrees.md +63 -0
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +3 -2
- package/.agents/workflows/helpers/code-review.md +7 -5
- package/.agents/workflows/helpers/deliver-digest.md +39 -36
- package/.agents/workflows/helpers/deliver-reference.md +115 -5
- package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
- package/.agents/workflows/helpers/deliver-story.md +2 -1
- package/docs/CHANGELOG.md +39 -0
- package/lib/cli/registry.js +125 -18
- package/lib/migrations/steps/strip-removed-agentrc-keys.js +0 -5
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insert-only baseline seating (`--seat-missing`): write ONLY rows whose seat
|
|
3
|
+
* key is absent from the committed baseline; prior rows keep their parsed
|
|
4
|
+
* objects, so they serialise byte-identically. The CRAP key is
|
|
5
|
+
* `path::method` — the gate matches a moved method by name, so it is not
|
|
6
|
+
* missing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import nodeFs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { execFileCaptureAsync } from '../child-exec.js';
|
|
12
|
+
import { getBaselines, resolveConfig } from '../config-resolver.js';
|
|
13
|
+
import { Logger } from '../Logger.js';
|
|
14
|
+
import { parseDiffScopeFlag } from './diff-scope-cli.js';
|
|
15
|
+
import { assertEnvelope } from './envelope.js';
|
|
16
|
+
import { getKindModule } from './kernel.js';
|
|
17
|
+
import { canonicalizeBaselinePath } from './path-canon.js';
|
|
18
|
+
import {
|
|
19
|
+
deriveScopeFromDiff,
|
|
20
|
+
fileFilterFor,
|
|
21
|
+
readPriorEnvelope,
|
|
22
|
+
resolveDefaultScorer,
|
|
23
|
+
} from './refresh-service.js';
|
|
24
|
+
import {
|
|
25
|
+
write as writeEnvelope,
|
|
26
|
+
writeFile as writeEnvelopeFile,
|
|
27
|
+
} from './writer.js';
|
|
28
|
+
|
|
29
|
+
/** Thrown when a seat must not write; the CLI prints `message` and exits 1. */
|
|
30
|
+
export class SeatRefusal extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'SeatRefusal';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SEAT_KEYS = Object.freeze({
|
|
38
|
+
crap: (row) => `${row.path}::${row.method}`,
|
|
39
|
+
maintainability: (row) => row.path,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} kind
|
|
44
|
+
* @returns {(row: object) => string}
|
|
45
|
+
*/
|
|
46
|
+
export function seatKeyFor(kind) {
|
|
47
|
+
const key = SEAT_KEYS[kind];
|
|
48
|
+
if (!key) throw new Error(`seat-missing: unsupported kind "${kind}"`);
|
|
49
|
+
return key;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Candidate rows whose seat key no prior row carries. Pure.
|
|
54
|
+
*
|
|
55
|
+
* @param {{kind: string, priorRows: object[], candidateRows: object[]}} args
|
|
56
|
+
* @returns {object[]}
|
|
57
|
+
*/
|
|
58
|
+
export function selectMissingRows({ kind, priorRows, candidateRows }) {
|
|
59
|
+
const key = seatKeyFor(kind);
|
|
60
|
+
const present = new Set((priorRows ?? []).map(key));
|
|
61
|
+
return (candidateRows ?? []).filter((row) => !present.has(key(row)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The CRAP seat's fail-closed precondition: every joinable method in scope
|
|
66
|
+
* resolved a coverage entry. Returns the refusal text, or `null` to proceed.
|
|
67
|
+
* Anything below 100% means the artifact's coordinates predate the tree.
|
|
68
|
+
*
|
|
69
|
+
* @param {{resolvedMethods?: number, joinableMethods?: number, rate?: number,
|
|
70
|
+
* worstFiles?: Array<{file: string, unresolved: number, total: number}>}
|
|
71
|
+
* | undefined} resolution
|
|
72
|
+
* @param {string} fixCommand
|
|
73
|
+
* @returns {string|null}
|
|
74
|
+
*/
|
|
75
|
+
export function checkSeatResolution(resolution, fixCommand) {
|
|
76
|
+
const { joinableMethods = 0, resolvedMethods = 0 } = resolution ?? {};
|
|
77
|
+
if (resolvedMethods >= joinableMethods) return null;
|
|
78
|
+
const rate = ((resolvedMethods / joinableMethods) * 100).toFixed(1);
|
|
79
|
+
const files = (resolution.worstFiles ?? [])
|
|
80
|
+
.map((w) => ` - ${w.file} (${w.unresolved}/${w.total} unresolved)`)
|
|
81
|
+
.join('\n');
|
|
82
|
+
return (
|
|
83
|
+
`[CRAP] --seat-missing refused: method resolution ${resolvedMethods}/${joinableMethods} ` +
|
|
84
|
+
`(${rate}%) over the in-scope files; seating requires 100%.\n` +
|
|
85
|
+
(files ? `Unresolved files:\n${files}\n` : '') +
|
|
86
|
+
`Fix: re-capture coverage for the current tree — ${fixCommand}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Three-dot range (from the merge-base): the files this branch changed.
|
|
92
|
+
*
|
|
93
|
+
* @param {{baseRef: string, headRef: string, cwd: string}} args
|
|
94
|
+
* @returns {Promise<string[]>}
|
|
95
|
+
*/
|
|
96
|
+
async function mergeBaseGitDiff({ baseRef, headRef, cwd }) {
|
|
97
|
+
const { stdout } = await execFileCaptureAsync(
|
|
98
|
+
'git',
|
|
99
|
+
['diff', '--name-only', `${baseRef}...${headRef}`],
|
|
100
|
+
{ cwd },
|
|
101
|
+
);
|
|
102
|
+
return stdout.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Prior rows are kept as parsed; only the seated rows are projected. With no
|
|
107
|
+
* prior baseline the writer builds a fresh envelope.
|
|
108
|
+
*/
|
|
109
|
+
function buildSeatedEnvelope({ kind, priorEnvelope, seated }) {
|
|
110
|
+
if (!priorEnvelope) return writeEnvelope({ kind, rows: seated });
|
|
111
|
+
const mod = getKindModule(kind);
|
|
112
|
+
const envelope = {
|
|
113
|
+
...priorEnvelope,
|
|
114
|
+
rows: mod.sortRows([...priorEnvelope.rows, ...seated.map(mod.projectRow)]),
|
|
115
|
+
};
|
|
116
|
+
assertEnvelope(envelope);
|
|
117
|
+
return envelope;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Seat the missing rows of `kind` for the files changed in `baseRef...HEAD`.
|
|
122
|
+
* `score(files)` may throw a {@link SeatRefusal}. No missing row, no write.
|
|
123
|
+
*
|
|
124
|
+
* @param {{kind: string, writePath: string, score: Function, baseRef: string,
|
|
125
|
+
* headRef?: string, cwd?: string, gitDiff?: Function, fs?: typeof nodeFs}} opts
|
|
126
|
+
* @returns {Promise<{seated: number, wrote: boolean, files: string[]}>}
|
|
127
|
+
*/
|
|
128
|
+
export async function seatMissingBaseline({
|
|
129
|
+
kind,
|
|
130
|
+
writePath,
|
|
131
|
+
score,
|
|
132
|
+
baseRef,
|
|
133
|
+
headRef = 'HEAD',
|
|
134
|
+
cwd = process.cwd(),
|
|
135
|
+
gitDiff = mergeBaseGitDiff,
|
|
136
|
+
fs = nodeFs,
|
|
137
|
+
}) {
|
|
138
|
+
const files = await deriveScopeFromDiff({
|
|
139
|
+
baseRef,
|
|
140
|
+
headRef,
|
|
141
|
+
predicate: fileFilterFor(kind),
|
|
142
|
+
gitDiff,
|
|
143
|
+
cwd,
|
|
144
|
+
});
|
|
145
|
+
if (files.length === 0) return { seated: 0, wrote: false, files };
|
|
146
|
+
|
|
147
|
+
const candidateRows = (await score(files)).map((row) => ({
|
|
148
|
+
...row,
|
|
149
|
+
path: canonicalizeBaselinePath(row.path ?? row.file),
|
|
150
|
+
}));
|
|
151
|
+
const priorEnvelope = readPriorEnvelope(writePath, fs);
|
|
152
|
+
const seated = selectMissingRows({
|
|
153
|
+
kind,
|
|
154
|
+
priorRows: priorEnvelope?.rows,
|
|
155
|
+
candidateRows,
|
|
156
|
+
});
|
|
157
|
+
if (seated.length === 0) return { seated: 0, wrote: false, files };
|
|
158
|
+
|
|
159
|
+
const envelope = buildSeatedEnvelope({ kind, priorEnvelope, seated });
|
|
160
|
+
writeEnvelopeFile(writePath, envelope, { fsImpl: fs });
|
|
161
|
+
return { seated: seated.length, wrote: true, files };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The updater CLIs' `--seat-missing` entry: prints `seated: N`; a
|
|
166
|
+
* {@link SeatRefusal} is an expected outcome, so exit 1 with its message.
|
|
167
|
+
*
|
|
168
|
+
* @param {{kind: string, label: string, writePath: string,
|
|
169
|
+
* diffScopeRef?: string|null, fullScope?: boolean, baseBranch?: string,
|
|
170
|
+
* score?: Function, cwd?: string, logger?: object, seat?: Function}} opts
|
|
171
|
+
* @returns {Promise<number>} The exit code.
|
|
172
|
+
*/
|
|
173
|
+
export async function runSeatMissing({
|
|
174
|
+
kind,
|
|
175
|
+
label,
|
|
176
|
+
writePath,
|
|
177
|
+
diffScopeRef = null,
|
|
178
|
+
fullScope = false,
|
|
179
|
+
baseBranch = 'main',
|
|
180
|
+
score,
|
|
181
|
+
cwd = process.cwd(),
|
|
182
|
+
logger = Logger,
|
|
183
|
+
seat = seatMissingBaseline,
|
|
184
|
+
}) {
|
|
185
|
+
if (fullScope) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`[${label}] --full-scope is incompatible with --seat-missing; pick one`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const { seated } = await seat({
|
|
192
|
+
kind,
|
|
193
|
+
writePath,
|
|
194
|
+
baseRef: diffScopeRef ?? `origin/${baseBranch}`,
|
|
195
|
+
score: score ?? ((files) => resolveDefaultScorer(kind, { cwd })(files)),
|
|
196
|
+
cwd,
|
|
197
|
+
});
|
|
198
|
+
logger.info(`[${label}] seated: ${seated}`);
|
|
199
|
+
return 0;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (!(err instanceof SeatRefusal)) throw err;
|
|
202
|
+
logger.error(err.message);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `update-maintainability-baseline.js --seat-missing`. MI is static, so the
|
|
209
|
+
* default scorer needs no coverage precondition. Resolves to the exit code.
|
|
210
|
+
*
|
|
211
|
+
* @param {string[]} argv
|
|
212
|
+
* @param {{config?: object, cwd?: string}} [deps]
|
|
213
|
+
* @returns {Promise<number>}
|
|
214
|
+
*/
|
|
215
|
+
export function seatMaintainabilityBaseline(
|
|
216
|
+
argv,
|
|
217
|
+
{ config = resolveConfig(), cwd = process.cwd() } = {},
|
|
218
|
+
) {
|
|
219
|
+
return runSeatMissing({
|
|
220
|
+
kind: 'maintainability',
|
|
221
|
+
label: 'Maintainability',
|
|
222
|
+
writePath: path.resolve(cwd, getBaselines(config).maintainability.path),
|
|
223
|
+
diffScopeRef: parseDiffScopeFlag(argv),
|
|
224
|
+
fullScope: argv.includes('--full-scope'),
|
|
225
|
+
baseBranch: config.project.baseBranch,
|
|
226
|
+
cwd,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -101,7 +101,10 @@ export function spawnChild(file, args, opts = {}) {
|
|
|
101
101
|
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
102
102
|
*/
|
|
103
103
|
export function spawnCapture(file, args, opts = {}) {
|
|
104
|
-
|
|
104
|
+
return normalizeCapture(spawnChild(file, args, opts));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeCapture(result) {
|
|
105
108
|
return {
|
|
106
109
|
status: result?.status ?? 1,
|
|
107
110
|
stdout: (result?.stdout ?? '').toString().trim(),
|
|
@@ -109,6 +112,41 @@ export function spawnCapture(file, args, opts = {}) {
|
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The async runner behind `spawnCaptureAsync`: `input` rides stdin; a
|
|
117
|
+
* timeout, overflow or spawn error resolves with a non-numeric status.
|
|
118
|
+
*
|
|
119
|
+
* @returns {Promise<{ status: number|null, stdout: string, stderr: string }>}
|
|
120
|
+
*/
|
|
121
|
+
function execCollect(file, args, { input, ...options }) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const child = execFile(file, args, options, (error, stdout, stderr) => {
|
|
124
|
+
const code = error ? error.code : 0;
|
|
125
|
+
const status = Number.isInteger(code) ? code : null;
|
|
126
|
+
const reason = status === null ? error.message : '';
|
|
127
|
+
resolve({ status, stdout, stderr: stderr || reason });
|
|
128
|
+
});
|
|
129
|
+
child.stdin?.on('error', () => {});
|
|
130
|
+
child.stdin?.end(input ?? '');
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `spawnCapture` without blocking the event loop: concurrent children keep
|
|
136
|
+
* draining while this one runs. Same normalisation (`null` → 1, trimmed).
|
|
137
|
+
*
|
|
138
|
+
* @param {string} file
|
|
139
|
+
* @param {string[]} args
|
|
140
|
+
* @param {object} [opts]
|
|
141
|
+
* @param {Function} [opts.run] - async (or sync) `(file, args, options)` seam.
|
|
142
|
+
* @returns {Promise<{ status: number, stdout: string, stderr: string }>}
|
|
143
|
+
*/
|
|
144
|
+
export async function spawnCaptureAsync(file, args, opts = {}) {
|
|
145
|
+
return normalizeCapture(
|
|
146
|
+
await spawnChild(file, args, { run: execCollect, ...opts }),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
/**
|
|
113
151
|
* `status=null` is printed verbatim: it means the child was killed.
|
|
114
152
|
*
|