mandrel 1.67.0 → 1.69.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/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +2 -4
- package/.agents/schemas/agentrc.schema.json +1 -5
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
- 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/lib/baselines/refresh-service.js +13 -1
- package/.agents/scripts/lib/config/explain.js +0 -2
- package/.agents/scripts/lib/config/limits.js +19 -8
- package/.agents/scripts/lib/config-settings-schema-delivery.js +17 -0
- package/.agents/scripts/lib/config-settings-schema.js +12 -2
- package/.agents/scripts/lib/maintainability-utils.js +32 -9
- 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-plan-spec/phases/run-spec-phase.js +32 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -78
- 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/listeners/watcher.js +13 -8
- package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
- package/.agents/scripts/lib/orchestration/planning-risk.js +45 -6
- 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 +36 -4
- package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
- package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
- package/.agents/scripts/lib/wave-runner/tick.js +312 -206
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
- package/.agents/scripts/lint-label-vocabulary.js +1 -1
- 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/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/docs/CHANGELOG.md +26 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/wave-runner/wave-checkpoint.js +0 -91
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"planning": {
|
|
62
|
-
"maxTickets": 60,
|
|
63
62
|
"context": {
|
|
64
63
|
"maxBytes": 50000,
|
|
65
64
|
"summaryMode": "auto"
|
|
@@ -91,7 +90,7 @@
|
|
|
91
90
|
"Schema migrations that rewrite existing rows or drop columns without a backfill or rollback plan."
|
|
92
91
|
],
|
|
93
92
|
"taskSizing": {
|
|
94
|
-
"softFiles":
|
|
93
|
+
"softFiles": 15,
|
|
95
94
|
"hardFiles": 30,
|
|
96
95
|
"maxAcceptance": 14,
|
|
97
96
|
"softAcceptanceCount": 10
|
|
@@ -26,7 +26,7 @@ with the runtime validators.
|
|
|
26
26
|
"$schema": "./.agents/schemas/agentrc.schema.json",
|
|
27
27
|
"project": { /* paths, commands, baseBranch, docsContextFiles */ },
|
|
28
28
|
"github": { /* owner, repo, branchProtection, mergeMethods, notifications */ },
|
|
29
|
-
"planning": { /* riskHeuristics,
|
|
29
|
+
"planning": { /* riskHeuristics, codebaseSnapshot, context */ },
|
|
30
30
|
"delivery": { /* execution, quality, worktreeIsolation, deliverRunner, ... */ }
|
|
31
31
|
}
|
|
32
32
|
```
|
|
@@ -97,7 +97,6 @@ top-level keys are validation errors.
|
|
|
97
97
|
| Key | Required | Type | Default | Description |
|
|
98
98
|
| --- | --- | --- | --- | --- |
|
|
99
99
|
| `riskHeuristics` | No | `string[]` or `{ append?, prepend? }` | — | — |
|
|
100
|
-
| `maxTickets` | No | `integer` | — | — |
|
|
101
100
|
| `context` | No | `object` | — | Nested configuration block. |
|
|
102
101
|
| `context.maxBytes` | No | `integer` | — | — |
|
|
103
102
|
| `context.summaryMode` | No | `"auto"` \| `"always"` \| `"never"` | — | — |
|
|
@@ -107,7 +106,7 @@ top-level keys are validation errors.
|
|
|
107
106
|
| `codebaseSnapshot.exclude` | No | `array<string>` | — | — |
|
|
108
107
|
| `codebaseSnapshot.recentCommitWindow` | No | `integer` | — | — |
|
|
109
108
|
| `taskSizing` | No | `object` | — | Story-sizing thresholds consumed by ticket-validator-sizing.js. Operator overrides shallow-merge with DEFAULT_TASK_SIZING defaults. Story #3760 collapsed the per-profile matrix and the parallel testSurface axis into a flat set of knobs; the sizingProfile enum was replaced by an optional body-level `wide` declaration that lifts the hardFiles rejection. Story #3874 cut over to one uniform relaxed profile sized for capability slices a frontier model delivers and self-verifies in one pass. |
|
|
110
|
-
| `taskSizing.softFiles` | No | `integer` | — | File-count soft-warn threshold above which a typical-Story width finding fires (default
|
|
109
|
+
| `taskSizing.softFiles` | No | `integer` | — | File-count soft-warn threshold above which a typical-Story width finding fires (default 15). |
|
|
111
110
|
| `taskSizing.hardFiles` | No | `integer` | — | File-count hard ceiling: a Story exceeding it is rejected unless it declares `wide` with a reason (default 30). |
|
|
112
111
|
| `taskSizing.maxAcceptance` | No | `integer` | — | Hard ceiling on acceptance[] item count (default 14). |
|
|
113
112
|
| `taskSizing.softAcceptanceCount` | No | `integer` | — | Soft-warn threshold on acceptance[] item count (default 10). |
|
|
@@ -430,7 +429,6 @@ suppress a channel entirely, set its array to `[]`.
|
|
|
430
429
|
| Field | Required | Default | Purpose |
|
|
431
430
|
| ------------------------------ | -------- | ---------- | ------------------------------------------------------------------------------------------------ |
|
|
432
431
|
| `riskHeuristics` | No | `[]` | Free-form rubric for `risk::high` decisions (informational only — `risk::high` does not gate runtime). Accepts a plain array or the `{ append/prepend }` extender form. |
|
|
433
|
-
| `maxTickets` | No | (none) | Soft cap on tickets a single Epic may decompose. |
|
|
434
432
|
| `failOnSharedEditors` | No | (none) | Hard-fail Phase 7 when two Stories declare the same editor. |
|
|
435
433
|
| `requireExplicitCrossStoryDeps`| No | (none) | Require explicit cross-Story `blocked by` declarations rather than inferring from shared paths. |
|
|
436
434
|
|
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
"softFiles": {
|
|
333
333
|
"type": "integer",
|
|
334
334
|
"minimum": 1,
|
|
335
|
-
"description": "File-count soft-warn threshold above which a typical-Story width finding fires (default
|
|
335
|
+
"description": "File-count soft-warn threshold above which a typical-Story width finding fires (default 15)."
|
|
336
336
|
},
|
|
337
337
|
"hardFiles": {
|
|
338
338
|
"type": "integer",
|
|
@@ -358,10 +358,6 @@
|
|
|
358
358
|
"riskHeuristics": {
|
|
359
359
|
"$ref": "#/$defs/listOrExtenderOfStrings"
|
|
360
360
|
},
|
|
361
|
-
"maxTickets": {
|
|
362
|
-
"type": "integer",
|
|
363
|
-
"minimum": 1
|
|
364
|
-
},
|
|
365
361
|
"context": {
|
|
366
362
|
"$ref": "#/$defs/planningContext"
|
|
367
363
|
},
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"prUrl": { "type": "string", "format": "uri", "minLength": 1 },
|
|
10
10
|
"merged": { "type": "boolean" },
|
|
11
|
-
"reason": { "type": "string" }
|
|
11
|
+
"reason": { "type": "string" },
|
|
12
|
+
"epicId": { "type": "integer", "minimum": 1 }
|
|
12
13
|
},
|
|
13
14
|
"additionalProperties": false
|
|
14
15
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* fan-out and surfaces the result to the operator on two channels:
|
|
9
9
|
*
|
|
10
10
|
* 1. A JSON envelope on stdout (always) with the keys
|
|
11
|
-
* `storyCount`, `installCostSeconds`, `
|
|
11
|
+
* `storyCount`, `installCostSeconds`, `dependencyDepth`,
|
|
12
12
|
* `githubApiRequests`, `claudeQuotaTokens`, plus `breaches`
|
|
13
13
|
* (the non-empty subset of `delivery.preflight.max*` thresholds the
|
|
14
14
|
* estimate exceeds).
|
|
@@ -30,8 +30,14 @@
|
|
|
30
30
|
* the comment can reason about the numbers):
|
|
31
31
|
*
|
|
32
32
|
* - `storyCount` = number of child `type::story` tickets.
|
|
33
|
-
* - `
|
|
34
|
-
*
|
|
33
|
+
* - `dependencyDepth` = longest dependency chain through the Story
|
|
34
|
+
* DAG (Story #4155). The ready-set runtime has
|
|
35
|
+
* no wave barrier, so wall-clock is bounded
|
|
36
|
+
* below by this depth, not by a wave count: a
|
|
37
|
+
* depth-1 Epic (all Stories independent) can run
|
|
38
|
+
* fully parallel, while a depth-N chain forces N
|
|
39
|
+
* sequential beats regardless of cap. Computed
|
|
40
|
+
* as the dependency-DAG layer count.
|
|
35
41
|
* - `installCostSeconds` = `storyCount * perStoryInstallSeconds`
|
|
36
42
|
* (default 45s, override via
|
|
37
43
|
* `--per-story-install-seconds`). Models a
|
|
@@ -115,7 +121,7 @@ const DEFAULTS = Object.freeze({
|
|
|
115
121
|
*
|
|
116
122
|
* @param {{
|
|
117
123
|
* storyCount: number,
|
|
118
|
-
*
|
|
124
|
+
* dependencyDepth: number,
|
|
119
125
|
* perStoryInstallSeconds?: number,
|
|
120
126
|
* perStoryApiRequests?: number,
|
|
121
127
|
* perStoryClaudeTokens?: number,
|
|
@@ -124,14 +130,14 @@ const DEFAULTS = Object.freeze({
|
|
|
124
130
|
* @returns {{
|
|
125
131
|
* storyCount: number,
|
|
126
132
|
* installCostSeconds: number,
|
|
127
|
-
*
|
|
133
|
+
* dependencyDepth: number,
|
|
128
134
|
* githubApiRequests: number,
|
|
129
135
|
* claudeQuotaTokens: number,
|
|
130
136
|
* }}
|
|
131
137
|
*/
|
|
132
138
|
export function computeEstimate({
|
|
133
139
|
storyCount,
|
|
134
|
-
|
|
140
|
+
dependencyDepth,
|
|
135
141
|
perStoryInstallSeconds = DEFAULTS.perStoryInstallSeconds,
|
|
136
142
|
perStoryApiRequests = DEFAULTS.perStoryApiRequests,
|
|
137
143
|
perStoryClaudeTokens = DEFAULTS.perStoryClaudeTokens,
|
|
@@ -142,15 +148,15 @@ export function computeEstimate({
|
|
|
142
148
|
'computeEstimate: storyCount must be a non-negative integer',
|
|
143
149
|
);
|
|
144
150
|
}
|
|
145
|
-
if (!Number.isInteger(
|
|
151
|
+
if (!Number.isInteger(dependencyDepth) || dependencyDepth < 0) {
|
|
146
152
|
throw new TypeError(
|
|
147
|
-
'computeEstimate:
|
|
153
|
+
'computeEstimate: dependencyDepth must be a non-negative integer',
|
|
148
154
|
);
|
|
149
155
|
}
|
|
150
156
|
return {
|
|
151
157
|
storyCount,
|
|
152
158
|
installCostSeconds: storyCount * perStoryInstallSeconds,
|
|
153
|
-
|
|
159
|
+
dependencyDepth,
|
|
154
160
|
githubApiRequests: baseApiRequests + storyCount * perStoryApiRequests,
|
|
155
161
|
claudeQuotaTokens: storyCount * perStoryClaudeTokens,
|
|
156
162
|
};
|
|
@@ -167,7 +173,14 @@ export function computeEstimate({
|
|
|
167
173
|
export function detectBreaches(estimate, thresholds) {
|
|
168
174
|
const mapping = [
|
|
169
175
|
{ key: 'storyCount', observedKey: 'storyCount', maxKey: 'maxStories' },
|
|
170
|
-
|
|
176
|
+
// Story #4155 — `dependencyDepth` replaces the retired `waveCount`
|
|
177
|
+
// estimate; it is still gated by the `maxWaves` config threshold (the
|
|
178
|
+
// published config key is left unchanged for contract stability).
|
|
179
|
+
{
|
|
180
|
+
key: 'dependencyDepth',
|
|
181
|
+
observedKey: 'dependencyDepth',
|
|
182
|
+
maxKey: 'maxWaves',
|
|
183
|
+
},
|
|
171
184
|
{
|
|
172
185
|
key: 'installCostSeconds',
|
|
173
186
|
observedKey: 'installCostSeconds',
|
|
@@ -213,7 +226,7 @@ export function renderPreflightBody({
|
|
|
213
226
|
lines.push('| --- | ---: | ---: |');
|
|
214
227
|
const rows = [
|
|
215
228
|
['storyCount', estimate.storyCount, thresholds.maxStories],
|
|
216
|
-
['
|
|
229
|
+
['dependencyDepth', estimate.dependencyDepth, thresholds.maxWaves],
|
|
217
230
|
[
|
|
218
231
|
'installCostSeconds',
|
|
219
232
|
estimate.installCostSeconds,
|
|
@@ -292,7 +305,11 @@ export async function runPreflight({
|
|
|
292
305
|
state = await runBuildWaveDagPhase(ctx, {}, state);
|
|
293
306
|
|
|
294
307
|
const storyCount = Array.isArray(state.stories) ? state.stories.length : 0;
|
|
295
|
-
|
|
308
|
+
// Dependency depth = the dependency-DAG layer count (the longest chain of
|
|
309
|
+
// `blocked by` edges). `runBuildWaveDagPhase` already computes this layering
|
|
310
|
+
// as `state.waves`, so its length is the depth even though the ready-set
|
|
311
|
+
// runtime no longer dispatches by wave (Story #4155).
|
|
312
|
+
const dependencyDepth = Array.isArray(state.waves) ? state.waves.length : 0;
|
|
296
313
|
|
|
297
314
|
// Persist the snapshot/DAG envelope so `epic-deliver-prepare.js` can
|
|
298
315
|
// reuse it instead of re-walking the hierarchy. The cache key is a
|
|
@@ -317,7 +334,7 @@ export async function runPreflight({
|
|
|
317
334
|
|
|
318
335
|
const estimate = computeEstimate({
|
|
319
336
|
storyCount,
|
|
320
|
-
|
|
337
|
+
dependencyDepth,
|
|
321
338
|
perStoryInstallSeconds,
|
|
322
339
|
perStoryApiRequests,
|
|
323
340
|
perStoryClaudeTokens,
|
|
@@ -5,17 +5,25 @@
|
|
|
5
5
|
* epic-deliver-prepare.js — Step 0/1 of the operator-driven `/deliver`.
|
|
6
6
|
*
|
|
7
7
|
* Composes the existing engine phases that the in-process epic-runner used to
|
|
8
|
-
* call sequentially, but does NOT dispatch any
|
|
8
|
+
* call sequentially, but does NOT dispatch any Stories. The CLI is the single
|
|
9
9
|
* point at which the slash-command captures:
|
|
10
10
|
*
|
|
11
11
|
* 1. The Epic ticket snapshot (`runSnapshotPhase`).
|
|
12
|
-
* 2. The
|
|
12
|
+
* 2. The story DAG (`runBuildWaveDagPhase`) computed from every child Story —
|
|
13
|
+
* used here only to enumerate the open Story set and run the
|
|
14
|
+
* concurrency-hazard gate; the ready-set runtime re-derives readiness
|
|
15
|
+
* from live labels on every `tick`, so the prepare no longer persists a
|
|
16
|
+
* wave grouping.
|
|
13
17
|
* 3. The seeded `epic-run-state` checkpoint (`epic-run-state-store.initialize`)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* in the per-Story-status shape (Story #4155): a flat
|
|
19
|
+
* `stories: { [storyId]: { status: 'pending' } }` map plus the GLOBAL
|
|
20
|
+
* in-flight `concurrencyCap`. Idempotent — re-running prepare against a
|
|
21
|
+
* partially-driven Epic preserves the original `startedAt` and every
|
|
22
|
+
* already-recorded Story status (it never resets recorded progress).
|
|
23
|
+
* 4. The dispatch hint (`StoryLauncher.planWave`) — a deterministic list of
|
|
24
|
+
* `{ storyId, worktree }` entries the slash command uses to resolve
|
|
25
|
+
* per-Story worktree paths. The ready-set `tick` selects which of these
|
|
26
|
+
* to dispatch on each beat; the prepare only enumerates the set.
|
|
19
27
|
*
|
|
20
28
|
* Stdout is a single JSON envelope so the slash command can parse without
|
|
21
29
|
* re-reading any tickets.
|
|
@@ -37,7 +45,6 @@ import {
|
|
|
37
45
|
} from './lib/orchestration/epic-deliver-lease-guard.js';
|
|
38
46
|
import {
|
|
39
47
|
initialize as initializeEpicRunState,
|
|
40
|
-
reconcileResumePointer,
|
|
41
48
|
write as writeEpicRunState,
|
|
42
49
|
} from './lib/orchestration/epic-run-state-store.js';
|
|
43
50
|
import {
|
|
@@ -133,9 +140,9 @@ function resolveGitUserEmail(cwd) {
|
|
|
133
140
|
* }} args
|
|
134
141
|
* @returns {Promise<{
|
|
135
142
|
* epicId: number,
|
|
136
|
-
*
|
|
143
|
+
* storyCount: number,
|
|
137
144
|
* concurrencyCap: number,
|
|
138
|
-
*
|
|
145
|
+
* stories: Array<{ storyId: number, title: string, worktree?: string }>,
|
|
139
146
|
* checkpointInitializedAt: string,
|
|
140
147
|
* }>}
|
|
141
148
|
*/
|
|
@@ -334,64 +341,44 @@ export async function runEpicDeliverPrepare({
|
|
|
334
341
|
ignoreConcurrencyHazards,
|
|
335
342
|
});
|
|
336
343
|
|
|
337
|
-
|
|
344
|
+
// Flatten the wave-DAG into the open Story set. The ready-set runtime
|
|
345
|
+
// re-derives readiness from live labels on every tick, so the checkpoint
|
|
346
|
+
// stores only the Story set in scope (seeded at `pending`) and the global
|
|
347
|
+
// in-flight cap — no wave grouping, no `currentWave`, no `totalWaves`.
|
|
348
|
+
const openStories = state.waves.flat();
|
|
338
349
|
const checkpointState = await initializeEpicRunState({
|
|
339
350
|
provider,
|
|
340
351
|
epicId,
|
|
341
|
-
|
|
352
|
+
storyIds: openStories,
|
|
342
353
|
concurrencyCap,
|
|
343
354
|
});
|
|
344
355
|
|
|
356
|
+
// Resolve per-Story worktree paths via the launcher so the slash command
|
|
357
|
+
// has a deterministic `{ storyId, worktree, title }` list to seed Agent
|
|
358
|
+
// dispatch from. This is a dispatch *hint* — the ready-set tick decides
|
|
359
|
+
// which Stories to dispatch on each beat; the prepare only enumerates them.
|
|
345
360
|
const launcher = new StoryLauncher({ concurrencyCap });
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
...entry,
|
|
350
|
-
title: stories[i]?.title ?? '',
|
|
351
|
-
})),
|
|
361
|
+
const stories = launcher.planWave(openStories).map((entry, i) => ({
|
|
362
|
+
...entry,
|
|
363
|
+
title: openStories[i]?.title ?? '',
|
|
352
364
|
}));
|
|
353
365
|
|
|
354
|
-
// Persist the
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// every wave as `wave-complete: empty` and the delivery stalls.
|
|
358
|
-
const tickPlan = plan.map((wave) => wave.stories);
|
|
359
|
-
|
|
360
|
-
// Story #3358 — reconcile the resume pointer against the recomputed
|
|
361
|
-
// plan. On a resumed Epic, `build-wave-dag.js` drops the already-merged
|
|
362
|
-
// (closed) Stories, so the recomputed plan is shorter and re-indexed
|
|
363
|
-
// from 0. The preserved `currentWave`/`waves[]` reference the *old*
|
|
364
|
-
// index space; left untouched, `wave-tick.js` would index
|
|
365
|
-
// `plan[currentWave]` into the new plan and dispatch the wrong wave.
|
|
366
|
-
// Prepare owns the `plan` field, so it owns the pointer that indexes
|
|
367
|
-
// into it. When the plan changed, reset the pointer to 0 and drop the
|
|
368
|
-
// stale history; when it is byte-identical (idempotent re-prepare),
|
|
369
|
-
// preserve in-flight progress verbatim.
|
|
370
|
-
const { currentWave, waves } = reconcileResumePointer(
|
|
371
|
-
checkpointState,
|
|
372
|
-
checkpointState.plan,
|
|
373
|
-
tickPlan,
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
// Persist the `--ignore-concurrency-hazards` flag on the checkpoint
|
|
377
|
-
// so retro tooling can flag a run that shipped despite an outstanding
|
|
378
|
-
// hazard (the warning above is one-shot; the checkpoint is durable).
|
|
379
|
-
const checkpointPayload = {
|
|
380
|
-
...checkpointState,
|
|
381
|
-
plan: tickPlan,
|
|
382
|
-
currentWave,
|
|
383
|
-
waves,
|
|
384
|
-
};
|
|
366
|
+
// Persist the `--ignore-concurrency-hazards` flag on the checkpoint so
|
|
367
|
+
// retro tooling can flag a run that shipped despite an outstanding hazard
|
|
368
|
+
// (the warning above is one-shot; the checkpoint is durable).
|
|
385
369
|
if (gate.bypassed) {
|
|
386
|
-
|
|
370
|
+
await writeEpicRunState({
|
|
371
|
+
provider,
|
|
372
|
+
epicId,
|
|
373
|
+
state: { ...checkpointState, ignoreConcurrencyHazards: true },
|
|
374
|
+
});
|
|
387
375
|
}
|
|
388
|
-
await writeEpicRunState({ provider, epicId, state: checkpointPayload });
|
|
389
376
|
|
|
390
377
|
return {
|
|
391
378
|
epicId,
|
|
392
|
-
|
|
379
|
+
storyCount: openStories.length,
|
|
393
380
|
concurrencyCap,
|
|
394
|
-
|
|
381
|
+
stories,
|
|
395
382
|
checkpointInitializedAt:
|
|
396
383
|
checkpointState.startedAt ??
|
|
397
384
|
checkpointState.lastUpdatedAt ??
|