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.
Files changed (85) hide show
  1. package/.agents/agents/acceptance-critic.md +2 -2
  2. package/.agents/agents/story-worker.md +15 -11
  3. package/.agents/docs/agentrc-reference.json +5 -2
  4. package/.agents/docs/configuration.md +38 -2
  5. package/.agents/docs/workflows.md +4 -2
  6. package/.agents/instructions.md +2 -1
  7. package/.agents/rules/git-conventions-reference.md +5 -5
  8. package/.agents/rules/git-conventions.md +1 -1
  9. package/.agents/schemas/agentrc.schema.json +20 -2
  10. package/.agents/schemas/story-deliver-terminal.schema.json +23 -1
  11. package/.agents/schemas/validation-evidence.schema.json +3 -1
  12. package/.agents/scripts/boot-sweep.js +97 -9
  13. package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
  14. package/.agents/scripts/clean-temp.js +54 -0
  15. package/.agents/scripts/clean-worktrees.js +593 -0
  16. package/.agents/scripts/coverage-capture.js +65 -9
  17. package/.agents/scripts/drain-pending-cleanup.js +5 -4
  18. package/.agents/scripts/evidence-gate.js +106 -8
  19. package/.agents/scripts/lib/baselines/coverage-refresh-scope.js +60 -0
  20. package/.agents/scripts/lib/baselines/crap-updater-cli.js +101 -4
  21. package/.agents/scripts/lib/baselines/refresh-service.js +1 -1
  22. package/.agents/scripts/lib/baselines/seat-missing.js +228 -0
  23. package/.agents/scripts/lib/child-exec.js +39 -1
  24. package/.agents/scripts/lib/clean-temp.js +440 -0
  25. package/.agents/scripts/lib/close-validation/gates.js +59 -19
  26. package/.agents/scripts/lib/close-validation/process.js +23 -24
  27. package/.agents/scripts/lib/close-validation/runner.js +71 -40
  28. package/.agents/scripts/lib/config/gates/coverage.schema.js +21 -0
  29. package/.agents/scripts/lib/config/quality.js +7 -1
  30. package/.agents/scripts/lib/config/temp-paths.js +15 -0
  31. package/.agents/scripts/lib/config-settings-schema-delivery.js +12 -3
  32. package/.agents/scripts/lib/coverage-baseline.js +78 -5
  33. package/.agents/scripts/lib/coverage-capture-affected.js +345 -0
  34. package/.agents/scripts/lib/coverage-capture-delta.js +180 -0
  35. package/.agents/scripts/lib/coverage-capture-fullscope.js +53 -32
  36. package/.agents/scripts/lib/coverage-capture-incremental.js +49 -26
  37. package/.agents/scripts/lib/coverage-capture-usage.js +1 -1
  38. package/.agents/scripts/lib/coverage-capture.js +121 -81
  39. package/.agents/scripts/lib/full-suite-lock.js +49 -46
  40. package/.agents/scripts/lib/full-suite-queue.js +83 -8
  41. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  42. package/.agents/scripts/lib/observability/source-classifier.js +4 -1
  43. package/.agents/scripts/lib/orchestration/code-review.js +15 -3
  44. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  45. package/.agents/scripts/lib/orchestration/merge-poll.js +5 -0
  46. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
  47. package/.agents/scripts/lib/orchestration/review-deposit.js +219 -0
  48. package/.agents/scripts/lib/orchestration/review-providers/code-review.js +11 -7
  49. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +29 -10
  50. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +124 -73
  51. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +38 -20
  52. package/.agents/scripts/lib/orchestration/single-story-close/phases/lock-wait-pending.js +8 -2
  53. package/.agents/scripts/lib/orchestration/single-story-close/review-overlap.js +161 -0
  54. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +47 -7
  55. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +8 -16
  56. package/.agents/scripts/lib/process-group.js +1 -1
  57. package/.agents/scripts/lib/single-story-sweep.js +2 -2
  58. package/.agents/scripts/lib/supervised-suite.js +247 -0
  59. package/.agents/scripts/lib/temp-removal.js +110 -0
  60. package/.agents/scripts/lib/temp-retention.js +122 -73
  61. package/.agents/scripts/lib/wave-runner/cross-run-overlap.js +120 -0
  62. package/.agents/scripts/lib/wave-runner/live-probe.js +5 -1
  63. package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
  64. package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
  65. package/.agents/scripts/quality-preview.js +112 -14
  66. package/.agents/scripts/single-story-init.js +120 -17
  67. package/.agents/scripts/stories-wave-tick.js +47 -0
  68. package/.agents/scripts/story-review-compute.js +207 -0
  69. package/.agents/scripts/update-coverage-baseline.js +15 -10
  70. package/.agents/scripts/update-crap-baseline.js +12 -2
  71. package/.agents/scripts/update-maintainability-baseline.js +12 -2
  72. package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
  73. package/.agents/workflows/clean-temp.md +67 -0
  74. package/.agents/workflows/clean-worktrees.md +63 -0
  75. package/.agents/workflows/git-deliver.md +1 -1
  76. package/.agents/workflows/helpers/acceptance-self-eval.md +3 -2
  77. package/.agents/workflows/helpers/code-review.md +7 -5
  78. package/.agents/workflows/helpers/deliver-digest.md +39 -36
  79. package/.agents/workflows/helpers/deliver-reference.md +115 -5
  80. package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
  81. package/.agents/workflows/helpers/deliver-story.md +2 -1
  82. package/docs/CHANGELOG.md +39 -0
  83. package/lib/cli/registry.js +125 -18
  84. package/lib/migrations/steps/strip-removed-agentrc-keys.js +0 -5
  85. 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
- const result = spawnChild(file, args, opts);
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
  *