mandrel 2.56.0 → 2.57.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 (106) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  16. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  17. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  18. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  19. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  20. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  21. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  22. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  23. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  24. package/.agents/scripts/lib/config/explain.js +0 -19
  25. package/.agents/scripts/lib/config/limits.js +18 -78
  26. package/.agents/scripts/lib/config/quality.js +6 -3
  27. package/.agents/scripts/lib/config/runners.js +3 -2
  28. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  29. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  30. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  31. package/.agents/scripts/lib/crap-engine.js +35 -4
  32. package/.agents/scripts/lib/crap-utils.js +17 -1
  33. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  34. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  35. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  36. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  37. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  39. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  40. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  41. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  42. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  43. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  44. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  45. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  46. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  48. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
  49. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  51. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  52. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  53. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  56. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  57. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  58. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  59. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  60. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  61. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  62. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  63. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  64. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  65. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  66. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  67. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  68. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  69. package/.agents/scripts/lib/test-run-credit.js +266 -0
  70. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  71. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  72. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  73. package/.agents/scripts/plan-context.js +7 -9
  74. package/.agents/scripts/plan-critics.js +28 -54
  75. package/.agents/scripts/plan-persist.js +25 -68
  76. package/.agents/scripts/quality-preview.js +51 -0
  77. package/.agents/scripts/run-tests.js +12 -0
  78. package/.agents/scripts/stories-wave-tick.js +23 -45
  79. package/.agents/scripts/test-isolate.js +13 -180
  80. package/.agents/scripts/update-coverage-baseline.js +25 -70
  81. package/.agents/scripts/update-crap-baseline.js +19 -123
  82. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  83. package/.agents/workflows/audit-clean-code.md +4 -3
  84. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  85. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  86. package/.agents/workflows/helpers/code-review.md +2 -3
  87. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  88. package/.agents/workflows/helpers/deliver-light.md +40 -105
  89. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  90. package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
  91. package/.agents/workflows/helpers/deliver-story.md +9 -13
  92. package/.agents/workflows/helpers/plan-reference.md +132 -219
  93. package/.agents/workflows/mandrel-plan.md +27 -40
  94. package/.agents/workflows/memory-consolidate.md +9 -13
  95. package/docs/CHANGELOG.md +23 -0
  96. package/lib/migrations/index.js +4 -0
  97. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  98. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  99. package/package.json +1 -1
  100. package/.agents/scripts/lib/framework-version.js +0 -39
  101. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  102. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  103. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  104. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  105. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  106. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -0,0 +1,87 @@
1
+ /**
2
+ * lib/test-isolate/run-isolate.js — the `test-isolate` orchestration.
3
+ *
4
+ * Story #5316: moved out of `.agents/scripts/test-isolate.js` (CRAP 56 at
5
+ * cyclomatic 7, 0% coverage) so it can be driven from a test. The CLI shell
6
+ * that remains does nothing but call this and translate the exit code.
7
+ *
8
+ * `resolveFiles` and `diagnose` are named seams defaulting to the real
9
+ * implementations, per [`rules/test-seams.md`](../../../rules/test-seams.md):
10
+ * a unit test substitutes them rather than running real suites in child
11
+ * processes, and the production caller passes neither.
12
+ */
13
+
14
+ import { parseIsolateArgv } from './cli-options.js';
15
+ import { resolveTestFiles as defaultResolveTestFiles } from './list-files.js';
16
+ import { createProgressLogger } from './progress-log.js';
17
+ import { renderReport } from './render-report.js';
18
+ import { diagnoseIsolation as defaultDiagnoseIsolation } from './runner.js';
19
+
20
+ /**
21
+ * The report shape returned when no file matched the pattern — a real report
22
+ * with nothing in it, so callers never branch on null.
23
+ *
24
+ * @returns {import('./runner.js').IsolateReport}
25
+ */
26
+ function emptyReport() {
27
+ return {
28
+ pattern: null,
29
+ files: [],
30
+ isolated: [],
31
+ suite: [],
32
+ flippers: [],
33
+ bisections: [],
34
+ envMutators: [],
35
+ durationMs: 0,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Run the isolation diagnosis end to end.
41
+ *
42
+ * Exit code is 1 when the run found anything worth an operator's attention —
43
+ * a flipper OR an env mutator — and 0 otherwise. An empty match set is not a
44
+ * failure: nothing was asked of the suite, so nothing can be wrong with it.
45
+ *
46
+ * @param {object} [opts]
47
+ * @param {string[]} [opts.argv]
48
+ * @param {string} [opts.repoRoot]
49
+ * @param {(line: string) => void} [opts.onLog]
50
+ * @param {(args: {pattern: string|undefined, repoRoot: string}) => string[]} [opts.resolveFiles]
51
+ * @param {(args: object) => Promise<import('./runner.js').IsolateReport>} [opts.diagnose]
52
+ * @returns {Promise<{ exitCode: number, report: import('./runner.js').IsolateReport }>}
53
+ */
54
+ export async function runTestIsolate({
55
+ argv = [],
56
+ repoRoot,
57
+ onLog = (s) => process.stdout.write(`${s}\n`),
58
+ resolveFiles = defaultResolveTestFiles,
59
+ diagnose = defaultDiagnoseIsolation,
60
+ } = {}) {
61
+ const options = parseIsolateArgv(argv);
62
+ const files = resolveFiles({ pattern: options.pattern, repoRoot });
63
+ if (files.length === 0) {
64
+ onLog(
65
+ `[test-isolate] no test files matched pattern: ${options.pattern ?? '<default>'}`,
66
+ );
67
+ return { exitCode: 0, report: emptyReport() };
68
+ }
69
+
70
+ if (!options.quiet) {
71
+ onLog(`[test-isolate] scanning ${files.length} file(s)...`);
72
+ }
73
+ const report = await diagnose({
74
+ repoRoot,
75
+ files,
76
+ workers: options.workers,
77
+ suiteConcurrency: options.suiteConcurrency,
78
+ maxBisectDepth: options.maxBisectDepth,
79
+ maxBisectTargets: options.maxBisectTargets,
80
+ onProgress: options.quiet ? undefined : createProgressLogger(onLog),
81
+ });
82
+
83
+ onLog(options.json ? JSON.stringify(report, null, 2) : renderReport(report));
84
+
85
+ const clean = report.flippers.length === 0 && report.envMutators.length === 0;
86
+ return { exitCode: clean ? 0 : 1, report };
87
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * lib/test-run-credit.js — let a green bare `npm test` earn the credit close
3
+ * reads (Story #5313).
4
+ *
5
+ * Until this module the only suite run that deposited credit was the one
6
+ * shaped exactly like the close gate — `coverage-capture.js --cwd <worktree>`
7
+ * or `evidence-gate.js --standalone … -- npm test` — and the digest, the
8
+ * worker boot context and the reference all carried prose explaining which
9
+ * invocation to type. A worker that ran the project's own test runner paid
10
+ * for the suite and then close paid for it again. The runner is the natural
11
+ * depositor: it knows the tree it ran against, whether the run was green,
12
+ * and whether it ran the whole suite.
13
+ *
14
+ * On a green **full-tier** run inside a `story-<id>` checkout the runner
15
+ * records the `test` gate's evidence in the same keyspace
16
+ * `close-validation/runner.js` consults — keyed on HEAD and the tree
17
+ * fingerprint, hashed on the exact `{ cmd: 'npm', args: ['test'], cwd }`
18
+ * close will spawn — so close's `test` gate short-circuits at unchanged HEAD.
19
+ * The freshness keying is untouched: a later commit voids the record exactly
20
+ * as it voids one `evidence-gate.js` wrote.
21
+ *
22
+ * What it deliberately does **not** do is write the coverage capture stamp:
23
+ * that stamp is a claim that `coverage/coverage-final.json` describes this
24
+ * tree, and a bare `npm test` produces no such artifact. The CRAP gate still
25
+ * runs `coverage-capture.js` when it needs one.
26
+ *
27
+ * Total: every failure — not a Story branch, no git, an unwritable evidence
28
+ * file — is reported by reason and never fails the test run that earned it.
29
+ *
30
+ * @module lib/test-run-credit
31
+ */
32
+
33
+ import path from 'node:path';
34
+
35
+ import { gitSpawn as defaultGitSpawn } from './git-utils.js';
36
+ import {
37
+ recordPass as defaultRecordPass,
38
+ shouldSkip as defaultShouldSkip,
39
+ hashCommandConfig,
40
+ treeFingerprint,
41
+ } from './validation-evidence.js';
42
+
43
+ /** The gate name close's runner looks the record up under. */
44
+ const GATE_NAME = 'test';
45
+
46
+ /** The exact command close spawns for that gate — the hash must match it. */
47
+ const GATE_COMMAND = Object.freeze({
48
+ cmd: 'npm',
49
+ args: Object.freeze(['test']),
50
+ });
51
+
52
+ /**
53
+ * Read one trimmed git stdout line, or `null` on any failure.
54
+ *
55
+ * @param {Function} gitSpawnFn
56
+ * @param {string} cwd
57
+ * @param {string[]} args
58
+ * @returns {string|null}
59
+ */
60
+ function gitLine(gitSpawnFn, cwd, args) {
61
+ try {
62
+ const res = gitSpawnFn(cwd, ...args);
63
+ if (res?.status !== 0) return null;
64
+ const line = String(res.stdout ?? '').trim();
65
+ return line.length > 0 ? line : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * The Story id a checkout's branch names, or `null` off a Story branch.
73
+ *
74
+ * @param {string|null} branch
75
+ * @returns {number|null}
76
+ */
77
+ export function storyIdFromBranch(branch) {
78
+ const match = /^story-(\d+)$/.exec(branch ?? '');
79
+ if (!match) return null;
80
+ const id = Number.parseInt(match[1], 10);
81
+ return Number.isInteger(id) && id > 0 ? id : null;
82
+ }
83
+
84
+ /**
85
+ * The checkout whose temp tree holds the evidence keyspace: the **main**
86
+ * checkout, which is where close runs with `--cwd <main-repo>`. From a
87
+ * worktree `git rev-parse --git-common-dir` names the main `.git`; from the
88
+ * main checkout it names its own. Either way the parent is the checkout.
89
+ *
90
+ * @param {string} cwd
91
+ * @param {Function} gitSpawnFn
92
+ * @returns {string|null}
93
+ */
94
+ export function resolveEvidenceRoot(cwd, gitSpawnFn) {
95
+ const common = gitLine(gitSpawnFn, cwd, ['rev-parse', '--git-common-dir']);
96
+ if (!common) return null;
97
+ const absolute = path.resolve(cwd, common);
98
+ return path.basename(absolute) === '.git' ? path.dirname(absolute) : null;
99
+ }
100
+
101
+ /**
102
+ * Deposit the `test` gate's evidence for a green full-suite run.
103
+ *
104
+ * @param {{
105
+ * cwd: string,
106
+ * tier?: string,
107
+ * status?: number,
108
+ * durationMs?: number|null,
109
+ * gitSpawnFn?: typeof defaultGitSpawn,
110
+ * recordPassFn?: typeof defaultRecordPass,
111
+ * }} args
112
+ * @returns {{ deposited: boolean, reason: string, storyId?: number, sha?: string }}
113
+ */
114
+ export function depositTestRunCredit({
115
+ cwd,
116
+ tier = 'full',
117
+ status = 0,
118
+ durationMs = null,
119
+ gitSpawnFn = defaultGitSpawn,
120
+ recordPassFn = defaultRecordPass,
121
+ } = {}) {
122
+ if (status !== 0) return { deposited: false, reason: 'run-not-green' };
123
+ if (tier !== 'full') return { deposited: false, reason: 'not-full-tier' };
124
+ const storyId = storyIdFromBranch(
125
+ gitLine(gitSpawnFn, cwd, ['rev-parse', '--abbrev-ref', 'HEAD']),
126
+ );
127
+ if (storyId === null)
128
+ return { deposited: false, reason: 'not-a-story-branch' };
129
+ const sha = gitLine(gitSpawnFn, cwd, ['rev-parse', 'HEAD']);
130
+ const evidenceRoot = resolveEvidenceRoot(cwd, gitSpawnFn);
131
+ if (!sha || !evidenceRoot) {
132
+ return { deposited: false, reason: 'tree-unreadable', storyId };
133
+ }
134
+ return writeRecord({
135
+ storyId,
136
+ sha,
137
+ cwd,
138
+ evidenceRoot,
139
+ durationMs,
140
+ gitSpawnFn,
141
+ recordPassFn,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Deposit and say so on stderr — the runner's one-line hook. The line is
147
+ * the only surface a worker sees, so it names the outcome by reason.
148
+ *
149
+ * @param {Parameters<typeof depositTestRunCredit>[0] & { log?: (line: string) => void }} args
150
+ * @returns {ReturnType<typeof depositTestRunCredit>}
151
+ */
152
+ export function reportTestRunCredit({
153
+ log = (line) => process.stderr.write(`${line}\n`),
154
+ ...args
155
+ } = {}) {
156
+ const credit = depositTestRunCredit(args);
157
+ log(
158
+ credit.deposited
159
+ ? `[run-tests] ✓ deposited the close test credit for story #${credit.storyId} at ${credit.sha.slice(0, 7)}`
160
+ : `[run-tests] no close test credit deposited (${credit.reason})`,
161
+ );
162
+ return credit;
163
+ }
164
+
165
+ /**
166
+ * Is the `test` gate already credited for this tree — did a green bare
167
+ * `npm test` in the Story worktree deposit its evidence? The read side of
168
+ * {@link depositTestRunCredit}, consulted by `close-validation/gates.js`.
169
+ *
170
+ * When it did, close registers the plain `test` gate beside the capture gate
171
+ * even though coverage-capture is the active test runner: the runner's
172
+ * evidence check then skips it as credited, so close REPORTS the suite the
173
+ * worker already ran instead of silently folding it into the capture. It is
174
+ * never a second spend — an uncredited tree resolves `false` and the
175
+ * pre-#5313 shape (capture alone runs the suite) is unchanged. Every
176
+ * uncertainty resolves `false`.
177
+ *
178
+ * @param {{
179
+ * storyId?: number|null,
180
+ * cwd?: string,
181
+ * evidenceCwd?: string|null,
182
+ * gitSpawnImpl?: typeof defaultGitSpawn,
183
+ * shouldSkipImpl?: typeof defaultShouldSkip,
184
+ * log?: (line: string) => void,
185
+ * }} opts
186
+ * @returns {boolean}
187
+ */
188
+ export function predictsTestEvidenceCredit({
189
+ storyId,
190
+ cwd,
191
+ evidenceCwd,
192
+ gitSpawnImpl = defaultGitSpawn,
193
+ shouldSkipImpl = defaultShouldSkip,
194
+ log,
195
+ } = {}) {
196
+ if (!Number.isInteger(storyId) || storyId <= 0) return false;
197
+ if (typeof cwd !== 'string' || cwd.length === 0) return false;
198
+ try {
199
+ const sha = gitLine(gitSpawnImpl, cwd, ['rev-parse', 'HEAD']);
200
+ if (!sha) return false;
201
+ const verdict = shouldSkipImpl(
202
+ {
203
+ storyId,
204
+ gateName: GATE_NAME,
205
+ currentSha: sha,
206
+ configHash: hashCommandConfig({
207
+ cmd: GATE_COMMAND.cmd,
208
+ args: [...GATE_COMMAND.args],
209
+ cwd: path.resolve(cwd),
210
+ }),
211
+ inputFingerprint: treeFingerprint(cwd, gitSpawnImpl),
212
+ },
213
+ { cwd: evidenceCwd ?? cwd, standalone: true },
214
+ );
215
+ if (verdict.skip !== true) return false;
216
+ log?.(
217
+ '[close-validation] a green `npm test` already deposited the test credit for this tree — registering the plain `test` gate so close reports it as credited.',
218
+ );
219
+ return true;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * The write itself, split out so the guard chain above stays flat.
227
+ *
228
+ * @param {{ storyId: number, sha: string, cwd: string, evidenceRoot: string, durationMs: number|null, gitSpawnFn: Function, recordPassFn: Function }} args
229
+ * @returns {{ deposited: boolean, reason: string, storyId: number, sha: string }}
230
+ */
231
+ function writeRecord({
232
+ storyId,
233
+ sha,
234
+ cwd,
235
+ evidenceRoot,
236
+ durationMs,
237
+ gitSpawnFn,
238
+ recordPassFn,
239
+ }) {
240
+ try {
241
+ recordPassFn(
242
+ {
243
+ storyId,
244
+ gateName: GATE_NAME,
245
+ sha,
246
+ configHash: hashCommandConfig({
247
+ cmd: GATE_COMMAND.cmd,
248
+ args: [...GATE_COMMAND.args],
249
+ cwd: path.resolve(cwd),
250
+ }),
251
+ exitCode: 0,
252
+ durationMs,
253
+ inputFingerprint: treeFingerprint(cwd, gitSpawnFn),
254
+ },
255
+ { cwd: evidenceRoot, standalone: true },
256
+ );
257
+ return { deposited: true, reason: 'recorded', storyId, sha };
258
+ } catch (err) {
259
+ return {
260
+ deposited: false,
261
+ reason: `record-failed: ${err?.message ?? err}`,
262
+ storyId,
263
+ sha,
264
+ };
265
+ }
266
+ }