mandrel 1.82.0 → 1.83.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 (45) hide show
  1. package/.agents/README.md +41 -0
  2. package/.agents/docs/SDLC.md +4 -2
  3. package/.agents/docs/agentrc-reference.json +10 -2
  4. package/.agents/docs/execution-reference.md +52 -0
  5. package/.agents/instructions.md +76 -38
  6. package/.agents/schemas/agentrc.schema.json +31 -3
  7. package/.agents/schemas/qa-ledger.schema.json +2 -2
  8. package/.agents/scripts/epic-deliver-prepare.js +41 -1
  9. package/.agents/scripts/lib/config/explain.js +4 -1
  10. package/.agents/scripts/lib/config-settings-schema.js +25 -1
  11. package/.agents/scripts/lib/epic-body-sections.js +88 -0
  12. package/.agents/scripts/lib/findings/promote-finding.js +3 -3
  13. package/.agents/scripts/lib/findings/severity.js +5 -6
  14. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +65 -2
  15. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +96 -11
  16. package/.agents/scripts/lib/orchestration/doc-reader.js +29 -0
  17. package/.agents/scripts/lib/orchestration/docs-digest.js +134 -0
  18. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +15 -1
  19. package/.agents/scripts/lib/qa/console-allowlist.js +5 -4
  20. package/.agents/scripts/lib/qa/resolve-qa-contract.js +144 -8
  21. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +7 -5
  22. package/.agents/skills/core/epic-plan-consolidate/examples.md +51 -0
  23. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +4 -22
  24. package/.agents/skills/core/epic-plan-decompose-author/examples.md +47 -0
  25. package/.agents/skills/core/epic-plan-premortem/SKILL.md +9 -8
  26. package/.agents/skills/core/epic-plan-premortem/examples.md +53 -0
  27. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +21 -81
  28. package/.agents/skills/core/epic-plan-spec-author/examples.md +91 -0
  29. package/.agents/skills/skills.index.json +3 -3
  30. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +52 -38
  31. package/.agents/workflows/helpers/deliver-epic-reference.md +514 -0
  32. package/.agents/workflows/helpers/deliver-epic.md +164 -469
  33. package/.agents/workflows/helpers/epic-deliver-story.md +35 -11
  34. package/.agents/workflows/helpers/plan-epic-reference.md +136 -0
  35. package/.agents/workflows/helpers/plan-epic.md +56 -186
  36. package/.agents/workflows/helpers/plan-story.md +31 -61
  37. package/.agents/workflows/helpers/qa-run-scenario.md +194 -0
  38. package/.agents/workflows/helpers/scope-triage-gate.md +97 -0
  39. package/.agents/workflows/helpers/single-story-deliver-reference.md +423 -0
  40. package/.agents/workflows/helpers/single-story-deliver.md +128 -392
  41. package/.agents/workflows/qa-explore.md +63 -32
  42. package/.agents/workflows/qa-run.md +293 -130
  43. package/docs/CHANGELOG.md +14 -0
  44. package/package.json +1 -1
  45. package/.agents/schemas/qa-finding.schema.json +0 -133
@@ -0,0 +1,514 @@
1
+ ---
2
+ description: >-
3
+ Reference companion to `helpers/deliver-epic.md` — the recovery
4
+ procedures, historical rationale, and troubleshooting detail lifted out
5
+ of the runtime core so the always-ingested Epic-delivery prose stays lean.
6
+ Not a slash command; consulted on demand when the core file points here.
7
+ caller: helpers/deliver-epic.md
8
+ ---
9
+
10
+ # helpers/deliver-epic — reference (recovery, rationale, troubleshooting)
11
+
12
+ > **Not a slash command, not the runtime path.** This file is the
13
+ > reference companion to [`deliver-epic.md`](deliver-epic.md). The core
14
+ > file carries the phase flow, commands, gate contracts, and return shapes
15
+ > a `/deliver` run needs on every beat; this file holds the recovery
16
+ > procedures, the "why it works this way" rationale, and the
17
+ > troubleshooting detail the core points at with one-line pointers. Read a
18
+ > section here only when the matching pointer in the core sends you.
19
+
20
+ ---
21
+
22
+ ## Phase 1 — Preflight guards (Story #3482 / F-workflow-guards)
23
+
24
+ Before the snapshot phase runs — and before any worktree is created —
25
+ `epic-deliver-prepare.js` runs two **fail-closed** guards
26
+ ([`lib/orchestration/epic-deliver-lease-guard.js`](../../scripts/lib/orchestration/epic-deliver-lease-guard.js)):
27
+
28
+ 1. **Checkout safety.** Prepare refuses to start when the working tree is
29
+ dirty or HEAD is on a branch other than the expected one (`epic/<id>`
30
+ on a resume, or the project base branch on a fresh run). It will
31
+ **not** check `epic/<id>` out over your work — the historic
32
+ HEAD-yank footgun. Remediation: commit/stash/clean the tree, or
33
+ switch to the expected branch, then re-run.
34
+ 2. **Epic lease.** Prepare acquires the assignee-as-lease on the Epic
35
+ ticket (`ticket-lease.acquireLease`). On a **live foreign claim**
36
+ (a teammate's run with a fresh `story.heartbeat` within
37
+ `delivery.lease.ttlMs`) it exits non-zero and names the current owner;
38
+ a **stale** claim is silently reclaimed. The operator identity is
39
+ resolved from `--as <handle>` → `github.operatorHandle` →
40
+ `git config user.email`. Pass `--steal` to forcibly transfer a live
41
+ foreign claim (the takeover is logged for auditability). The committed
42
+ `github.operatorHandle` is the non-personal `@[USERNAME]` placeholder,
43
+ which resolves to null — so when none of the three sources yields a real
44
+ identity the guard **fails closed** (throws after the checkout guard
45
+ runs) rather than driving an ownerless, unguarded delivery. Set your own
46
+ handle in `.agentrc.local.json`, pass `--as <handle>`, or configure
47
+ `git user.email`. The lease is the cross-clone coordination layer, while
48
+ `epic-merge-lock.js` continues to serialize same-machine sessions.
49
+
50
+ Both guards throw on failure, which `runAsCli` maps to `process.exit(1)`
51
+ per [`orchestration-error-handling.md`](../../rules/orchestration-error-handling.md).
52
+
53
+ ### Acceptance-table start gate
54
+
55
+ Once the preflight guards pass, the snapshot phase applies one more gate.
56
+ Before the wave loop fans out, the snapshot phase
57
+ ([`lib/orchestration/epic-runner/phases/snapshot.js`](../../scripts/lib/orchestration/epic-runner/phases/snapshot.js))
58
+ asserts that the Epic either carries the `acceptance::n-a` waiver label
59
+ **or** has a `## Acceptance Table` managed section in its body — section
60
+ presence is sufficient. The reviewer's OK during `/plan` Phase 7 is the
61
+ approval signal. Neither condition met → the snapshot throws a clear error
62
+ (`[epic-deliver] Epic #<id> cannot launch: …` — naming the missing
63
+ `## Acceptance Table` section and the absent waiver) and `runAsCli` maps it
64
+ to `process.exit(1)`. Operator remediation: either run `/plan` Phase 7 to
65
+ author the acceptance table, or apply the `acceptance::n-a` label to opt
66
+ out.
67
+
68
+ ---
69
+
70
+ ## Phase 2 — Dispatch rationale and recovery
71
+
72
+ ### Throughput tradeoff
73
+
74
+ The default `concurrencyCap` of 3 is the GLOBAL in-flight cap,
75
+ intentionally conservative — it keeps host-quota consumption low and avoids
76
+ flooding the GitHub API. For Epics with wide dependency-free fronts where
77
+ the host has adequate parallel-agent quota, raising
78
+ `delivery.deliverRunner.concurrencyCap` in `.agentrc.json` reduces
79
+ wall-clock time proportionally to the extra concurrency. The safe default
80
+ is left in place; this is a deliberate operator-tuning knob, not a hidden
81
+ performance ceiling. See `agentrc-reference.json`
82
+ `delivery.deliverRunner.concurrencyCap` for the configuration surface.
83
+
84
+ ### Sub-agent dispatch capability
85
+
86
+ `Agent` calls emit no `model:` argument by default — children inherit from
87
+ the `general-purpose` sub-agent definition and the parent's worktree
88
+ context. No `--dangerously-skip-permissions` (no subprocess is spawned).
89
+ Per [`.agents/instructions.md` § 4](../../instructions.md)'s cost-aware
90
+ spawning heuristic, this optional per-call `model:` is the escape hatch: if
91
+ a specific call would run better on a cheaper or faster capability the host
92
+ exposes (a mechanical or read-only Story), pass `model:` as a per-call
93
+ literal at the `Agent(...)` site. This is **guidance only** — it adds no
94
+ config key, requires no model argument, and names no specific model; the
95
+ host and operator own the concrete choice.
96
+
97
+ ### Fail-closed on an old-shape checkpoint
98
+
99
+ If the Epic still carries a pre-ready-set (`plan` / `currentWave` /
100
+ `totalWaves`) `epic-run-state` checkpoint, the tick **refuses to run** and
101
+ throws an explicit operator message — re-run
102
+ `epic-deliver-prepare.js --epic <id>` to re-seed the checkpoint in the
103
+ per-Story-status shape, then re-run `/deliver`.
104
+
105
+ ### Crash recovery (record step)
106
+
107
+ If the host crashed *after* a child finished but *before* its return was
108
+ recorded, the next `tick` re-derives that Story's state directly from its
109
+ live label (the tick reads labels every beat), so a done-but-unrecorded
110
+ Story is recognised as done and never re-dispatched — there is no
111
+ falsely-`complete` empty wave to recover from. If you want to reconcile a
112
+ known-completed Story whose return text was lost, re-record it from its
113
+ live state by passing `--results '[{"storyId":<n>,"status":"done"}]'`
114
+ (verification re-checks the live label before recording `done`).
115
+
116
+ ### 2e. Idle Watchdog
117
+
118
+ A Story's implementation loop can run for many minutes between
119
+ `story.dispatch.start` and the post-merge `story.merged` record. While
120
+ `observe` keeps the host LLM polling the Epic, it does **not** flag a
121
+ sub-agent that has gone silent (host crash, mid-Story stall, lost
122
+ return). The Idle Watchdog closes that gap.
123
+
124
+ **Cadence.** While any Story is in flight (i.e. `nextAction.kind` is
125
+ `observe` or the most recent dispatch's `in-flight` list is non-empty),
126
+ re-tick every **30 minutes** with the watchdog flag:
127
+
128
+ ```bash
129
+ node .agents/scripts/wave-tick.js --epic <epicId> --check-idle 30
130
+ ```
131
+
132
+ > **Why 30, not 10 (Story #3900).** Heartbeats fire only at Story-level
133
+ > phase transitions, and `implementing → closing` routinely exceeds 10
134
+ > minutes for a healthy Story. A 10-minute threshold therefore tripped the
135
+ > watchdog on every long-running Story, whose prescribed remediation —
136
+ > re-dispatch — put two agents on one `story-<id>` branch (the worst
137
+ > failure mode in the system). The threshold is widened to 30 minutes and
138
+ > the staleness test now also consults a deterministic branch-commit
139
+ > signal (below), so a Story still gaining commits is never flagged.
140
+
141
+ The `--check-idle <minutes>` mode scans the per-Epic lifecycle ledger
142
+ (`temp/epic-<epicId>/lifecycle.ndjson`) for Stories that carry a
143
+ `story.dispatch.start` without a matching `story.dispatch.end` (the
144
+ canonical in-flight list — see § 2a's `nextAction['in-flight']`), and
145
+ compares each in-flight Story's most recent ledger event (any
146
+ `story.*` event, notably the `story.heartbeat` records emitted by
147
+ `story-phase.js` at each Story-level phase transition) against the
148
+ threshold. **Before flagging a stall, it also checks the last commit on
149
+ `story-<id>` via `git log` (Story #3900): a Story whose branch carries a
150
+ commit newer than the threshold is making forward progress and is left
151
+ in-flight, never stalled — deterministic protection against the
152
+ false-positive re-dispatch hazard even when no heartbeat has landed.**
153
+ The CLI emits one envelope on stdout and exits non-zero when at least
154
+ one in-flight Story has been silent for ≥ the threshold:
155
+
156
+ ```json
157
+ {
158
+ "kind": "wave-stall",
159
+ "epicId": <n>,
160
+ "thresholdMinutes": <n>,
161
+ "checkedAt": "<ISO-8601>",
162
+ "stalled": [{ "storyId": <n>, "lastEventAt": "<ISO-8601>", "idleMinutes": <n> }],
163
+ "inFlight": [<storyId>, ...]
164
+ }
165
+ ```
166
+
167
+ **On a stall.** When the watchdog exits non-zero, post the envelope
168
+ verbatim as a `wave-stall` structured comment on the Epic (use
169
+ [`post-structured-comment.js`](../../scripts/post-structured-comment.js)
170
+ with `--kind wave-stall`), then re-evaluate the affected Stories: if a
171
+ child sub-agent has crashed (no `story.dispatch.end`, no recent
172
+ heartbeat, no commit on `story-<id>`), re-dispatch the Story per § 2b of
173
+ the core incrementing the `--attempt` counter; if the child is alive but
174
+ genuinely blocked, flip the Story to `agent::blocked` and proceed per
175
+ § 2d's `observe` branch.
176
+
177
+ Stop the watchdog cadence once `wave-tick.js` returns `epic-complete` —
178
+ there are no in-flight Stories left to monitor.
179
+
180
+ ---
181
+
182
+ ## Phase 6.5 — Post-wave integration gate (Epic #4131, F1/F4)
183
+
184
+ This phase runs **after** the Phase 2 wave loop reports `epic-complete` and
185
+ **before** the Phase 7 finalize emit opens the PR to `main`. Where every gate
186
+ before it (`selectAudits`, the cross-epic-leak guard `#3362`, diff-against-base,
187
+ the file-assumption gate) is **change-set-scoped** — it narrows its evidence to
188
+ the Epic's `changedFiles` — this is the one **deliberately-global** gate: its
189
+ evidence spans the **whole product**, not just the change set. The disease it
190
+ catches is the surface that each Story shipped correctly in isolation yet that
191
+ the assembled product cannot reach: a route nobody can navigate to, or a
192
+ persona journey that the integrated waves silently broke. A change-set lens
193
+ never sees that, because no single Story's diff contains the orphan.
194
+
195
+ Skip when `--skip-integration-gate` (log the override; record a manual
196
+ intervention per [§ Recording manual interventions](#recording-manual-interventions)).
197
+ The gate is otherwise **always evaluated**, but it is a **silent no-op when
198
+ unconfigured** (see *No-op when unconfigured* below) so consumers who ship no
199
+ nav config and no journey suite are unaffected.
200
+
201
+ ### 6.5a — Whole-product navigability (reuses the Phase 4 lens)
202
+
203
+ Run the `navigability` lens
204
+ ([`audit-navigability.md`](../audit-navigability.md), the deliberately-global
205
+ lens delivered in this Epic's wave 0) in **whole-route mode** against the
206
+ integrated `epic/<epicId>` tip. Unlike its Phase 4 change-set-routed invocation,
207
+ here it is driven explicitly over the consumer's **entire** route tree + nav
208
+ registry — it is on the global-lens allowlist (`GLOBAL_LENS_ALLOWLIST` in
209
+ [`lib/audit-suite/selector.js`](../../scripts/lib/audit-suite/selector.js)) and
210
+ exempt from the cross-epic-leak guard `#3362`, so a route orphaned anywhere in
211
+ the product is in scope even when no Story in this Epic touched it.
212
+
213
+ The lens reads the consumer's navigability config (resolved from `.agentrc.json`):
214
+
215
+ - `delivery.quality.navigability.routeGlobs` — the route-tree SSOT the lens
216
+ enumerates.
217
+ - `delivery.quality.navigability.navRegistry` — the nav-registry SSOT the lens
218
+ cross-checks every route against.
219
+
220
+ A whole-product finding is a **hard failure**: an **orphaned route** (a route
221
+ with no nav door for any entitled persona) or a **dead nav href** (a nav door
222
+ pointing at a route that no longer exists). On a hard failure the gate **blocks
223
+ finalize** and names the orphaned surface (route identifier / nav-door
224
+ identifier only — never the route body or persona PII, per
225
+ `security-baseline.md`). Do **not** proceed to Phase 7.
226
+
227
+ ### 6.5b — Consumer journey suite
228
+
229
+ Run the consumer's per-persona journey suite — the integrated, cross-Epic
230
+ persona-journey verification — over the `epic/<epicId>` tip:
231
+
232
+ - `delivery.quality.navigability.journeySuite` — path/command for the journey
233
+ suite. Absent ⇒ this sub-step is skipped (no-op).
234
+
235
+ A failing journey (a persona cannot complete an end-to-end journey through the
236
+ assembled product) is a **hard failure**: block finalize and name the broken
237
+ journey. This is the runtime complement to 6.5a's static reachability check —
238
+ 6.5a proves a door exists; the journey suite proves the door, and everything
239
+ behind it, actually works when the waves are integrated.
240
+
241
+ ### 6.5c — `@pending` ≠ green for surface-adding Epics (F4)
242
+
243
+ The Phase 7 finalize chain runs the acceptance-spec reconciler
244
+ ([`acceptance-spec-reconciler.js`](../../scripts/acceptance-spec-reconciler.js)),
245
+ which classifies every AC ID into `satisfied` (covered by a non-pending
246
+ scenario), `pending` (covered **only** by scenarios tagged `@pending`), or
247
+ `missing`. For a **surface-adding Epic** — one whose risk verdict carries a
248
+ surface-adding signal (a route-adding change set, the `navigability` lens routed
249
+ in Phase 4, or a configured `routeGlobs` match) — this phase tightens that
250
+ contract: an AC whose coverage is **only `@pending`** is treated as
251
+ **unsatisfied**, not green. A surface-adding Epic whose acceptance coverage is
252
+ **only `@pending`** therefore **fails the close gate** instead of passing —
253
+ shipping a new surface behind a deferred-forever `@pending` scenario is exactly
254
+ the late-gate gap this Epic exists to close.
255
+
256
+ This is **purely additive** and **scoped to surface-adding Epics**:
257
+ refactor-only and docs-only Epics (no surface-adding signal) are **unaffected** —
258
+ their `@pending` handling is exactly as before, and the existing
259
+ `satisfied` / `missing` reconciliation is **not** de-scoped for any Epic.
260
+
261
+ ### No-op when unconfigured
262
+
263
+ With **no** navigability config (`routeGlobs` / `navRegistry`) and **no**
264
+ `journeySuite` present in `.agentrc.json`, this entire phase degrades to a
265
+ **silent no-op**: 6.5a and 6.5b skip (nothing to enumerate or run) and 6.5c's
266
+ surface-adding signal cannot fire without route globs, so the `@pending`
267
+ tightening never engages. The gate adds **zero** behaviour to an unconfigured
268
+ consumer — it neither blocks finalize nor changes the existing change-set-scoped
269
+ gates. The override flag `--skip-integration-gate` is the explicit operator
270
+ escape hatch (consistent with `--skip-epic-audit`) for a configured consumer who
271
+ wants to bypass the gate for a specific run.
272
+
273
+ ### Fail safe and loud
274
+
275
+ Per the Tech Spec's security note, this gate sits on the **critical path** to
276
+ finalize. It MUST fail **safe and loud**: a hard failure **blocks** finalize and
277
+ **names the cause** (the orphaned route, dead href, broken journey, or
278
+ `@pending`-only AC), never silently passes. A genuinely unconfigured consumer is
279
+ the **only** silent path, and that path **passes** (it is a no-op, not a block).
280
+ On a hard failure, post a friction structured comment naming the surface, flip
281
+ the Epic to `agent::blocked`, and park for the operator — do **not** open the PR.
282
+
283
+ ---
284
+
285
+ ## Phase 7 — Finalize (close-tail listener chain)
286
+
287
+ ### 7.0 — Sync Epic branch from `main` (Story #2580)
288
+
289
+ Before firing the close-tail emit, sync the Epic branch with
290
+ `origin/main` so the PR opens with the latest base commits already
291
+ integrated. The Epic branch may be behind `main` if other PRs merged
292
+ during the wave loop; without this step, the Epic→`main` PR opens
293
+ "behind base" and (with branch-protection's `up-to-date branch` rule)
294
+ stalls at the merge gate.
295
+
296
+ ```bash
297
+ git checkout epic/<epicId>
298
+ node .agents/scripts/sync-branch-from-base.js \
299
+ --branch epic/<epicId> --base main
300
+ ```
301
+
302
+ Outcomes:
303
+
304
+ - **`fast-forward` / `merge-commit` / `noop-already-current`** → push
305
+ the resulting tip and continue to Phase 7.1: `git push origin epic/<epicId>`.
306
+ - **`conflict`** → resolve in the Epic checkout (`git merge --no-edit
307
+ origin/main`, fix conflicts, `git commit --no-edit`), then re-run the
308
+ sync command. Once it exits 0, continue. Operator-recoverable; not an
309
+ agent loop.
310
+ - **`fetch-failed`** → re-check network / `origin` access and re-run.
311
+
312
+ This is a workflow-level step (operator-driven), not part of the
313
+ close-tail listener chain. The sync runs from the main checkout so
314
+ the resulting tip lands on `epic/<epicId>` before Phase 7.1 fires
315
+ the bus-driven close-tail.
316
+
317
+ ### 7.1 — Close-tail listener chain
318
+
319
+ `node .agents/scripts/lifecycle-emit.js --epic <epicId> --event epic.close.end`
320
+ emits `epic.close.end` onto the lifecycle bus. **Every close-time
321
+ responsibility below runs inside the listener chain — the operator
322
+ shells nothing manually. The `Finalizer` listener (Story #2894 —
323
+ bus-owned finalize) composes three helpers under
324
+ `.agents/scripts/lib/orchestration/finalize/` and emits the canonical
325
+ chain.** Treat this section as a runtime contract — `/deliver`
326
+ just fires the emit and reads the resulting ledger.
327
+
328
+ 1. **Acceptance-table reconciliation — bus-driven.** The
329
+ `AcceptanceReconciler` listener invokes
330
+ [`acceptance-spec-reconciler.js`](../../scripts/acceptance-spec-reconciler.js)
331
+ to diff the AC IDs declared in the Epic body's `## Acceptance Table`
332
+ section against `@ac-*` / `@pending` tags in `tests/features/**`,
333
+ recording each row's verification outcome
334
+ (`satisfied` / `pending` / `missing`) into the table's Disposition
335
+ column via a section-scoped upsert of the Epic body. A
336
+ non-OK reconciliation throws (per
337
+ [`rules/orchestration-error-handling.md`](../../rules/orchestration-error-handling.md)),
338
+ aborting finalize **before** any PR is opened — so the Epic stays
339
+ unfinalized until the AC coverage gap is fixed. The reconciler returns
340
+ `status: 'waived'` without scanning features when the Epic carries
341
+ `acceptance::n-a`, and defends against direct CLI invocation by
342
+ refusing to run when the body has no `## Acceptance Table` section
343
+ and no waiver is set (the
344
+ start gate in Phase 1 would normally catch that first).
345
+ 2. **PR open — bus-driven (Story #2894).** On
346
+ `acceptance.reconcile.ok` the `Finalizer` listener invokes
347
+ [`openOrLocatePr`](../../scripts/lib/orchestration/finalize/open-or-locate-pr.js)
348
+ with `{ epicId, headBranch: 'epic/<id>', baseBranch: 'main' }`.
349
+ The helper probes for an existing open PR on the head branch
350
+ first (idempotent locate path — a re-run of `/deliver`
351
+ on the same branch short-circuits without opening a duplicate)
352
+ and only opens a new PR when none exists. The listener then
353
+ emits `pr.created` → `epic.finalize.end` and **stops** (Story
354
+ #3367). It does **not** emit `epic.merge.ready`: that event is
355
+ the sole `AutomergeArmer` trigger, and emitting it from finalize
356
+ would cascade `epic.close.end` synchronously through the arm →
357
+ `MergeWatcher` → `Cleaner` → `BranchCleaner` reap, deleting the
358
+ `epic/<id>` branch before the PR merged and bypassing the
359
+ `AutomergePredicate` disqualification gate. The auto-merge arm is
360
+ driven later from the gated watch path (`pr.created` → `Watcher`
361
+ → `epic.watch.end` → `AutomergePredicate` → `epic.merge.ready` →
362
+ `AutomergeArmer`) re-entered in Phase 8.5. The merge-lockout rule
363
+ in [`check-lifecycle-lint.js`](../../scripts/check-lifecycle-lint.js)
364
+ keeps `gh pr merge --auto --squash --delete-branch` confined to
365
+ `AutomergeArmer` — Phase 7 never shells the merge command.
366
+ 3. **Hand-off — bus-driven (Story #2894).** After `openOrLocatePr`
367
+ returns, the `Finalizer` chains
368
+ [`postHandoffComment`](../../scripts/lib/orchestration/finalize/post-handoff-comment.js)
369
+ to upsert the canonical `epic-handoff` structured comment naming
370
+ the PR URL. The helper is idempotent — the handoff comment is
371
+ edited in place via `upsertStructuredComment` rather than
372
+ appending a duplicate. There is **no planning-ticket close sweep**
373
+ (Story #4324): the planning artifacts live as sections of the Epic
374
+ body itself, so there are no context tickets to close and nothing
375
+ blocks the Epic's `Closes #<id>` auto-close path.
376
+
377
+ Branch cleanup is out-of-band (Phase 9 reaps local refs after merge; the
378
+ rare "scrap and reset" case for an unmerged Epic is handled manually).
379
+
380
+ ---
381
+
382
+ ## Phase 8 — Watch-and-iterate remediation
383
+
384
+ ### 8.1 Remediation
385
+
386
+ For each failed required check: fetch the log
387
+ (`gh run view <runId> --log-failed`), classify and fix:
388
+
389
+ - **lint / format** → `npm run lint` + `npx biome check --apply` (or
390
+ `format --write`); commit, push.
391
+ - **maintainability / crap baseline drift** → re-run the ratcheted
392
+ script. Refresh the baseline only when drift is justified by the
393
+ diff; otherwise fix at source.
394
+ - **test failure** → reproduce with `npm test`, fix source or test.
395
+ - **coverage threshold** → add tests (preferred); refresh baseline only
396
+ when the diff demonstrably can't be covered.
397
+ - **anything else** → read the log, fix at source.
398
+
399
+ Push to `epic/<epicId>` and re-run
400
+ `node <agentRoot>/scripts/pr-watch-with-update.js --pr <prNumber>`.
401
+
402
+ ### 8.2 When to halt
403
+
404
+ Three consecutive iterations on the same failure class without
405
+ convergence → friction comment, flip to `agent::blocked`, park. Unknown
406
+ failure class on first encounter → attempt source-level fix; log
407
+ friction if diagnosis takes more than one round.
408
+
409
+ ### 8.3 Hard prohibitions
410
+
411
+ **Never** `gh pr merge` from Phase 8 (Phase 8.5 is the only merge
412
+ site). **Never** force-push to `main`. **Never** push empty commits or
413
+ refresh baselines to dodge a red check.
414
+
415
+ ---
416
+
417
+ ## Phase 8.5 — Auto-merge predicate detail
418
+
419
+ `AutomergePredicate` subscribes to `epic.automerge.start` (Story #3901 —
420
+ before that this event had **zero** subscribers and the entire Phase 8.5
421
+ gate was a dead wire). It evaluates the structured-signal verdict and
422
+ emits `epic.merge.ready` on a clean verdict or `epic.merge.blocked`
423
+ otherwise. The CI-freshness gate is skipped on this event because Phase 8
424
+ already polled every required check to green — `epic.automerge.start`
425
+ carries `prUrl` but no `checkOutcomes`.
426
+
427
+ The `AutomergeArmer` listener subscribes to the downstream
428
+ `epic.merge.ready` outcome and fires `gh pr merge --auto --squash
429
+ --delete-branch` only when `clean: true`. `clean: true` requires
430
+ **all** of:
431
+
432
+ - `state.manualInterventions[]` is empty;
433
+ - every wave's `status === "complete"`;
434
+ - no story envelope carries a `blockerCommentId` or non-`done` status;
435
+ - code-review reports `0` 🔴 + `0` 🟠 findings;
436
+ - the retro's machine-readable `automerge-verdict` trailer reports
437
+ `cleanSprint: true` (Story #3901 — the predicate reads the parsed JSON
438
+ trailer `retro-run.js` writes into the retro body, **not** an emoji
439
+ string-match on the human-facing "🟢 Clean sprint" prose).
440
+
441
+ When clean, the listener fires `gh pr merge --squash --delete-branch`.
442
+ Otherwise the listener records disqualifying reasons via
443
+ `epic.merge.blocked` and exits without merging — operator merges
444
+ manually.
445
+
446
+ ### Recording manual interventions
447
+
448
+ Whenever you step outside the happy path during a delivery, record it
449
+ (each entry disqualifies auto-merge):
450
+
451
+ ```bash
452
+ node .agents/scripts/epic-deliver-note-intervention.js \
453
+ --epic <epicId> --reason "<one-line description>"
454
+ ```
455
+
456
+ Triggers: `AskUserQuestion` mid-run; `git restore`/`reset` against the
457
+ tree; child-reported `--no-ff` recovery, stash dance, or out-of-band
458
+ merge surgery; child closes via `--skipValidation`; force-pushing or
459
+ empty-committing to dodge CI diagnosis.
460
+
461
+ ---
462
+
463
+ ## Phase 9 — Local branch cleanup detail
464
+
465
+ Phase 9 runs **automatically** inside the lifecycle bus once auto-merge
466
+ arms: the `BranchCleaner` listener subscribes to `epic.cleanup.start`
467
+ and reaps local refs before `Cleaner` archives the `temp/epic-<id>/`
468
+ tree. No operator step is required on the auto-merge path.
469
+
470
+ What gets reaped (in order, all in-process):
471
+
472
+ 1. The main checkout is switched off `epic/<id>` to `baseBranch` when
473
+ needed (otherwise `git branch -D epic/<id>` is refused).
474
+ 2. Every `story-<id>` listed in the `epic-run-state` checkpoint, plus
475
+ `epic/<id>`. Attached worktrees are removed with the standard
476
+ `git worktree remove` → `--force` → filesystem-rm fallback (the
477
+ last step covers Windows file-locks).
478
+ 3. `git remote prune <remote>` drops stale `<remote>/...` tracking
479
+ refs left behind by `gh pr merge --delete-branch`.
480
+ 4. The `wt-branch` scratch ref left by `story-close.js`'s internal
481
+ merge worktree is deleted when no worktree still points at it.
482
+
483
+ Per-branch failures aggregate into the listener's classification log
484
+ (`reaped` / `failed` / `no-state` / `skipped-duplicate`) and are
485
+ visible in `temp/epic-<id>/lifecycle.ndjson`. They do not block the
486
+ rest of cleanup.
487
+
488
+ For out-of-band cleanup re-entry (resume after a crash, or operator
489
+ override), fire `epic.merge.armed` via the lifecycle-emit helper:
490
+
491
+ ```bash
492
+ node .agents/scripts/lifecycle-emit.js --epic <epicId> \
493
+ --event epic.merge.armed --pr-url <prUrl>
494
+ ```
495
+
496
+ If Phase 8.5 fell back to the operator-merges-button path (`gh pr
497
+ merge --auto` was declined), the `epic.merge.armed` event never fires
498
+ inside this run and Phase 9 will not run automatically. After the
499
+ operator merges the PR, `epic/<epicId>` and each `story-<id>` ref can
500
+ be reaped manually:
501
+
502
+ ```bash
503
+ git checkout main
504
+ git pull --ff-only origin main
505
+ git branch -D epic/<epicId>
506
+ git branch -D story-<id1> story-<id2> ...
507
+ git remote prune origin
508
+ ```
509
+
510
+ Note that `git-cleanup.js` alone will not catch `story-<id>` refs in
511
+ this case because the epic PR squash-merges break the `git branch
512
+ --merged main` signal and the stories never had their own PRs. Wiring
513
+ a CLI surface that drives the BranchCleaner listener for this
514
+ fallback is tracked as follow-up to Story #2398.