mandrel 1.68.0 → 1.70.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 +1 -1
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +2 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/schemas/agentrc.schema.json +1 -5
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
- package/.agents/scripts/agents-update-preflight.js +235 -0
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/epic-deliver-preflight.js +30 -13
- package/.agents/scripts/epic-deliver-prepare.js +40 -53
- package/.agents/scripts/epic-execute-record-wave.js +119 -133
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +13 -5
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/explain.js +0 -2
- package/.agents/scripts/lib/config/limits.js +19 -8
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-settings-schema.js +1 -2
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/maintainability-utils.js +32 -9
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -162
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
- package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
- package/.agents/scripts/lib/story-adjacency.js +14 -10
- package/.agents/scripts/lib/story-body/story-body.js +142 -65
- package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
- package/.agents/scripts/lib/wave-runner/tick.js +446 -216
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/lint-label-vocabulary.js +1 -1
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/stories-wave-tick.js +262 -161
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/agents-update.md +205 -28
- package/.agents/workflows/deliver.md +12 -9
- package/.agents/workflows/helpers/deliver-epic.md +126 -90
- package/.agents/workflows/helpers/deliver-stories.md +131 -85
- package/.agents/workflows/helpers/plan-epic.md +13 -10
- package/.agents/workflows/plan.md +1 -1
- package/README.md +20 -0
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- package/.agents/scripts/lib/wave-runner/wave-checkpoint.js +0 -91
|
@@ -2,36 +2,39 @@
|
|
|
2
2
|
/* node:coverage ignore file */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* epic-execute-record-wave.js — record one
|
|
6
|
-
*
|
|
5
|
+
* epic-execute-record-wave.js — record one recorder beat's per-Story
|
|
6
|
+
* returns, splice each Story's terminal status into the `epic-run-state`
|
|
7
|
+
* checkpoint's flat per-Story `stories` map, and re-render the unified
|
|
7
8
|
* `epic-run-progress` rollup on the Epic.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Story #4155 (Epic #4151) — the Epic `/deliver` runtime cut over from the
|
|
11
|
+
* wave-batch scheduler to the continuous ready-set core. This recorder lost
|
|
12
|
+
* its wave semantics with it: there is no `--wave` flag, no wave-level
|
|
13
|
+
* status aggregation, no `currentWave` advance, and no `waves[]` history. The
|
|
14
|
+
* tick (`lib/wave-runner/tick.js`) re-derives readiness from live labels on
|
|
15
|
+
* every beat, so the checkpoint only records terminal Story outcomes (for the
|
|
16
|
+
* auto-merge predicate, branch cleanup, and the operator rollup). The
|
|
17
|
+
* recorder also emits one `story.dispatch.end` per recorded Story so the
|
|
18
|
+
* tick's ledger-derived in-flight set and the `--check-idle` watchdog can
|
|
19
|
+
* clear the dispatch start/end pairing.
|
|
15
20
|
*
|
|
16
21
|
* 1. Parse / reconcile / verify the per-Story returns.
|
|
17
|
-
* 2.
|
|
18
|
-
* 3.
|
|
19
|
-
*
|
|
20
|
-
* 4. Re-render `epic-run-progress` from `state.waves[]`.
|
|
22
|
+
* 2. Record each Story's terminal status onto the checkpoint `stories` map.
|
|
23
|
+
* 3. Emit `story.dispatch.end` per recorded Story.
|
|
24
|
+
* 4. Re-render `epic-run-progress` from the checkpoint `stories` map.
|
|
21
25
|
* 5. Print the next action for the slash-command (`dispatch-next` |
|
|
22
|
-
* `
|
|
26
|
+
* `halt-blocked` | `halt-failed`).
|
|
23
27
|
*
|
|
24
28
|
* The implementation is split across three modules so the parent stays a
|
|
25
29
|
* thin runner shell:
|
|
26
30
|
*
|
|
27
|
-
* - `lib/orchestration/wave-record-projection.js` — pure
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* so existing callers see an unchanged public surface.
|
|
31
|
+
* - `lib/orchestration/wave-record-projection.js` — pure helpers
|
|
32
|
+
* (per-Story validation / normalization, rollup-row shaping). Re-exported
|
|
33
|
+
* from this file so existing callers see an unchanged public surface.
|
|
31
34
|
* - `lib/orchestration/wave-record-io.js` — impure helpers (ticket
|
|
32
35
|
* verification, manifest title lookup, returns reconciliation).
|
|
33
36
|
* - `lib/orchestration/wave-record-notifications.js` — curated webhook
|
|
34
|
-
* emit chain for the
|
|
37
|
+
* emit chain for the recorder beat.
|
|
35
38
|
*/
|
|
36
39
|
|
|
37
40
|
import { readFileSync } from 'node:fs';
|
|
@@ -52,14 +55,11 @@ import {
|
|
|
52
55
|
resolveResolvedResults,
|
|
53
56
|
verifyWaveResults,
|
|
54
57
|
} from './lib/orchestration/wave-record-io.js';
|
|
55
|
-
import {
|
|
58
|
+
import { emitRecordNotifications } from './lib/orchestration/wave-record-notifications.js';
|
|
56
59
|
import {
|
|
57
|
-
projectWaveRecord,
|
|
58
|
-
resolveConcurrencyCap,
|
|
59
60
|
selectInputFlag,
|
|
60
|
-
|
|
61
|
+
validateEpic,
|
|
61
62
|
validateResults,
|
|
62
|
-
validateResultsReturnsXor,
|
|
63
63
|
} from './lib/orchestration/wave-record-projection.js';
|
|
64
64
|
import { createProvider } from './lib/provider-factory.js';
|
|
65
65
|
import { notify } from './notify.js';
|
|
@@ -73,29 +73,25 @@ export {
|
|
|
73
73
|
// Re-export the pure projection surface so tests and downstream consumers
|
|
74
74
|
// can keep importing from `epic-execute-record-wave.js` after the extract.
|
|
75
75
|
export {
|
|
76
|
-
aggregateWaveStatus,
|
|
77
76
|
classifyParsedReturn,
|
|
78
|
-
|
|
79
|
-
countDoneStories,
|
|
80
|
-
resolveConcurrencyCap,
|
|
77
|
+
normalizeReturnsPure,
|
|
81
78
|
STORY_STATUS_TO_ROW_STATE,
|
|
82
79
|
selectInputFlag,
|
|
83
80
|
toRollupRow,
|
|
84
|
-
VALID_RESULT_STATUSES,
|
|
85
81
|
VALID_STORY_STATUSES,
|
|
86
|
-
|
|
82
|
+
validateEpic,
|
|
87
83
|
validateResults,
|
|
88
|
-
validateResultsReturnsXor,
|
|
89
84
|
validateReturnsEntry,
|
|
90
85
|
} from './lib/orchestration/wave-record-projection.js';
|
|
91
86
|
|
|
92
87
|
const HELP = `Usage: node .agents/scripts/epic-execute-record-wave.js \\
|
|
93
|
-
--epic <epicId>
|
|
88
|
+
--epic <epicId> \\
|
|
94
89
|
(--returns @<file>|<inline-json> | --results @<file>|<inline-json>)
|
|
95
90
|
|
|
96
|
-
Records
|
|
97
|
-
checkpoint
|
|
98
|
-
Prints the next action for the
|
|
91
|
+
Records this recorder beat's per-Story outcomes onto the epic-run-state
|
|
92
|
+
checkpoint's flat per-Story status map and upserts the unified
|
|
93
|
+
epic-run-progress rollup on the Epic. Prints the next action for the
|
|
94
|
+
/deliver slash command.
|
|
99
95
|
`;
|
|
100
96
|
|
|
101
97
|
/**
|
|
@@ -135,15 +131,36 @@ export function parseInputArg(value, deps = {}) {
|
|
|
135
131
|
}
|
|
136
132
|
|
|
137
133
|
/**
|
|
138
|
-
*
|
|
134
|
+
* Classify the recorder beat's next action from the verified rows. Pure.
|
|
135
|
+
* Any failed Story → `halt-failed`; else any blocked Story → `halt-blocked`;
|
|
136
|
+
* else → `dispatch-next` (the host re-ticks to pick up the next ready set).
|
|
137
|
+
*
|
|
138
|
+
* @param {Array<{ status: string }>} verified
|
|
139
|
+
* @returns {{ status: 'complete'|'blocked'|'failed', nextAction: string, blockedStoryIds: number[] }}
|
|
140
|
+
*/
|
|
141
|
+
export function classifyRecordOutcome(verified) {
|
|
142
|
+
const rows = Array.isArray(verified) ? verified : [];
|
|
143
|
+
const failed = rows.some((r) => r.status === 'failed');
|
|
144
|
+
const blockedStoryIds = rows
|
|
145
|
+
.filter((r) => r.status === 'blocked')
|
|
146
|
+
.map((r) => r.storyId);
|
|
147
|
+
if (failed) {
|
|
148
|
+
return { status: 'failed', nextAction: 'halt-failed', blockedStoryIds };
|
|
149
|
+
}
|
|
150
|
+
if (blockedStoryIds.length > 0) {
|
|
151
|
+
return { status: 'blocked', nextAction: 'halt-blocked', blockedStoryIds };
|
|
152
|
+
}
|
|
153
|
+
return { status: 'complete', nextAction: 'dispatch-next', blockedStoryIds };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* End-to-end record beat. DI-friendly: tests pass `injectedProvider` and a
|
|
139
158
|
* fully-formed `results` (or `returns`) array to skip real network reads.
|
|
140
159
|
*
|
|
141
160
|
* @param {{
|
|
142
161
|
* epicId: number,
|
|
143
|
-
* wave: number,
|
|
144
162
|
* results?: unknown,
|
|
145
163
|
* returns?: unknown,
|
|
146
|
-
* concurrencyCap?: number,
|
|
147
164
|
* cwd?: string,
|
|
148
165
|
* injectedProvider?: object,
|
|
149
166
|
* injectedConfig?: object,
|
|
@@ -153,18 +170,25 @@ export function parseInputArg(value, deps = {}) {
|
|
|
153
170
|
*/
|
|
154
171
|
export async function runEpicExecuteRecordWave({
|
|
155
172
|
epicId,
|
|
156
|
-
wave,
|
|
157
173
|
results,
|
|
158
174
|
returns,
|
|
159
|
-
concurrencyCap: concurrencyCapOverride,
|
|
160
175
|
cwd,
|
|
161
176
|
injectedProvider,
|
|
162
177
|
injectedConfig,
|
|
163
178
|
injectedNotify,
|
|
164
179
|
now = () => new Date(),
|
|
165
180
|
} = {}) {
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
validateEpic(epicId);
|
|
182
|
+
if (results == null && returns == null) {
|
|
183
|
+
throw new TypeError(
|
|
184
|
+
'runEpicExecuteRecordWave: either `results` or `returns` is required',
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (results != null && returns != null) {
|
|
188
|
+
throw new TypeError(
|
|
189
|
+
'runEpicExecuteRecordWave: pass `results` OR `returns`, not both',
|
|
190
|
+
);
|
|
191
|
+
}
|
|
168
192
|
|
|
169
193
|
const config = injectedConfig ?? resolveConfig({ cwd });
|
|
170
194
|
const provider = injectedProvider ?? createProvider(config);
|
|
@@ -176,25 +200,16 @@ export async function runEpicExecuteRecordWave({
|
|
|
176
200
|
'run `node .agents/scripts/epic-deliver-prepare.js --epic <id>` first.',
|
|
177
201
|
);
|
|
178
202
|
}
|
|
203
|
+
const firstRecord = !hasRecordedStory(existing.stories);
|
|
179
204
|
|
|
180
205
|
const deliverRunner = getRunners(config).deliverRunner ?? {};
|
|
181
|
-
const concurrencyCap = resolveConcurrencyCap(
|
|
182
|
-
concurrencyCapOverride,
|
|
183
|
-
existing,
|
|
184
|
-
deliverRunner,
|
|
185
|
-
);
|
|
186
206
|
|
|
187
|
-
// 1. Parse / reconcile the per-Story returns.
|
|
188
|
-
// wave-complete-livelock recovery (Story #3907) can reconcile every
|
|
189
|
-
// Story in `plan[wave]` from GitHub when mode B records a wave with no
|
|
190
|
-
// child returns.
|
|
207
|
+
// 1. Parse / reconcile the per-Story returns.
|
|
191
208
|
const { resolvedResults, parseFailures } = await resolveResolvedResults({
|
|
192
209
|
provider,
|
|
193
210
|
epicId,
|
|
194
|
-
wave,
|
|
195
211
|
results,
|
|
196
212
|
returns,
|
|
197
|
-
existing,
|
|
198
213
|
});
|
|
199
214
|
|
|
200
215
|
const validated = validateResults(resolvedResults);
|
|
@@ -209,90 +224,61 @@ export async function runEpicExecuteRecordWave({
|
|
|
209
224
|
// 3. Cross-look manifest titles for the rollup rows.
|
|
210
225
|
const titleById = await loadManifestTitleMap({ provider, epicId });
|
|
211
226
|
|
|
212
|
-
// 4.
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await epicRunStateStore.write({
|
|
226
|
-
provider,
|
|
227
|
-
epicId,
|
|
228
|
-
state: {
|
|
229
|
-
...existing,
|
|
230
|
-
currentWave: projection.nextCurrentWave,
|
|
231
|
-
totalWaves: projection.totalWaves,
|
|
232
|
-
waves: projection.nextWaves,
|
|
233
|
-
},
|
|
234
|
-
});
|
|
227
|
+
// 4. Record each Story's terminal status onto the checkpoint `stories`
|
|
228
|
+
// map. Recorded serially so the upserts do not race the same comment.
|
|
229
|
+
let state = existing;
|
|
230
|
+
for (const row of verified) {
|
|
231
|
+
state = await epicRunStateStore.recordStoryStatus({
|
|
232
|
+
provider,
|
|
233
|
+
epicId,
|
|
234
|
+
storyId: row.storyId,
|
|
235
|
+
status: row.status,
|
|
236
|
+
title: titleById.get(row.storyId),
|
|
237
|
+
blockerCommentId: row.blockerCommentId,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
235
240
|
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// /deliver path never imports — so every dispatched Story stayed
|
|
241
|
-
// "in-flight" forever and completed Stories tripped the watchdog.
|
|
242
|
-
// Best-effort: a failed append must not block the wave loop.
|
|
241
|
+
// 5. Emit one `story.dispatch.end` per recorded Story. Closes the
|
|
242
|
+
// start/end pairing the tick reconciler and the `--check-idle`
|
|
243
|
+
// watchdog use to derive in-flight Stories. Best-effort: a failed
|
|
244
|
+
// append must not block the loop.
|
|
243
245
|
emitWaveDispatchEnds({ epicId, verified, config });
|
|
244
246
|
|
|
245
247
|
// 6. Re-render the unified `epic-run-progress` rollup from the checkpoint
|
|
246
|
-
//
|
|
247
|
-
// separate per-wave structured comment.
|
|
248
|
+
// `stories` map. The single operator-facing summary.
|
|
248
249
|
const { body: renderedBody } = await upsertEpicRunProgress({
|
|
249
250
|
provider,
|
|
250
251
|
epicId,
|
|
251
|
-
|
|
252
|
-
currentWave: projection.nextCurrentWave,
|
|
253
|
-
totalWaves: projection.totalWaves,
|
|
252
|
+
stories: state.stories,
|
|
254
253
|
startedAt: existing.startedAt,
|
|
255
254
|
now,
|
|
256
255
|
});
|
|
257
256
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
// misconfig or a transient Slack outage
|
|
263
|
-
|
|
257
|
+
const { status, nextAction, blockedStoryIds } =
|
|
258
|
+
classifyRecordOutcome(verified);
|
|
259
|
+
|
|
260
|
+
// 7. Fire the curated webhook events for this recorder beat. Each helper
|
|
261
|
+
// is fire-and-forget — webhook misconfig or a transient Slack outage
|
|
262
|
+
// must not block the loop.
|
|
263
|
+
await emitRecordNotifications({
|
|
264
264
|
injectedNotify,
|
|
265
265
|
defaultNotify: notify,
|
|
266
266
|
config,
|
|
267
267
|
provider,
|
|
268
268
|
epicId,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
priorWaves: projection.priorWaves,
|
|
272
|
-
nextWaves: projection.nextWaves,
|
|
273
|
-
titleById,
|
|
274
|
-
totalWaves: projection.totalWaves,
|
|
275
|
-
nextCurrentWave: projection.nextCurrentWave,
|
|
269
|
+
firstRecord,
|
|
270
|
+
stories: state.stories,
|
|
276
271
|
verified,
|
|
277
|
-
blockedStoryIds
|
|
272
|
+
blockedStoryIds,
|
|
278
273
|
});
|
|
279
274
|
|
|
280
|
-
// Note (Story #3909): the per-wave dispatch-manifest refresh hop was
|
|
281
|
-
// deleted. It re-ran the full dispatch pipeline (re-fetch every ticket,
|
|
282
|
-
// recompute waves) on every tick only to re-render a comment nothing reads
|
|
283
|
-
// for control flow. The manifest is written once at prepare time; the
|
|
284
|
-
// surviving operator-facing surface is the `epic-run-progress` rollup
|
|
285
|
-
// re-rendered above.
|
|
286
|
-
|
|
287
275
|
const envelope = {
|
|
288
276
|
epicId,
|
|
289
|
-
wave,
|
|
290
277
|
recorded: true,
|
|
291
|
-
status
|
|
278
|
+
status,
|
|
292
279
|
stories: verified.map((r) => ({ id: r.storyId, status: r.status })),
|
|
293
|
-
blockedStoryIds
|
|
294
|
-
nextAction
|
|
295
|
-
remainingWaves: projection.remainingWaves,
|
|
280
|
+
blockedStoryIds,
|
|
281
|
+
nextAction,
|
|
296
282
|
renderedBody,
|
|
297
283
|
};
|
|
298
284
|
if (discrepancies.length > 0) {
|
|
@@ -307,12 +293,28 @@ export async function runEpicExecuteRecordWave({
|
|
|
307
293
|
return envelope;
|
|
308
294
|
}
|
|
309
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Whether the checkpoint's per-Story `stories` map already carries a
|
|
298
|
+
* non-`pending` (recorded) status — used to fire `epic-started` exactly
|
|
299
|
+
* once, on the first recorder beat. Pure helper.
|
|
300
|
+
*
|
|
301
|
+
* @param {Record<string, { status?: string }>|undefined} stories
|
|
302
|
+
* @returns {boolean}
|
|
303
|
+
*/
|
|
304
|
+
function hasRecordedStory(stories) {
|
|
305
|
+
const map = stories && typeof stories === 'object' ? stories : {};
|
|
306
|
+
for (const rec of Object.values(map)) {
|
|
307
|
+
if (rec?.status && rec.status !== 'pending') return true;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
310
312
|
/**
|
|
311
313
|
* Append one `story.dispatch.end` lifecycle record per recorded Story
|
|
312
314
|
* (Story #3900). Each emit is independent and best-effort: a single failed
|
|
313
|
-
* append is logged and swallowed so one bad record never aborts the
|
|
314
|
-
*
|
|
315
|
-
*
|
|
315
|
+
* append is logged and swallowed so one bad record never aborts the loop.
|
|
316
|
+
* The Story status taxonomy (`done`/`blocked`/`failed`) maps directly onto
|
|
317
|
+
* the `story.dispatch.end` outcome enum.
|
|
316
318
|
*
|
|
317
319
|
* Exported for unit testing.
|
|
318
320
|
*
|
|
@@ -378,8 +380,6 @@ export function parseArgv(argv) {
|
|
|
378
380
|
const { values } = defineFlags(
|
|
379
381
|
{
|
|
380
382
|
epic: { type: 'integer', alias: 'epicId' },
|
|
381
|
-
wave: { type: 'integer' },
|
|
382
|
-
'concurrency-cap': { type: 'integer' },
|
|
383
383
|
results: { type: 'string', alias: 'resultsRaw' },
|
|
384
384
|
returns: { type: 'string', alias: 'returnsRaw' },
|
|
385
385
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -393,8 +393,7 @@ export function parseArgv(argv) {
|
|
|
393
393
|
* Orchestration body of `main` extracted as a sibling exported function so
|
|
394
394
|
* the validate / dispatch / envelope-shape ladder is unit-testable without
|
|
395
395
|
* spawning a process. `main` becomes a thin shell: parse → call this →
|
|
396
|
-
* render → exit.
|
|
397
|
-
* stdout JSON schema).
|
|
396
|
+
* render → exit.
|
|
398
397
|
*
|
|
399
398
|
* @param {ReturnType<typeof parseArgv>} values
|
|
400
399
|
* @param {{
|
|
@@ -421,23 +420,10 @@ export async function runRecordWaveCli(values, deps = {}) {
|
|
|
421
420
|
},
|
|
422
421
|
};
|
|
423
422
|
}
|
|
424
|
-
if (!Number.isInteger(values.wave) || values.wave < 0) {
|
|
425
|
-
return {
|
|
426
|
-
exitCode: 2,
|
|
427
|
-
result: {
|
|
428
|
-
kind: 'validation-error',
|
|
429
|
-
message:
|
|
430
|
-
'[epic-execute-record-wave] ERROR: --wave <index> is required (>= 0).',
|
|
431
|
-
help: helpText,
|
|
432
|
-
},
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
423
|
const resolveInput = deps.resolveRecordInput ?? resolveRecordInput;
|
|
436
424
|
const runner = deps.runRecordWave ?? runEpicExecuteRecordWave;
|
|
437
425
|
const envelope = await runner({
|
|
438
426
|
epicId: values.epicId,
|
|
439
|
-
wave: values.wave,
|
|
440
|
-
concurrencyCap: values.concurrencyCap,
|
|
441
427
|
...resolveInput(values),
|
|
442
428
|
});
|
|
443
429
|
return { exitCode: 0, result: { kind: 'envelope', envelope } };
|