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.
Files changed (50) hide show
  1. package/.agents/docs/agentrc-reference.json +1 -2
  2. package/.agents/docs/configuration.md +2 -4
  3. package/.agents/schemas/agentrc.schema.json +1 -5
  4. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +2 -1
  5. package/.agents/scripts/epic-deliver-preflight.js +30 -13
  6. package/.agents/scripts/epic-deliver-prepare.js +40 -53
  7. package/.agents/scripts/epic-execute-record-wave.js +119 -133
  8. package/.agents/scripts/lib/baselines/refresh-service.js +13 -1
  9. package/.agents/scripts/lib/config/explain.js +0 -2
  10. package/.agents/scripts/lib/config/limits.js +19 -8
  11. package/.agents/scripts/lib/config-settings-schema-delivery.js +17 -0
  12. package/.agents/scripts/lib/config-settings-schema.js +12 -2
  13. package/.agents/scripts/lib/maintainability-utils.js +32 -9
  14. package/.agents/scripts/lib/orchestration/epic-cleanup.js +11 -7
  15. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +6 -6
  16. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +11 -5
  17. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +32 -1
  18. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +203 -110
  19. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +38 -78
  20. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +16 -13
  21. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +10 -7
  22. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +37 -24
  23. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +13 -8
  24. package/.agents/scripts/lib/orchestration/manifest-builder.js +6 -0
  25. package/.agents/scripts/lib/orchestration/planning-risk.js +45 -6
  26. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +6 -2
  27. package/.agents/scripts/lib/orchestration/wave-record-io.js +18 -77
  28. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +78 -122
  29. package/.agents/scripts/lib/orchestration/wave-record-projection.js +21 -226
  30. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +18 -1
  31. package/.agents/scripts/lib/presentation/manifest-render-waves.js +77 -4
  32. package/.agents/scripts/lib/story-adjacency.js +14 -10
  33. package/.agents/scripts/lib/story-body/story-body.js +36 -4
  34. package/.agents/scripts/lib/templates/decomposer-prompts.js +23 -3
  35. package/.agents/scripts/lib/wave-runner/ready-set.js +295 -0
  36. package/.agents/scripts/lib/wave-runner/tick.js +312 -206
  37. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +2 -1
  38. package/.agents/scripts/lint-label-vocabulary.js +1 -1
  39. package/.agents/scripts/stories-wave-tick.js +262 -161
  40. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +6 -0
  41. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +108 -101
  42. package/.agents/skills/skills.index.json +2 -2
  43. package/.agents/workflows/deliver.md +12 -9
  44. package/.agents/workflows/helpers/deliver-epic.md +126 -90
  45. package/.agents/workflows/helpers/deliver-stories.md +131 -85
  46. package/.agents/workflows/helpers/plan-epic.md +13 -10
  47. package/.agents/workflows/plan.md +1 -1
  48. package/docs/CHANGELOG.md +26 -0
  49. package/package.json +1 -1
  50. package/.agents/scripts/lib/wave-runner/wave-checkpoint.js +0 -91
@@ -1,10 +1,12 @@
1
1
  ---
2
2
  description: >-
3
3
  Deliver one or more standalone Stories end-to-end. Accepts 1+ Story IDs,
4
- computes a dependency-aware wave plan via `stories-wave-tick.js`, asks the
5
- operator to confirm the plan, then fans out parallel Agent calls per wave
6
- — each delegating to `helpers/single-story-deliver`. Stories without an
7
- `Epic: #N` reference only; Epic-attached Stories use `/deliver`.
4
+ builds a dependency DAG, asks the operator to confirm it, then drives a
5
+ continuous ready-set loop via `stories-wave-tick.js` (the shared
6
+ `selectReadySet` core) dispatching each Story the instant its own deps
7
+ are done, under one global concurrency cap and one file-overlap guard,
8
+ each dispatch delegating to `helpers/single-story-deliver`. Stories
9
+ without an `Epic: #N` reference only; Epic-attached Stories use `/deliver`.
8
10
  ---
9
11
 
10
12
  # helpers/deliver-stories — multi-Story delivery path (invoked by /deliver)
@@ -16,17 +18,24 @@ This helper is the **standalone multi-Story delivery path** behind
16
18
  standalone Stories — either as the sole route (Story-only input) or as the
17
19
  **standalone segment** of a mixed segment plan (run first, before any Epic
18
20
  segments; see [`deliver.md`](../deliver.md)). It takes one or more Story
19
- IDs, builds a dependency-aware wave plan, optionally confirms it with the
20
- operator, and fans out one Agent call per Story per wave — parallel within
21
- each wave, serialised across waves.
21
+ IDs, builds a dependency DAG, optionally confirms it with the operator, and
22
+ then drives a **continuous ready-set loop**: it dispatches each Story the
23
+ instant its **own** dependencies are done — there is no wave barrier where a
24
+ whole group must drain before the next opens — capped by one global
25
+ concurrency limit and guarded by one file-overlap rule, the same scheduling
26
+ core (`lib/wave-runner/ready-set.js#selectReadySet`) the Epic path uses.
22
27
 
23
28
  ```text
24
29
  /deliver 101 102 103
25
30
  → Phase 0 — Validate input & build DAG
26
- → Phase 1 — stories-wave-tick.js → wave plan + operator confirmation
27
- → Phase 2 — for each wave:
28
- Agent tool × min(wave.stories.length, plan.concurrencyCap) parallel calls
29
- helpers/single-story-deliver <storyId>
31
+ → Phase 1 — stories-wave-tick.js → DAG sanity + operator confirmation
32
+ → Phase 2 — continuous ready-set loop:
33
+ repeat until every Story is done:
34
+ stories-wave-tick.js --dag … --done <doneIds> --in-flight <n>
35
+ → ready[] = Stories dispatchable on this beat
36
+ Agent tool × ready.length parallel calls (≤ concurrencyCap − inFlight)
37
+ helpers/single-story-deliver <storyId>
38
+ as each child returns done, add its id to <doneIds> and re-tick
30
39
  → Phase 3 — Summary
31
40
  ```
32
41
 
@@ -42,17 +51,19 @@ each wave, serialised across waves.
42
51
  This helper **refuses** Stories that carry an `Epic: #N` reference in
43
52
  their body. Those Stories belong to an Epic's dispatch manifest and must flow
44
53
  through `/deliver <epicId>`. Use `/single-story-deliver` for a single
45
- Epic-free Story when you want the leaner one-story path without wave
46
- machinery.
54
+ Epic-free Story when you want the leaner one-story path without the
55
+ ready-set scheduling loop.
47
56
 
48
57
  > **Concurrency cap.** The cap is resolved **deterministically in code** by
49
- > `stories-wave-tick.js` (Phase 1a) — the same `resolveConfig` + `getRunners`
50
- > seam `/deliver` uses — and emitted as the `concurrencyCap` field on the
51
- > `stories-wave-plan` envelope. The default is 3; override persistently via
52
- > `delivery.deliverRunner.concurrencyCap` in `.agentrc.json` (a
53
- > `.agentrc.local.json` override is honored) or per-run via the `--concurrency`
54
- > flag below. Phase 2 dispatches `min(wave.stories.length, plan.concurrencyCap)`
55
- > straight from the emitted field do **not** re-read or merge config yourself.
58
+ > `stories-wave-tick.js` — the same `resolveConfig` + `getRunners` seam
59
+ > `/deliver` uses — and emitted as the `concurrencyCap` field on the
60
+ > `stories-ready-set` envelope on **every** beat. The default is 3; override
61
+ > persistently via `delivery.deliverRunner.concurrencyCap` in `.agentrc.json`
62
+ > (a `.agentrc.local.json` override is honored) or per-run via the
63
+ > `--concurrency` flag below. The cap is global, not per-wave: the core caps
64
+ > each beat's `ready[]` at `concurrencyCap inFlight`, so the workflow simply
65
+ > dispatches every id in `ready[]` — do **not** re-read or merge config
66
+ > yourself, and do **not** re-throttle the returned set.
56
67
 
57
68
  ---
58
69
 
@@ -66,13 +77,13 @@ machinery.
66
77
  **no** `Epic: #N` reference. At least one is required.
67
78
  - `--dep <fromId>:<toId>` — Declare an explicit dependency edge: `<fromId>`
68
79
  must complete before `<toId>` runs. Repeat for each edge. When omitted,
69
- all Stories are treated as independent (wave 0 catches everything) unless
70
- `blocked by #N` references between the supplied IDs are detected
71
- automatically.
80
+ all Stories are treated as independent (every Story is dispatchable on the
81
+ first beat) unless `blocked by #N` references between the supplied IDs are
82
+ detected automatically.
72
83
  - `--yes` — Skip the operator confirmation in Phase 1 and proceed
73
84
  immediately. Safe for scripted / sub-agent invocations.
74
- - `--concurrency <n>` — Override the per-wave concurrency cap for this run
75
- only. Passed through to `stories-wave-tick.js` in Phase 1a, which validates
85
+ - `--concurrency <n>` — Override the global concurrency cap for this run
86
+ only. Passed through to `stories-wave-tick.js` on every beat, which validates
76
87
  it (must be a positive integer) and reflects it in the emitted
77
88
  `concurrencyCap` field. When omitted, the cap is resolved from
78
89
  `delivery.deliverRunner.concurrencyCap` (default 3).
@@ -108,9 +119,9 @@ Construct the DAG input array:
108
119
 
109
120
  ---
110
121
 
111
- ## Phase 1 — Wave planning and operator confirmation
122
+ ## Phase 1 — DAG sanity check and operator confirmation
112
123
 
113
- ### 1a. Compute the wave plan
124
+ ### 1a. Sanity-check the DAG (first beat, nothing done yet)
114
125
 
115
126
  ```bash
116
127
  node .agents/scripts/stories-wave-tick.js --dag '<dag-json>'
@@ -123,17 +134,16 @@ verbatim to `stories-wave-tick.js`. The script resolves the cap from config
123
134
  (`delivery.deliverRunner.concurrencyCap`, default 3) and the override wins for
124
135
  that run.
125
136
 
126
- Stdout is one JSON envelope:
137
+ With no `--done` / `--in-flight` (the first beat), stdout is one JSON
138
+ envelope describing the Stories dispatchable **right now** — the roots:
127
139
 
128
140
  ```json
129
141
  {
130
- "kind": "stories-wave-plan",
131
- "waves": [
132
- { "waveIndex": 0, "stories": [101, 103] },
133
- { "waveIndex": 1, "stories": [102] }
134
- ],
142
+ "kind": "stories-ready-set",
143
+ "ready": [101, 103],
135
144
  "totalStories": 3,
136
145
  "concurrencyCap": 3,
146
+ "inFlight": 0,
137
147
  "cycleError": null
138
148
  }
139
149
  ```
@@ -141,27 +151,35 @@ Stdout is one JSON envelope:
141
151
  - **`cycleError` non-null** → STOP. Report the cycle to the operator and
142
152
  exit. The Story set cannot be delivered until the circular dependency is
143
153
  resolved.
144
- - **`waves` empty** → STOP. Zero Stories resolved — report and exit.
145
- - **`concurrencyCap`** is the resolved per-wave cap. Phase 2 dispatches
146
- `min(wave.stories.length, plan.concurrencyCap)` from this field the
147
- workflow never re-reads or merges config to recover the cap.
148
- - An invalid `--concurrency` value (non-positive / non-integer) makes the
154
+ - **`totalStories === 0`** → STOP. Zero Stories resolved — report and exit.
155
+ - **`ready` empty with `totalStories > 0` on the first beat** → STOP. Every
156
+ Story is gated behind a dependency outside the supplied set; report and
157
+ exit (the operator must include the missing prerequisites).
158
+ - **`concurrencyCap`** is the resolved global cap. Phase 2 re-reads it from
159
+ each beat's envelope and never re-derives it from config.
160
+ - An invalid `--concurrency` / `--in-flight` / `--done` value makes the
149
161
  script exit non-zero with an `inputError` → STOP and surface the message.
150
162
 
151
- ### 1b. Operator confirmation (skipped with `--yes` or for single-story plans)
163
+ This first call is a **sanity gate**, not the schedule: Phase 2 re-ticks
164
+ continuously as Stories complete. The envelope no longer enumerates future
165
+ waves — ordering emerges beat by beat from the live `--done` set.
152
166
 
153
- **Auto-skip rule (Story #3302):** When `waves.length === 1` and
154
- `waves[0].stories.length === 1`, skip the confirmation prompt
155
- automatically and proceed. A single-Story plan has no ordering ambiguity
156
- and no meaningful operator decision to make — the plan *is* "deliver this
157
- one Story". Prompting for confirmation would just be friction.
167
+ ### 1b. Operator confirmation (skipped with `--yes` or for single-story runs)
158
168
 
159
- Otherwise, present the wave plan to the operator in a readable table:
169
+ **Auto-skip rule (Story #3302):** When `totalStories === 1`, skip the
170
+ confirmation prompt automatically and proceed. A single-Story run has no
171
+ ordering ambiguity and no meaningful operator decision to make — the run
172
+ *is* "deliver this one Story". Prompting for confirmation would just be
173
+ friction.
174
+
175
+ Otherwise, present the dependency DAG to the operator in a readable table —
176
+ the roots dispatch immediately, dependents unlock as their prerequisites
177
+ complete:
160
178
 
161
179
  ```text
162
- Wave plan — 3 Stories across 2 waves
163
- Wave 0 (parallel): #101 "<title>", #103 "<title>"
164
- Wave 1 (after wave 0): #102 "<title>"
180
+ Delivery plan — 3 Stories (continuous, dependency-driven)
181
+ Ready now: #101 "<title>", #103 "<title>"
182
+ Unlocks after its deps complete: #102 "<title>" (needs #101)
165
183
 
166
184
  Proceed? [Y/n]
167
185
  ```
@@ -169,23 +187,44 @@ Proceed? [Y/n]
169
187
  Wait for the operator to confirm before dispatching. When the operator
170
188
  types `n` or `N`, abort cleanly with a summary of the plan that was
171
189
  declined. When `--yes` was passed, skip this step and proceed regardless
172
- of wave count.
190
+ of Story count.
173
191
 
174
192
  ---
175
193
 
176
- ## Phase 2 — Wave dispatch loop
194
+ ## Phase 2 — Continuous ready-set loop
195
+
196
+ Drive a single loop, re-ticking after every child returns. Maintain two
197
+ pieces of state across the loop:
198
+
199
+ - **`doneIds`** — the set of Story IDs that have returned `status: 'done'`
200
+ (start empty).
201
+ - **`inFlight`** — the count of Stories dispatched but not yet returned
202
+ (start at 0).
203
+
204
+ Each iteration (one **beat**):
205
+
206
+ ### 2a. Tick the ready set
207
+
208
+ ```bash
209
+ node .agents/scripts/stories-wave-tick.js --dag '<dag-json>' \
210
+ --done '<comma-separated doneIds>' --in-flight <inFlight> [--concurrency <n>]
211
+ ```
177
212
 
178
- For each wave in `waves` (in `waveIndex` order):
213
+ The envelope's `ready[]` is the set of Stories whose **own** dependencies
214
+ are all in `doneIds` and that fit the remaining capacity
215
+ (`concurrencyCap − inFlight`) without a file-overlap collision — the core
216
+ has already applied the global cap and the overlap guard, so the list is
217
+ safe to dispatch verbatim. **Termination:** when `ready` is empty **and**
218
+ `inFlight === 0` **and** `doneIds` covers every Story, the run is complete →
219
+ go to Phase 3. When `ready` is empty but `inFlight > 0`, dispatch nothing
220
+ and wait for an in-flight child to return (a dependent is waiting on it).
179
221
 
180
- ### 2a. Fan out per-Story Agent calls
222
+ ### 2b. Fan out per-Story Agent calls
181
223
 
182
- Emit **one `Agent` tool call per Story** in the wave, capped at
183
- `plan.concurrencyCap` from the Phase 1a `stories-wave-plan` envelope. When
184
- the wave contains more Stories than `plan.concurrencyCap`, dispatch the first
185
- `plan.concurrencyCap` in one turn with `run_in_background: true` and refill
186
- as each child returns — never exceed the cap, never wait for the whole
187
- batch before refilling. Do **not** re-derive the cap from config here; the
188
- envelope field is the single deterministic source.
224
+ Emit **one `Agent` tool call per Story ID in `ready[]`**, all with
225
+ `run_in_background: true`. The list is already capped, so dispatch the whole
226
+ of `ready[]` and increment `inFlight` by `ready.length`. Do **not** re-derive
227
+ or re-throttle the cap here; the core sized `ready[]` for you.
189
228
 
190
229
  Each Agent call:
191
230
 
@@ -199,7 +238,7 @@ Each Agent call:
199
238
  internally-blocking step (`gh pr checks --watch` blocks the turn), **not**
200
239
  a reason to suspend and hand back. A child that ends its turn with
201
240
  free-form prose and an unconfirmed merge (e.g. "I'll wait for the
202
- background watch task…") has violated the contract — the wave loop cannot
241
+ background watch task…") has violated the contract — the loop cannot
203
242
  advance, and the Story strands at `agent::closing` (the Story #1553 /
204
243
  PR #1554 failure mode). There is no "pending" return status: the child
205
244
  returns `done` (merge confirmed), `blocked` (transitioned + friction
@@ -212,11 +251,14 @@ Each Agent call:
212
251
 
213
252
  Use `subagent_type: general-purpose`.
214
253
 
215
- ### 2b. Collect results
254
+ ### 2b′. Collect each return and re-tick
216
255
 
217
- Wait for all dispatched Stories in the current wave to return before
218
- advancing to the next wave. A wave is complete when every dispatched
219
- Agent call has returned a result (success, blocked, or failed).
256
+ As each child returns, decrement `inFlight` by 1 and apply its outcome
257
+ 2d). When a child returns `done`, add its id to `doneIds`. Then **re-tick
258
+ immediately** 2a) so a newly-unblocked Story dispatches the instant its
259
+ last dependency clears — do not wait for the whole batch to drain. This is
260
+ the no-false-barrier behavior: an unrelated still-running Story never holds
261
+ back a Story whose own deps are already done.
220
262
 
221
263
  ### 2c. Per-Story return contract
222
264
 
@@ -240,36 +282,38 @@ The status enum is **closed** — `done`, `blocked`, or `failed`. There is no
240
282
  **not** a returnable suspension: the child blocks on `gh pr checks --watch`
241
283
  *inside its own turn*, confirms the merge, flips `agent::done`, and only then
242
284
  returns `status: "done"`. A child that returns prose instead — parking on the
243
- CI wait with an unconfirmed merge — breaks the wave loop's ability to advance
285
+ CI wait with an unconfirmed merge — breaks the loop's ability to advance
244
286
  and leaves the Story at `agent::closing` (Story #1553 / PR #1554). The
245
287
  single-homed restatement of this no-park rule for the child's own perspective
246
288
  is [`single-story-deliver.md` § Step 7](single-story-deliver.md#return-contract).
247
289
 
248
- ### 2d. Wave outcome handling
290
+ ### 2d. Per-return outcome handling
249
291
 
250
- After every Story in a wave returns:
292
+ As each child returns (before the next re-tick):
251
293
 
252
- - **All `status === 'done'`** → Advance to the next wave (or Phase 3 if
253
- this was the last wave). Print a one-line wave-complete summary.
254
- - **Any `status === 'blocked'`** → STOP the wave loop. Post a summary
255
- of blocked Stories and their `blockerCommentId` references. Do not
256
- dispatch the next wave. Wait for the operator to resolve each blocker
257
- and re-run `/deliver` with the same set (already-done Stories
258
- will short-circuit because `single-story-close.js` is idempotent).
259
- - **Any `status === 'failed'`** → STOP the wave loop. Report the
260
- failures. The operator must fix the failing Stories before re-running.
294
+ - **`status === 'done'`** → Add its id to `doneIds` and re-tick. Print a
295
+ one-line per-Story complete summary.
296
+ - **`status === 'blocked'`** → STOP dispatching new Stories. Drain the
297
+ Stories still in flight (let them return), then post a summary of blocked
298
+ Stories and their `blockerCommentId` references. Do not dispatch any
299
+ further beat. Wait for the operator to resolve each blocker and re-run
300
+ `/deliver` with the same set (already-done Stories will short-circuit
301
+ because `single-story-close.js` is idempotent).
302
+ - **`status === 'failed'`** STOP dispatching new Stories, drain the
303
+ in-flight set, and report the failures. The operator must fix the failing
304
+ Stories before re-running.
261
305
 
262
306
  ---
263
307
 
264
308
  ## Phase 3 — Summary
265
309
 
266
- Print a final run summary:
310
+ Print a final run summary listing every delivered Story in completion order:
267
311
 
268
312
  ```text
269
- /deliver — 3 Stories delivered in 2 waves
313
+ /deliver — 3 Stories delivered (continuous, dependency-driven)
270
314
 
271
- Wave 0: #101 ✅ done, #103 ✅ done
272
- Wave 1: #102 ✅ done
315
+ #101 ✅ done, #103 ✅ done (dispatched first — roots)
316
+ #102 ✅ done (after #101 cleared)
273
317
 
274
318
  All Stories delivered. PRs opened, auto-merge armed. CI will merge each
275
319
  PR when checks pass; each child then confirms the merge and flips its
@@ -331,11 +375,13 @@ blocked or unstarted Stories execute.
331
375
 
332
376
  - **Never** pass Epic-attached Stories to this command. Detect `Epic: #N`
333
377
  in Phase 0 and STOP.
334
- - **Never** advance to the next wave while any Story in the current wave
335
- is `blocked` or `failed`.
336
- - **Never** exceed `plan.concurrencyCap` (the resolved cap emitted by
337
- `stories-wave-tick.js`) parallel Agent calls at any moment.
338
- - **Always** confirm the wave plan with the operator before dispatching,
378
+ - **Never** dispatch a new beat once any Story has returned `blocked` or
379
+ `failed` drain the in-flight set and stop.
380
+ - **Never** exceed the resolved `concurrencyCap` (emitted by
381
+ `stories-wave-tick.js` on every beat) parallel Agent calls at any moment.
382
+ The core caps each beat's `ready[]` at `concurrencyCap inFlight`;
383
+ dispatch the whole list and never add to it.
384
+ - **Always** confirm the dependency DAG with the operator before dispatching,
339
385
  unless `--yes` was passed.
340
386
  - **Label transitions**: drive every `agent::*` state change through
341
387
  `node .agents/scripts/update-ticket-state.js --ticket <id> --state <state>`.
@@ -713,16 +713,19 @@ node .agents/scripts/epic-plan-spec-validate.js \
713
713
  ticket array to `temp/epic-[Epic_ID]/tickets.json`. Do **not** inline
714
714
  the JSON authoring in the workflow body.
715
715
 
716
- The `maxTickets` cap (`planning.maxTickets` in
717
- `.agentrc.json`; framework default in
718
- `.agents/scripts/lib/config/limits.js`) is a **reviewability budget**,
719
- not a hard authoring ceiling: a draft over budget warns at authoring
720
- time and is rejected at persist unless rerun with `--allow-over-budget`
721
- (after confirming the over-budget rationale on the Epic). The
722
- `epic-plan-decompose.js` script also logs the resolved budget to stderr
723
- so a misconfigured key surfaces immediately. The skill body is the
724
- authoritative source of the decomposer prompt; the `systemPrompt`
725
- field on the emit envelope is a backstop for legacy callers.
716
+ The `maxTickets` cap is a **framework constant**
717
+ (`LIMITS_DEFAULTS.maxTickets` in
718
+ `.agents/scripts/lib/config/limits.js`; not operator-configurable) and
719
+ a **reviewability budget**, not a hard authoring ceiling: a draft over
720
+ budget warns at authoring time and is rejected at persist unless rerun
721
+ with `--allow-over-budget` (after confirming the over-budget rationale
722
+ on the Epic). The `epic-plan-decompose.js` script also logs the
723
+ resolved budget to stderr. The decomposer system prompt is
724
+ single-sourced in
725
+ `.agents/scripts/lib/templates/decomposer-prompts.js`
726
+ (`renderDecomposerSystemPrompt`); the `epic-plan-decompose-author`
727
+ skill references that rendered `systemPrompt` rather than carrying
728
+ its own copy of the prompt body.
726
729
 
727
730
  When the Tech Spec carries a `## Delivery Slicing` section (authored by
728
731
  `epic-plan-spec-author` in Phase 7), the decompose-author skill authors
@@ -43,7 +43,7 @@ forwards them to the active path helper:
43
43
  | `--from-notes <path>` | both | Pre-triaged handoff notes; skips triage. |
44
44
  | `--force` | Epic | Close + recreate an existing ticket tree on re-plan. |
45
45
  | `--force-review` | Epic | Force the operator review gate even when risk routing would skip it. |
46
- | `--allow-over-budget` | Epic | Permit a decomposition that exceeds `planning.maxTickets`. |
46
+ | `--allow-over-budget` | Epic | Permit a decomposition that exceeds the framework `maxTickets` reviewability budget. |
47
47
  | `--steal` | Epic | Forcibly transfer a foreign Epic-lease. |
48
48
  | `--dry-run` | both | Author + validate without GitHub writes. |
49
49
  | `--body <path>` | Story | Pre-authored Story body file; validate (and create, unless `--dry-run`) without re-authoring. |
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.69.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.68.0...mandrel-v1.69.0) (2026-06-16)
6
+
7
+
8
+ ### Added
9
+
10
+ * Epic [#4151](https://github.com/dsj1984/mandrel/issues/4151) ([#4159](https://github.com/dsj1984/mandrel/issues/4159)) ([f4ad69b](https://github.com/dsj1984/mandrel/commit/f4ad69b1663fca95c8e121f837a2e511ab306706))
11
+ * Epic [#4158](https://github.com/dsj1984/mandrel/issues/4158) ([#4167](https://github.com/dsj1984/mandrel/issues/4167)) ([939265e](https://github.com/dsj1984/mandrel/commit/939265ef589a24a72d6beebad14197a245ec31ec))
12
+
13
+
14
+ ### Fixed
15
+
16
+ * **baselines:** apply ignoreGlobs in diff-scope maintainability refresh ([#4166](https://github.com/dsj1984/mandrel/issues/4166)) ([436e3a4](https://github.com/dsj1984/mandrel/commit/436e3a424db9aac54f720a9a993ad0dd285998f3))
17
+ * **lifecycle:** allow epicId in epic.automerge.end schema so the CLI can emit it ([#4168](https://github.com/dsj1984/mandrel/issues/4168)) ([5536cdc](https://github.com/dsj1984/mandrel/commit/5536cdc348e4958b9d05eebd3cd90e1554a1675c))
18
+
19
+ ## [1.68.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.67.0...mandrel-v1.68.0) (2026-06-15)
20
+
21
+
22
+ ### Added
23
+
24
+ * **plan:** waive acceptance disposition when no BDD runner detected (refs [#4145](https://github.com/dsj1984/mandrel/issues/4145)) ([#4147](https://github.com/dsj1984/mandrel/issues/4147)) ([f119919](https://github.com/dsj1984/mandrel/commit/f119919c4e69b4a583eeec7c8fe860116dcdf87c))
25
+
26
+
27
+ ### Fixed
28
+
29
+ * **watcher:** default gh ports in watchPrToTerminal so the CLI path runs (refs [#4144](https://github.com/dsj1984/mandrel/issues/4144)) ([#4148](https://github.com/dsj1984/mandrel/issues/4148)) ([764cc05](https://github.com/dsj1984/mandrel/commit/764cc05b4414f2dff0fb1247b647460df32d037d))
30
+
5
31
  ## [1.67.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.66.0...mandrel-v1.67.0) (2026-06-15)
6
32
 
7
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.67.0",
3
+ "version": "1.69.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",
@@ -1,91 +0,0 @@
1
- /**
2
- * Wave-checkpoint reducers — pure functions that read the
3
- * `epic-run-state` checkpoint payload and the per-wave outcome maps.
4
- *
5
- * These two helpers historically lived inside the iterate-waves phase
6
- * (`lib/orchestration/epic-runner/phases/iterate-waves.js`) but are
7
- * consumed by both the phase and the stateless wave-runner `tick.js`.
8
- * Hosting them here severs the planner→phase import coupling: `tick.js`
9
- * no longer reaches up into the epic-runner phase tree just to derive a
10
- * force-fresh set, and the phase imports them back from this neutral
11
- * module.
12
- *
13
- * Both functions are pure: they read their argument and return a value
14
- * without I/O, so they can be unit-tested in isolation.
15
- *
16
- * @module lib/wave-runner/wave-checkpoint
17
- */
18
-
19
- /**
20
- * Pull the set of Story IDs that the prior checkpoint marked as part of
21
- * a halted wave. Story #1795 — used by the resume-check cache pre-warm:
22
- * Stories appearing in this set are force-fresh-fetched on resume (the
23
- * operator may have hand-edited their labels during the blocker
24
- * window); every other Story serves its resume-check from the
25
- * provider's in-process cache.
26
- *
27
- * Tolerant of partial/legacy checkpoint shapes: a missing or
28
- * unparseable checkpoint returns an empty set so the resume-check
29
- * gracefully degrades to "use cache for all" — the existing
30
- * cold-start fallback inside `getTicket` still issues the real fetch.
31
- *
32
- * @param {object | null | undefined} checkpoint
33
- * @returns {Set<number>}
34
- */
35
- export function collectHaltedStoryIds(checkpoint) {
36
- const halted = new Set();
37
- const waves = checkpoint?.waves;
38
- if (!Array.isArray(waves)) return halted;
39
- for (const wave of waves) {
40
- if (wave?.status !== 'halted') continue;
41
- const stories = Array.isArray(wave.stories) ? wave.stories : [];
42
- for (const story of stories) {
43
- const id =
44
- story?.storyId ??
45
- story?.id ??
46
- (typeof story === 'number' ? story : null);
47
- if (Number.isInteger(id) && id > 0) halted.add(id);
48
- }
49
- }
50
- return halted;
51
- }
52
-
53
- /**
54
- * Cross-event invariant guard for `wave.end` (Acceptance Spec AC-8 /
55
- * Repeatability AC #5 — wave completeness).
56
- *
57
- * The schema layer can declare key/value types for `outcomes` but
58
- * cannot enforce that the key set equals the `wave.start.storyIds` set
59
- * from earlier in the run. We enforce it here, before emit, so a
60
- * violation throws synchronously and the ledger never carries a
61
- * non-conformant record.
62
- *
63
- * Throws a typed `Error` with `code: 'WAVE_COMPLETENESS_VIOLATION'` and
64
- * attached diagnostic fields so tests and operators can reason about
65
- * the mismatch without grepping the message.
66
- *
67
- * @param {{ waveIndex: number, storyIds: number[], outcomes: Record<string, string> }} args
68
- */
69
- export function assertWaveCompleteness({ waveIndex, storyIds, outcomes }) {
70
- const expected = new Set(storyIds);
71
- const actual = new Set(
72
- Object.keys(outcomes ?? {})
73
- .map((k) => Number(k))
74
- .filter((n) => Number.isInteger(n)),
75
- );
76
- const missing = [...expected].filter((id) => !actual.has(id));
77
- const extra = [...actual].filter((id) => !expected.has(id));
78
- if (missing.length === 0 && extra.length === 0) return;
79
- const err = new Error(
80
- `wave-completeness violation for wave #${waveIndex}: ${
81
- missing.length ? `missing outcomes for [${missing.join(', ')}]` : ''
82
- }${missing.length && extra.length ? '; ' : ''}${
83
- extra.length ? `extra outcomes for [${extra.join(', ')}]` : ''
84
- }`,
85
- );
86
- err.code = 'WAVE_COMPLETENESS_VIOLATION';
87
- err.waveIndex = waveIndex;
88
- err.missing = missing;
89
- err.extra = extra;
90
- throw err;
91
- }