mandrel 2.58.0 → 2.60.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/README.md +17 -12
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +12 -13
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +9 -5
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +5 -7
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/maintainability-engine.js +3 -3
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +44 -50
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +28 -33
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -39
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/audit-to-stories.md +14 -11
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +82 -60
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +32 -30
- package/.agents/workflows/mandrel-update.md +36 -21
- package/README.md +3 -3
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +45 -25
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +8 -2
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- package/.agents/skills/core/scope-triage/SKILL.md +0 -48
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* deliver-run.js — one beat of a multi-Story `/mandrel-deliver` run
|
|
5
|
+
* (Story #5345).
|
|
6
|
+
*
|
|
7
|
+
* The multi-Story path used to be a hand-driven protocol: loop
|
|
8
|
+
* `stories-wave-tick.js`, keep an append-only `--dispatched` list across
|
|
9
|
+
* beats, paste a `node --input-type=module -e` block to build each worker's
|
|
10
|
+
* checklist, and remember to add `--merge-watch-mode async` to every close
|
|
11
|
+
* because close cannot see run topology. Four pieces of bookkeeping, all of
|
|
12
|
+
* them carried in the session's head, each of them silently wrong when
|
|
13
|
+
* forgotten — a dropped `--dispatched` id joins a second worker to a live
|
|
14
|
+
* branch, and a forgotten async flag serializes the run's merge waits.
|
|
15
|
+
*
|
|
16
|
+
* This CLI is that bookkeeping, scripted. One beat per invocation:
|
|
17
|
+
*
|
|
18
|
+
* 1. Tick with `--probe-live`, seeding `dispatched` from the **run ledger**
|
|
19
|
+
* rather than from the caller (`<tempRoot>/run-<id>/ledger.json`).
|
|
20
|
+
* 2. Write one dispatch prompt per ready Story — id, `workCwd` conventions,
|
|
21
|
+
* docs digest path, checklist path, change-set discipline — so the
|
|
22
|
+
* session's spawn is "use this file as the prompt" and nothing else.
|
|
23
|
+
* 3. Print the exact `single-story-close.js` command for every hand-off,
|
|
24
|
+
* with `--merge-watch-mode async` decided from run topology here, where
|
|
25
|
+
* the topology is known.
|
|
26
|
+
*
|
|
27
|
+
* Stdout is one compact JSON envelope (`kind: "deliver-run-beat"`). The
|
|
28
|
+
* tick's exit-code contract is preserved byte for byte — 0 ok · 1 input
|
|
29
|
+
* error · 2 cycle · 3 wedged · 4 blocked — because the loop's stopping rules
|
|
30
|
+
* are the tick's and this script must not invent a second dialect of them.
|
|
31
|
+
*
|
|
32
|
+
* The ledger is append-only on purpose, which is also its one sharp edge: an
|
|
33
|
+
* id whose spawn never reached `single-story-init.js` stays in it, is withheld
|
|
34
|
+
* as in flight on every later beat, and the run reads as "waiting" forever.
|
|
35
|
+
* The beat therefore reports every ledgered id live state still calls
|
|
36
|
+
* `agent::ready` in `stalledDispatch[]` with the recovery in
|
|
37
|
+
* `stalledDispatchReason`. It never auto-releases one: a slow init and a dead
|
|
38
|
+
* spawn are indistinguishable at this altitude, and releasing the first joins
|
|
39
|
+
* a second worker to a live branch (Story #5363).
|
|
40
|
+
*
|
|
41
|
+
* Scheduling itself is untouched: the ready set, the concurrency cap, the
|
|
42
|
+
* footprint guard and the foreign-lease withholding all come from
|
|
43
|
+
* `stories-wave-tick.js#runProbedStoriesWaveTick`. This is a ledger, a
|
|
44
|
+
* prompt writer and a command renderer around that one beat.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { createHash } from 'node:crypto';
|
|
48
|
+
import fs from 'node:fs';
|
|
49
|
+
import path from 'node:path';
|
|
50
|
+
import { parseArgs } from 'node:util';
|
|
51
|
+
import { buildDispatchChecklist } from './lib/audit-suite/index.js';
|
|
52
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
53
|
+
import { getPaths, resolveConfig } from './lib/config-resolver.js';
|
|
54
|
+
import { Logger } from './lib/Logger.js';
|
|
55
|
+
import { ensureDocsDigest } from './lib/orchestration/docs-digest.js';
|
|
56
|
+
import { parse as parseStoryBody } from './lib/story-body/story-body.js';
|
|
57
|
+
import { expandIdList } from './lib/util/parse-id-list.js';
|
|
58
|
+
import { runProbedStoriesWaveTick } from './stories-wave-tick.js';
|
|
59
|
+
|
|
60
|
+
const INPUT_ERROR_EXIT_CODE = 1;
|
|
61
|
+
|
|
62
|
+
const USAGE = {
|
|
63
|
+
invocation:
|
|
64
|
+
'node .agents/scripts/deliver-run.js --stories <ids> [--handoff <id>]... [--concurrency <n>] [--run-id <id>] [--cwd <path>]',
|
|
65
|
+
summary:
|
|
66
|
+
'Run one beat of a multi-Story delivery: tick from live state, write a dispatch prompt per ready Story, and render the close command for each hand-off. Prints one JSON envelope.',
|
|
67
|
+
flags: [
|
|
68
|
+
[
|
|
69
|
+
'--stories <ids>',
|
|
70
|
+
'Story ids in the run. Singles, commas and inclusive A-B ranges (5340,5342-5345).',
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
'--handoff <id>',
|
|
74
|
+
'A Story whose worker has pushed its branch. Repeatable. Each one gets a close[] entry carrying the exact single-story-close.js command.',
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
'--concurrency <n>',
|
|
78
|
+
'Per-beat concurrency override, forwarded to the tick. Omit it so delivery.deliverRunner.concurrencyCap (and any .agentrc.local.json override) wins.',
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
'--run-id <id>',
|
|
82
|
+
'Pin the run directory under <tempRoot>. Default: a stable digest of the Story id set, so every beat of the same run finds the same ledger.',
|
|
83
|
+
],
|
|
84
|
+
['--cwd <path>', 'Main checkout. Default: the current directory.'],
|
|
85
|
+
],
|
|
86
|
+
notes: [
|
|
87
|
+
'The run ledger (<tempRoot>/run-<id>/ledger.json) records every id handed out\nas ready, so a repeat beat withholds it with no --dispatched bookkeeping from\nthe caller. It is additive: the tick still filters it against live state, so a\nledgered id that has since gone agent::done is dropped for you.',
|
|
88
|
+
'A ledgered id that live state still reports as agent::ready is named in\nstalledDispatch[], with stalledDispatchReason carrying the recovery. It is a\nreport, never a release: a slow init and a dead spawn read alike here, so the\noperator edits the ledger and re-beats with --run-id.',
|
|
89
|
+
'--merge-watch-mode async is added to every close command when the run holds\nmore than one Story, and omitted for a run of one. Close sees a single Story\nand cannot make that call for itself.',
|
|
90
|
+
'Exit codes:\n 0 beat emitted\n 1 input error\n 2 dependency cycle (cycleError)\n 3 wedged\n 4 blocked — the HITL pause; stop the loop, do not poll',
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Derive the run's stable identity from its Story id set.
|
|
96
|
+
*
|
|
97
|
+
* The identity must be the same on every beat of one run (so the ledger is
|
|
98
|
+
* found again) and different across runs (so two concurrent deliveries do not
|
|
99
|
+
* share a dispatched list). The sorted id set is the only thing that satisfies
|
|
100
|
+
* both — a timestamp fails the first, and a single id fails the second.
|
|
101
|
+
*
|
|
102
|
+
* @param {number[]} ids
|
|
103
|
+
* @returns {string} an 8-hex-character digest
|
|
104
|
+
*/
|
|
105
|
+
export function deriveRunId(ids) {
|
|
106
|
+
const key = [...new Set(ids)].sort((a, b) => a - b).join(',');
|
|
107
|
+
return createHash('sha1').update(key).digest('hex').slice(0, 8);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read the run ledger's dispatched ids, tolerating absence and corruption.
|
|
112
|
+
*
|
|
113
|
+
* A ledger that cannot be read is treated as empty rather than fatal: the
|
|
114
|
+
* consequence is one extra beat of the init window (which `--dispatched`
|
|
115
|
+
* existed to close), whereas refusing the beat would strand a live run on a
|
|
116
|
+
* bookkeeping artifact.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} ledgerPath
|
|
119
|
+
* @param {{ readFileFn?: (p: string, enc: string) => string }} [deps]
|
|
120
|
+
* @returns {number[]}
|
|
121
|
+
*/
|
|
122
|
+
export function readLedgerDispatched(
|
|
123
|
+
ledgerPath,
|
|
124
|
+
{ readFileFn = fs.readFileSync } = {},
|
|
125
|
+
) {
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = readFileFn(ledgerPath, 'utf8');
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(raw);
|
|
134
|
+
const ids = Array.isArray(parsed?.dispatched) ? parsed.dispatched : [];
|
|
135
|
+
return ids.filter((id) => Number.isInteger(id) && id > 0);
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Persist the union of the previously-ledgered ids and the ids handed out on
|
|
143
|
+
* this beat. Append-only by construction: nothing here removes an id, so the
|
|
144
|
+
* caller cannot get the "forgot to re-list one" failure `--dispatched` had.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} args
|
|
147
|
+
* @param {string} args.ledgerPath
|
|
148
|
+
* @param {string} args.runId
|
|
149
|
+
* @param {number[]} args.stories
|
|
150
|
+
* @param {number[]} args.dispatched ids already ledgered
|
|
151
|
+
* @param {number[]} args.ready ids handed out this beat
|
|
152
|
+
* @param {{ writeFileFn?: (p: string, c: string, enc: string) => void }} [deps]
|
|
153
|
+
* @returns {number[]} the persisted dispatched set
|
|
154
|
+
*/
|
|
155
|
+
function writeLedger(
|
|
156
|
+
{ ledgerPath, runId, stories, dispatched, ready },
|
|
157
|
+
{ writeFileFn = fs.writeFileSync } = {},
|
|
158
|
+
) {
|
|
159
|
+
const merged = [...new Set([...dispatched, ...ready])].sort((a, b) => a - b);
|
|
160
|
+
const payload = {
|
|
161
|
+
kind: 'deliver-run-ledger',
|
|
162
|
+
runId,
|
|
163
|
+
stories,
|
|
164
|
+
dispatched: merged,
|
|
165
|
+
updatedAt: new Date().toISOString(),
|
|
166
|
+
};
|
|
167
|
+
writeFileFn(ledgerPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
168
|
+
return merged;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Render the close command for one hand-off.
|
|
173
|
+
*
|
|
174
|
+
* `--merge-watch-mode async` is a **run-topology** decision, which is why it
|
|
175
|
+
* is made here: the close process sees one Story and cannot tell whether a
|
|
176
|
+
* sibling is queued behind its merge wait. On a multi-Story run the serialized
|
|
177
|
+
* close tail is the dominant cost, and each synchronous close holds the
|
|
178
|
+
* foreground for its full merge wait before the next may start; on a run of
|
|
179
|
+
* one there is no sibling to unblock and the sync default reaches `landed`
|
|
180
|
+
* fastest.
|
|
181
|
+
*
|
|
182
|
+
* @param {{ storyId: number, mainRepo: string, storyCount: number }} args
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
export function renderCloseCommand({ storyId, mainRepo, storyCount }) {
|
|
186
|
+
const parts = [
|
|
187
|
+
'node',
|
|
188
|
+
path.join(mainRepo, '.agents', 'scripts', 'single-story-close.js'),
|
|
189
|
+
`--story ${storyId}`,
|
|
190
|
+
`--cwd ${mainRepo}`,
|
|
191
|
+
];
|
|
192
|
+
if (storyCount > 1) parts.push('--merge-watch-mode async');
|
|
193
|
+
return parts.join(' ');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Render one ready Story's dispatch prompt — the whole spawn payload, so the
|
|
198
|
+
* session's `Agent` call is this file and nothing else.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} args
|
|
201
|
+
* @param {number} args.storyId
|
|
202
|
+
* @param {string} args.mainRepo
|
|
203
|
+
* @param {string|null} args.docsDigestPath
|
|
204
|
+
* @param {string|null} args.checklistPath
|
|
205
|
+
* @returns {string} markdown
|
|
206
|
+
*/
|
|
207
|
+
export function renderDispatchPrompt({
|
|
208
|
+
storyId,
|
|
209
|
+
mainRepo,
|
|
210
|
+
docsDigestPath,
|
|
211
|
+
checklistPath,
|
|
212
|
+
}) {
|
|
213
|
+
const lines = [
|
|
214
|
+
`# Deliver Story #${storyId}`,
|
|
215
|
+
'',
|
|
216
|
+
`Main checkout: \`${mainRepo}\`.`,
|
|
217
|
+
'',
|
|
218
|
+
`You own Steps 0 through 2.5 of \`.agents/workflows/helpers/deliver-story.md\`.`,
|
|
219
|
+
'The orchestrator owns Step 3 (close): do not open a PR, do not run',
|
|
220
|
+
'`single-story-close.js`, and do not compose a terminal envelope.',
|
|
221
|
+
'',
|
|
222
|
+
'## Reads',
|
|
223
|
+
'',
|
|
224
|
+
'1. `.agents/workflows/helpers/deliver-digest.md` — once, first.',
|
|
225
|
+
'2. `.agents/workflows/helpers/deliver-story.md` — the steps.',
|
|
226
|
+
`3. The Story body (\`gh issue view ${storyId}\`) — its \`## Spec\`,`,
|
|
227
|
+
' `acceptance[]` and `verify[]` are the contract.',
|
|
228
|
+
'',
|
|
229
|
+
`- Docs digest: ${docsDigestPath ? `\`${docsDigestPath}\`` : 'none (project.docsContextFiles is unset) — no mandatory docs read'}`,
|
|
230
|
+
`- Write-time checklist: ${checklistPath ? `\`${checklistPath}\`` : 'none matched this footprint — the maker-blind close-scope pass still covers it'}`,
|
|
231
|
+
'',
|
|
232
|
+
'## Worktree',
|
|
233
|
+
'',
|
|
234
|
+
'Initialize from the main checkout, synchronously, at the maximum Bash',
|
|
235
|
+
'timeout — never in the background:',
|
|
236
|
+
'',
|
|
237
|
+
'```bash',
|
|
238
|
+
`node ${path.join(mainRepo, '.agents', 'scripts', 'single-story-init.js')} --story ${storyId}`,
|
|
239
|
+
'```',
|
|
240
|
+
'',
|
|
241
|
+
'Capture `workCwd` from its envelope and prefix **every** path-based',
|
|
242
|
+
'Read/Edit/Write with that absolute worktree root — `cd` alone does not',
|
|
243
|
+
'scope those tools. `remoteVerified: false` → flip `agent::blocked` quoting',
|
|
244
|
+
'`remoteProbe.detail` and stop.',
|
|
245
|
+
'',
|
|
246
|
+
'## Change-set discipline',
|
|
247
|
+
'',
|
|
248
|
+
'Derive the change set, the level and the ceremony with **one** call, and',
|
|
249
|
+
'hand that one list to the verdict owner — never let a critic re-run its',
|
|
250
|
+
'own `git diff`:',
|
|
251
|
+
'',
|
|
252
|
+
'```bash',
|
|
253
|
+
`node ${path.join(mainRepo, '.agents', 'scripts', 'ceremony-derive.js')} --story ${storyId} --cwd <workCwd>`,
|
|
254
|
+
'```',
|
|
255
|
+
'',
|
|
256
|
+
'## Hand-off',
|
|
257
|
+
'',
|
|
258
|
+
'Run the bounded acceptance self-eval (digest § 4), the one credited suite',
|
|
259
|
+
'run (digest § 5), then push `story-' + storyId + '` to `origin` and',
|
|
260
|
+
'confirm the remote ref moved. Return: Story id, `workCwd`, branch, pushed',
|
|
261
|
+
'head SHA, the self-eval verdict, and the `verify[]` evidence. Say the',
|
|
262
|
+
'branch is pushed and unclosed.',
|
|
263
|
+
'',
|
|
264
|
+
];
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Build one ready Story's dispatch prompt file and return its entry.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} args
|
|
272
|
+
* @param {number} args.storyId
|
|
273
|
+
* @param {string} args.body the Story body the probe already fetched
|
|
274
|
+
* @param {string} args.runTempDir
|
|
275
|
+
* @param {string} args.mainRepo
|
|
276
|
+
* @param {string|null} args.docsDigestPath
|
|
277
|
+
* @param {object} [deps]
|
|
278
|
+
* @returns {{ id: number, promptPath: string }}
|
|
279
|
+
*/
|
|
280
|
+
function buildDispatchEntry(
|
|
281
|
+
{ storyId, body, runTempDir, mainRepo, docsDigestPath },
|
|
282
|
+
{
|
|
283
|
+
buildChecklistFn = buildDispatchChecklist,
|
|
284
|
+
writeFileFn = fs.writeFileSync,
|
|
285
|
+
} = {},
|
|
286
|
+
) {
|
|
287
|
+
let changes = [];
|
|
288
|
+
let references = [];
|
|
289
|
+
try {
|
|
290
|
+
// `parse` returns `{ body, warnings, info }` — the path entries live on
|
|
291
|
+
// `.body`, not at the top level. The retired `node -e` snippet in
|
|
292
|
+
// `helpers/deliver-reference.md` destructured the top level and so built
|
|
293
|
+
// every checklist from an empty footprint.
|
|
294
|
+
const { body: parsed } = parseStoryBody(body ?? '');
|
|
295
|
+
changes = parsed?.changes ?? [];
|
|
296
|
+
references = parsed?.references ?? [];
|
|
297
|
+
} catch {
|
|
298
|
+
// An unparseable body costs a footprint-matched checklist, never the
|
|
299
|
+
// dispatch: the worker reads the real Story body itself, and close-scope
|
|
300
|
+
// lens coverage runs maker-blind regardless.
|
|
301
|
+
changes = [];
|
|
302
|
+
references = [];
|
|
303
|
+
}
|
|
304
|
+
const { checklistPath } = buildChecklistFn({
|
|
305
|
+
storyId,
|
|
306
|
+
changes,
|
|
307
|
+
references,
|
|
308
|
+
runTempDir,
|
|
309
|
+
});
|
|
310
|
+
const promptPath = path.join(runTempDir, `dispatch-${storyId}.md`);
|
|
311
|
+
writeFileFn(
|
|
312
|
+
promptPath,
|
|
313
|
+
renderDispatchPrompt({
|
|
314
|
+
storyId,
|
|
315
|
+
mainRepo,
|
|
316
|
+
docsDigestPath,
|
|
317
|
+
checklistPath,
|
|
318
|
+
}),
|
|
319
|
+
'utf8',
|
|
320
|
+
);
|
|
321
|
+
return { id: storyId, promptPath };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Flatten the two withhold reports the tick emits into one list, so an
|
|
326
|
+
* unfilled dispatch slot is explained in a single place.
|
|
327
|
+
*
|
|
328
|
+
* @param {object} envelope the tick envelope
|
|
329
|
+
* @returns {Array<{id: number, blockedBy: number, reason: string, paths: string[]}>}
|
|
330
|
+
*/
|
|
331
|
+
function collectWithheld(envelope) {
|
|
332
|
+
const reservation = envelope?.inFlightReservation?.withheld ?? [];
|
|
333
|
+
const guard = envelope?.footprintGuard?.withheld ?? [];
|
|
334
|
+
return [
|
|
335
|
+
...reservation.map((w) => ({
|
|
336
|
+
id: w.id,
|
|
337
|
+
blockedBy: w.blockedBy,
|
|
338
|
+
reason: w.reason ?? 'in-flight-earlier-beat',
|
|
339
|
+
paths: w.paths ?? [],
|
|
340
|
+
})),
|
|
341
|
+
...guard.map((w) => ({
|
|
342
|
+
id: w.id,
|
|
343
|
+
blockedBy: w.blockedBy,
|
|
344
|
+
reason: 'beat-peer',
|
|
345
|
+
paths: w.paths ?? [],
|
|
346
|
+
})),
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Render the operator-facing reason for the ledgered ids live state still
|
|
352
|
+
* reports as `agent::ready`.
|
|
353
|
+
*
|
|
354
|
+
* **Report, do not release.** A slow `single-story-init.js` and a spawn that
|
|
355
|
+
* died before reaching one look identical from here, and releasing the first
|
|
356
|
+
* re-dispatches a live Story onto its own branch — the exact failure the
|
|
357
|
+
* ledger exists to prevent. So the beat names the id and hands the operator
|
|
358
|
+
* the two things the call needs: where the ledger lives, and the flag that
|
|
359
|
+
* pins the run directory once they have edited it. Neither is derivable from
|
|
360
|
+
* the envelope's other fields, which is why both are spelled out here rather
|
|
361
|
+
* than left to a reader of the source.
|
|
362
|
+
*
|
|
363
|
+
* @param {number[]} ids
|
|
364
|
+
* @param {{ ledgerPath: string, runId: string }} run
|
|
365
|
+
* @returns {string|null} null when nothing is stalled
|
|
366
|
+
*/
|
|
367
|
+
export function renderStalledDispatchReason(ids, { ledgerPath, runId }) {
|
|
368
|
+
if (!Array.isArray(ids) || ids.length === 0) return null;
|
|
369
|
+
const list = ids.map((id) => `#${id}`).join(', ');
|
|
370
|
+
const subject = ids.length === 1 ? 'it' : 'they';
|
|
371
|
+
return (
|
|
372
|
+
`${list}: handed out as ready on an earlier beat, but live state still ` +
|
|
373
|
+
`reports ${subject} as agent::ready. Either single-story-init.js is still ` +
|
|
374
|
+
'running, or the spawn never reached it and the id is pinned in flight ' +
|
|
375
|
+
'for every later beat. This beat does not release it — re-dispatching a ' +
|
|
376
|
+
'live Story onto its own branch is the failure the ledger prevents. ' +
|
|
377
|
+
'Confirm no worker is running, then remove the id from "dispatched" in ' +
|
|
378
|
+
`${ledgerPath} and beat again with --run-id ${runId} so the same run ` +
|
|
379
|
+
'directory is reused.'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The input-error result, shaped like a beat envelope so a caller branching on
|
|
385
|
+
* `kind` never has to special-case the failure.
|
|
386
|
+
*
|
|
387
|
+
* @param {string} message
|
|
388
|
+
* @returns {{ envelope: object, exitCode: 1 }}
|
|
389
|
+
*/
|
|
390
|
+
function inputError(message) {
|
|
391
|
+
return {
|
|
392
|
+
envelope: { kind: 'deliver-run-beat', inputError: message },
|
|
393
|
+
exitCode: INPUT_ERROR_EXIT_CODE,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Resolve the ids for `--stories` and `--handoff`, or the error to report.
|
|
399
|
+
*
|
|
400
|
+
* @param {{ stories?: string, handoff?: string[] }} args
|
|
401
|
+
* @returns {{ ids: number[]|null, handoffIds: number[], error: string|null }}
|
|
402
|
+
*/
|
|
403
|
+
export function resolveRunIds({ stories, handoff }) {
|
|
404
|
+
const { ids, error } = expandIdList(stories, {
|
|
405
|
+
flag: '--stories',
|
|
406
|
+
prefix: '[deliver-run] ',
|
|
407
|
+
});
|
|
408
|
+
if (error) return { ids: null, handoffIds: [], error };
|
|
409
|
+
if (ids.length === 0) {
|
|
410
|
+
return {
|
|
411
|
+
ids: null,
|
|
412
|
+
handoffIds: [],
|
|
413
|
+
error:
|
|
414
|
+
'[deliver-run] --stories is required: node .agents/scripts/deliver-run.js --stories 5340,5341',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const raw = (Array.isArray(handoff) ? handoff : [handoff]).filter(
|
|
418
|
+
(v) => v != null && v !== '',
|
|
419
|
+
);
|
|
420
|
+
const handoffIds = [];
|
|
421
|
+
for (const token of raw) {
|
|
422
|
+
const expanded = expandIdList(String(token), {
|
|
423
|
+
flag: '--handoff',
|
|
424
|
+
prefix: '[deliver-run] ',
|
|
425
|
+
});
|
|
426
|
+
if (expanded.error)
|
|
427
|
+
return { ids: null, handoffIds: [], error: expanded.error };
|
|
428
|
+
handoffIds.push(...expanded.ids);
|
|
429
|
+
}
|
|
430
|
+
return { ids, handoffIds: [...new Set(handoffIds)], error: null };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Run one beat: tick, write the dispatch prompts, persist the ledger, render
|
|
435
|
+
* the close commands, and shape the envelope.
|
|
436
|
+
*
|
|
437
|
+
* Every collaborator is injectable so the beat is testable without a provider,
|
|
438
|
+
* a network call or the repository's own temp root.
|
|
439
|
+
*
|
|
440
|
+
* @param {object} args
|
|
441
|
+
* @param {string} args.stories raw `--stories` value
|
|
442
|
+
* @param {string[]} [args.handoff] raw `--handoff` values
|
|
443
|
+
* @param {string} [args.concurrency] raw `--concurrency` value
|
|
444
|
+
* @param {string} [args.runId] explicit run id
|
|
445
|
+
* @param {string} [args.cwd] main checkout
|
|
446
|
+
* @param {object} [args.config] pre-resolved config (test injection)
|
|
447
|
+
* @param {Function} [args.probe] tick probe seam (test injection)
|
|
448
|
+
* @param {Function} [args.context] tick provider-context seam
|
|
449
|
+
* @param {object} [deps]
|
|
450
|
+
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
451
|
+
*/
|
|
452
|
+
export async function runDeliverRunBeat(
|
|
453
|
+
{
|
|
454
|
+
stories,
|
|
455
|
+
handoff = [],
|
|
456
|
+
concurrency,
|
|
457
|
+
runId: runIdOverride,
|
|
458
|
+
cwd,
|
|
459
|
+
config,
|
|
460
|
+
probe,
|
|
461
|
+
context,
|
|
462
|
+
} = {},
|
|
463
|
+
deps = {},
|
|
464
|
+
) {
|
|
465
|
+
const {
|
|
466
|
+
tickFn = runProbedStoriesWaveTick,
|
|
467
|
+
resolveConfigFn = resolveConfig,
|
|
468
|
+
ensureDocsDigestFn = ensureDocsDigest,
|
|
469
|
+
mkdirFn = fs.mkdirSync,
|
|
470
|
+
...entryDeps
|
|
471
|
+
} = deps;
|
|
472
|
+
|
|
473
|
+
const { ids, handoffIds, error } = resolveRunIds({ stories, handoff });
|
|
474
|
+
if (error) return inputError(error);
|
|
475
|
+
|
|
476
|
+
const mainRepo = path.resolve(cwd ?? process.cwd());
|
|
477
|
+
const resolved = config ?? resolveConfigFn({ cwd: mainRepo });
|
|
478
|
+
const runId = runIdOverride ?? deriveRunId(ids);
|
|
479
|
+
const runTempDir = path.resolve(
|
|
480
|
+
mainRepo,
|
|
481
|
+
getPaths(resolved).tempRoot,
|
|
482
|
+
`run-${runId}`,
|
|
483
|
+
);
|
|
484
|
+
mkdirFn(runTempDir, { recursive: true });
|
|
485
|
+
const ledgerPath = path.join(runTempDir, 'ledger.json');
|
|
486
|
+
const ledgered = readLedgerDispatched(ledgerPath, entryDeps);
|
|
487
|
+
|
|
488
|
+
const {
|
|
489
|
+
envelope: tick,
|
|
490
|
+
exitCode,
|
|
491
|
+
records = [],
|
|
492
|
+
} = await tickFn({
|
|
493
|
+
stories,
|
|
494
|
+
concurrency,
|
|
495
|
+
// The ledger IS the dispatched list. It is additive, never authoritative:
|
|
496
|
+
// the probe unions it into the label-derived in-flight set and then
|
|
497
|
+
// filters it against live state, so an id that has since gone done is
|
|
498
|
+
// dropped rather than pinned in flight forever.
|
|
499
|
+
dispatched: ledgered.join(','),
|
|
500
|
+
cwd: mainRepo,
|
|
501
|
+
...(config ? { config } : {}),
|
|
502
|
+
...(probe ? { probe } : {}),
|
|
503
|
+
...(context ? { context } : {}),
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
if (tick?.inputError) return inputError(tick.inputError);
|
|
507
|
+
|
|
508
|
+
const stalledDispatch = Array.isArray(tick.stalledDispatch)
|
|
509
|
+
? tick.stalledDispatch
|
|
510
|
+
: [];
|
|
511
|
+
|
|
512
|
+
const readyIds = Array.isArray(tick.ready) ? tick.ready : [];
|
|
513
|
+
const digest = await ensureDocsDigestFn({
|
|
514
|
+
docsContextFiles: resolved?.project?.docsContextFiles,
|
|
515
|
+
docsRoot: getPaths(resolved).docsRoot,
|
|
516
|
+
outputPath: path.join(runTempDir, 'docs-digest.md'),
|
|
517
|
+
});
|
|
518
|
+
const docsDigestPath = digest?.outputPath ?? null;
|
|
519
|
+
|
|
520
|
+
const bodyById = new Map(
|
|
521
|
+
records.map((record) => [record.id, record.body ?? '']),
|
|
522
|
+
);
|
|
523
|
+
const ready = readyIds.map((storyId) =>
|
|
524
|
+
buildDispatchEntry(
|
|
525
|
+
{
|
|
526
|
+
storyId,
|
|
527
|
+
body: bodyById.get(storyId) ?? '',
|
|
528
|
+
runTempDir,
|
|
529
|
+
mainRepo,
|
|
530
|
+
docsDigestPath,
|
|
531
|
+
},
|
|
532
|
+
entryDeps,
|
|
533
|
+
),
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
writeLedger(
|
|
537
|
+
{ ledgerPath, runId, stories: ids, dispatched: ledgered, ready: readyIds },
|
|
538
|
+
entryDeps,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
envelope: {
|
|
543
|
+
kind: 'deliver-run-beat',
|
|
544
|
+
runId,
|
|
545
|
+
runTempDir,
|
|
546
|
+
stories: ids,
|
|
547
|
+
ready,
|
|
548
|
+
close: handoffIds.map((storyId) => ({
|
|
549
|
+
id: storyId,
|
|
550
|
+
command: renderCloseCommand({
|
|
551
|
+
storyId,
|
|
552
|
+
mainRepo,
|
|
553
|
+
storyCount: ids.length,
|
|
554
|
+
}),
|
|
555
|
+
})),
|
|
556
|
+
done: tick.epilogueDue === true,
|
|
557
|
+
doneStories: tick.done ?? [],
|
|
558
|
+
inFlight: tick.inFlight ?? 0,
|
|
559
|
+
concurrencyCap: tick.concurrencyCap ?? null,
|
|
560
|
+
docsDigestPath,
|
|
561
|
+
cycleError: tick.cycleError ?? null,
|
|
562
|
+
wedged: tick.wedged ?? null,
|
|
563
|
+
blocked: tick.blocked ?? [],
|
|
564
|
+
blockedReason: tick.blockedReason ?? null,
|
|
565
|
+
foreignHeld: tick.foreignHeld ?? [],
|
|
566
|
+
// Its own reason, deliberately beside `withheld[]` rather than inside
|
|
567
|
+
// it: a footprint withhold names a blocking peer Story and its colliding
|
|
568
|
+
// paths, and a stalled dispatch has neither — it names a recovery the
|
|
569
|
+
// operator owns.
|
|
570
|
+
stalledDispatch,
|
|
571
|
+
stalledDispatchReason: renderStalledDispatchReason(stalledDispatch, {
|
|
572
|
+
ledgerPath,
|
|
573
|
+
runId,
|
|
574
|
+
}),
|
|
575
|
+
withheld: collectWithheld(tick),
|
|
576
|
+
},
|
|
577
|
+
exitCode,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Parse argv into the beat's inputs.
|
|
583
|
+
*
|
|
584
|
+
* @param {string[]} argv
|
|
585
|
+
* @returns {{ stories?: string, handoff: string[], concurrency?: string, runId?: string, cwd?: string }}
|
|
586
|
+
*/
|
|
587
|
+
function parseArgv(argv) {
|
|
588
|
+
const { values } = parseArgs({
|
|
589
|
+
args: argv,
|
|
590
|
+
options: {
|
|
591
|
+
stories: { type: 'string' },
|
|
592
|
+
handoff: { type: 'string', multiple: true },
|
|
593
|
+
concurrency: { type: 'string' },
|
|
594
|
+
'run-id': { type: 'string' },
|
|
595
|
+
cwd: { type: 'string' },
|
|
596
|
+
},
|
|
597
|
+
strict: false,
|
|
598
|
+
allowPositionals: false,
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
stories: values.stories,
|
|
602
|
+
handoff: values.handoff ?? [],
|
|
603
|
+
concurrency: values.concurrency,
|
|
604
|
+
runId: values['run-id'],
|
|
605
|
+
cwd: values.cwd,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function main(argv) {
|
|
610
|
+
const { envelope, exitCode } = await runDeliverRunBeat(parseArgv(argv));
|
|
611
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
612
|
+
if (exitCode !== 0) {
|
|
613
|
+
Logger.error(
|
|
614
|
+
`deliver-run: ${
|
|
615
|
+
envelope.inputError ??
|
|
616
|
+
envelope.cycleError ??
|
|
617
|
+
envelope.blockedReason ??
|
|
618
|
+
envelope.wedged?.reason ??
|
|
619
|
+
'error'
|
|
620
|
+
}`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
return exitCode;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
runAsCli(import.meta.url, () => main(process.argv.slice(2)), {
|
|
627
|
+
source: 'deliver-run',
|
|
628
|
+
propagateExitCode: true,
|
|
629
|
+
errorPrefix: '[deliver-run] ❌ Fatal error',
|
|
630
|
+
usage: USAGE,
|
|
631
|
+
});
|