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,423 @@
1
+ ---
2
+ description: >-
3
+ Reference companion to `helpers/single-story-deliver.md` — the lease and
4
+ sweep detail, worktree-scope warnings, CI-recovery procedures, and
5
+ Status-column reconciliation lifted out of the runtime core so the
6
+ always-ingested standalone-delivery prose stays lean. Not a slash command;
7
+ consulted on demand when the core file points here.
8
+ caller: helpers/single-story-deliver.md
9
+ ---
10
+
11
+ # helpers/single-story-deliver — reference (lease, recovery, troubleshooting)
12
+
13
+ > **Not a slash command, not the runtime path.** This file is the
14
+ > reference companion to
15
+ > [`single-story-deliver.md`](single-story-deliver.md). The core file
16
+ > carries the step flow, commands, gate contracts, and the return contract a
17
+ > standalone-Story run needs; this file holds the lease/sweep mechanics, the
18
+ > worktree-scope safety warning, the CI-recovery procedures, and the
19
+ > Status-column reconciliation the core points at with one-line pointers.
20
+ > Read a section here only when the matching pointer in the core sends you.
21
+
22
+ ---
23
+
24
+ ## Step 0 — Lease preflight and merged-sweep
25
+
26
+ ### Lease preflight (Story #3483)
27
+
28
+ Before any git mutation, init takes an exclusive, time-bounded **lease** on
29
+ the Story ticket via the assignee-as-lease primitive
30
+ (`lib/orchestration/ticket-lease.js`). The single assignee *is* the lease
31
+ owner (resolved from `github.operatorHandle`). The standalone path has no
32
+ Epic-scoped dispatch manifest to serialise two operators driving the same
33
+ Story, so this lease is the only guard against a concurrent
34
+ `single-story-init` clobbering an in-flight run.
35
+
36
+ **Fail-closed (audit #3513).** Unlike `/deliver`, the standalone path
37
+ has **no Epic-scoped lifecycle ledger** to read a per-owner
38
+ `story.heartbeat` from, so there is no live-heartbeat source to decide
39
+ whether a foreign claim is stale. Rather than silently reclaim every
40
+ foreign assignee (which would leave the guard inert), the standalone lease
41
+ **fails closed**: a foreign assignee is treated as a *live* claim. Outcomes:
42
+
43
+ - **Unclaimed / self-held** → init proceeds (a self-held claim is
44
+ re-affirmed without re-writing assignees).
45
+ - **Any foreign assignee** → init **exits non-zero** with a message naming
46
+ the current owner. Coordinate with that operator, or pass **`--steal`** to
47
+ forcibly transfer the claim once you have confirmed the other run is dead.
48
+
49
+ `--dry-run` skips the lease (no assignee mutation). The matching release
50
+ runs in `single-story-close.js` (Step 3).
51
+
52
+ ### Branch reuse (Story #3483)
53
+
54
+ When a `story-<id>` branch already exists locally, init **reuses** it rather
55
+ than re-creating it (re-running `git branch` on an existing ref throws
56
+ `branch already exists`). The seed decision (`reuse` / `fetch` / `create`)
57
+ keys off local + remote ref presence, so re-running init on a
58
+ partially-initialized Story is idempotent.
59
+
60
+ ### Merged-`story-*` sweep
61
+
62
+ Between the fetch and the branch-seed step, the script runs a
63
+ **merged-`story-*` sweep**: it invokes the same primitive as
64
+ `<agentRoot>/scripts/git-cleanup.js` (`<agentRoot>` resolves
65
+ from `project.paths.agentRoot`, default `.agents`) scoped to `story-*`
66
+ only, in `--execute --remote` mode, with the current run's
67
+ `story-<id>` branch excluded from the candidate list. Local refs, the
68
+ matching `origin/` ref, and stale tracking refs for any merged sibling
69
+ stories are reaped in one pass. The sweep never blocks init — failures
70
+ are logged and the new story is initialized regardless.
71
+
72
+ The sweep applies two hardening layers (Story #2011):
73
+
74
+ - **Per-candidate protection.** Each merged-PR candidate is filtered
75
+ through three guards before reaching `executeCleanup`:
76
+ - `unpushed-work` — branch HEAD SHA differs from the PR's
77
+ `headRefOid`, meaning the operator has commits the merge didn't
78
+ capture.
79
+ - `dirty-tree` — the attached worktree (if any) has uncommitted
80
+ changes.
81
+ - `ticket-not-done` — the parent Story ticket isn't closed and
82
+ doesn't carry `agent::done`.
83
+ Protected candidates are skipped, listed in the sweep result envelope
84
+ under `protected[]`, and named in the `CLEANUP` log line so the
85
+ operator can see what was preserved.
86
+ - **Cross-session lock.** The sweep acquires a process-scoped lockfile
87
+ at `<tempRoot>/single-story-sweep.lock` before planning. On
88
+ contention (another `/single-story-deliver` already in the sweep
89
+ step), this run's sweep is **skipped** with a warn log; init
90
+ continues normally. Stale lockfiles (mtime older than the timeout)
91
+ are treated as expired. The timeout defaults to 60 seconds and is
92
+ overridable via `delivery.worktreeIsolation.sweepLockMs` in
93
+ `.agentrc.json`.
94
+
95
+ Both layers are non-fatal — sweep failure / skip never blocks init, and
96
+ the new story is always created. `--dry-run` also skips the sweep.
97
+
98
+ ### Worktree scope is not just the Bash cwd
99
+
100
+ `cd <workCwd>` steers the **Bash** tool's working directory, but it does
101
+ **not** scope the path-based **Edit/Write/Read** tools — those resolve
102
+ **absolute paths** and ignore the shell cwd. On Windows especially, an agent
103
+ whose shell sits in the worktree can still silently edit the **main
104
+ checkout** if it resolves a main-checkout absolute path. To stay in the
105
+ worktree you MUST prefix **every Edit/Write/Read path with the absolute
106
+ worktree root** (the `workCwd` value from Step 0), not merely `cd` into it.
107
+ Never edit files under the bare main-checkout root. `single-story-close.js`
108
+ runs a **wrong-tree guard** (Story #3364) that aborts close and posts a
109
+ `friction` comment if it finds uncommitted tracked-path edits in the main
110
+ checkout while the worktree is the active work tree — but that is a backstop,
111
+ not a substitute for prefixing paths correctly.
112
+
113
+ ---
114
+
115
+ ## Step 3 — Close pipeline detail
116
+
117
+ The `single-story-close.js` script, in order:
118
+
119
+ 1. Runs the close-validation gates against `baseBranch` as the baseline.
120
+ On any gate failure it throws — the operator fixes and re-runs close.
121
+ 1a. **Syncs the Story branch from `origin/<baseBranch>`** before push
122
+ (Story #2580). Runs `git fetch origin <baseBranch>` followed by
123
+ `git merge --no-edit origin/<baseBranch>` inside the worktree. This
124
+ defends against the parallel-`/single-story-deliver` race: when
125
+ multiple sessions run in parallel, the Story that auto-merges first
126
+ bumps `baseBranch`, and without this sync the lagging Stories open
127
+ PRs that are "behind base" and stall against branch-protection's
128
+ `up-to-date branch` rule. Outcomes:
129
+ - **No-op / fast-forward / clean merge-commit** → close proceeds to
130
+ push.
131
+ - **Merge conflict** → the merge is aborted, a `friction` structured
132
+ comment is posted on the Story (conflicting file list + recovery
133
+ command set), the Story flips to `agent::blocked`, and close
134
+ throws. Resolve in the worktree (`git merge origin/<base>` + fix
135
+ conflicts + `git commit --no-edit`) and re-run
136
+ `/single-story-deliver`.
137
+ - **Fetch failed** → close throws with the git stderr; no label
138
+ transition.
139
+
140
+ Note: the merge queue (when enabled) re-tests each PR against the
141
+ queue tip before merging, so this sync + merge queue is the complete
142
+ defence against the parallel race. Without merge queue, the sync
143
+ closes the PR-open-time race but a residual race remains between PR
144
+ open and auto-merge fire.
145
+ 2. Pushes `story-<id>` to `origin`.
146
+ 3. Probes for an existing open PR with `head = story-<id>`. If none
147
+ exists, opens one via `gh pr create --base <baseBranch>`. The PR
148
+ body carries `Closes #<storyId>` so the GitHub merge auto-closes the
149
+ issue.
150
+ 3a. **Enables GitHub native auto-merge by default** via
151
+ `gh pr merge <prNumber> --auto --squash --delete-branch`. Once CI's
152
+ required checks turn green, GitHub squash-merges the PR and deletes
153
+ the source branch — the operator does not need to babysit the merge
154
+ button. Mirrors the `/deliver` finalize path. Failure is
155
+ non-fatal: the operator retains the manual merge surface in the
156
+ GitHub UI. Pass `--no-auto-merge` to opt out when the PR needs a
157
+ pre-merge eyeball.
158
+ 4. Flips the Story to **`agent::closing`** (NOT `agent::done`) and leaves
159
+ the GitHub issue **OPEN** (Story #3385). Auto-merge completes
160
+ asynchronously *after* this script exits, so closing the issue here
161
+ would strand a CLOSED issue with no merged work if the PR later failed
162
+ CI, went `BEHIND` base, or was closed without merging. The Story rests
163
+ at `agent::closing` while the PR is open with auto-merge armed; the
164
+ `agent::done` flip (which closes the issue) is deferred to Step 5.5's
165
+ `single-story-confirm-merge.js`. This brings the standalone path to
166
+ parity with the epic path (#2155), where a Story only reaches
167
+ `agent::done` once its merge into `epic/<id>` is confirmed.
168
+ 5. Reaps the worktree when `delivery.worktreeIsolation.reapOnSuccess`
169
+ is enabled.
170
+ 6. **Releases the Story lease** (Story #3483). Clears the Story assignment
171
+ that init claimed so the next `/single-story-deliver` run sees an
172
+ unclaimed ticket. The release is a no-op when the operator no longer
173
+ holds the claim (a later run took over via reclaim/steal), so a late
174
+ close never yanks a live claim away from its current owner. Best-effort:
175
+ a release failure is logged but does not fail an otherwise-clean close —
176
+ the lease goes stale via TTL regardless. The close result carries
177
+ `leaseReleased: <boolean>`.
178
+
179
+ `--skip-validation` bypasses Step 1 (gates). Use only when re-running
180
+ close after a fixed gate failure that's already known to pass.
181
+
182
+ `--skip-sync` bypasses Step 1a (base-sync). Use only when re-running
183
+ close after a hand-resolved sync, or in tests.
184
+
185
+ `--no-auto-merge` disables Step 3a. Use when the PR materially changes
186
+ behaviour and warrants pre-merge review.
187
+
188
+ ---
189
+
190
+ ## Step 4 — CI watch + fix recovery
191
+
192
+ ### The auto-merge wait is an internally-blocking step
193
+
194
+ This is the single most important contract of this workflow, and the seam
195
+ where a worker most often misbehaves: it delivers up to arming auto-merge,
196
+ then ends its turn with **free-form prose** — e.g. "I'll wait for the
197
+ background watch task to complete" or "the next event will be its completion
198
+ notification" — leaving the merge unconfirmed and the Story stranded at
199
+ `agent::closing` (observed on Story #1553 / PR #1554). **Do not do this.**
200
+ `gh pr checks <prNumber> --watch` *blocks the current turn* until CI resolves
201
+ — that is the mechanism by which you wait. You MUST keep your turn alive
202
+ across the wait: watch → (fix + push + re-watch on red) → confirm the merge
203
+ (Step 5) → flip `agent::done` → run the post-merge steps → and only then
204
+ return the terminal JSON status contract. The CI wait NEVER terminates your
205
+ turn; **only** a confirmed-`MERGED` PR (→ `status: "done"`), an
206
+ `agent::blocked` transition (→ `status: "blocked"`), or an unrecoverable
207
+ failure (→ `status: "failed"`) does. Ending your turn with prose and an
208
+ unconfirmed merge is a contract violation — it is the very bug this workflow
209
+ exists to prevent.
210
+
211
+ ### Resurrecting the worktree after `reapOnSuccess`
212
+
213
+ `single-story-close.js` reaps the worktree on success when
214
+ `delivery.worktreeIsolation.reapOnSuccess` is enabled (the default). To
215
+ fix CI you must re-attach a worktree to the existing remote branch:
216
+
217
+ ```bash
218
+ cd <main-repo>
219
+ git fetch origin story-<storyId>
220
+ git worktree add .worktrees/story-<storyId> story-<storyId>
221
+ cd .worktrees/story-<storyId>
222
+ ```
223
+
224
+ Do **not** re-run `single-story-init.js` — it would reset the branch
225
+ state and lose the close commit's structured comment.
226
+
227
+ ### Diagnosing the failure
228
+
229
+ Pull the failing job log via:
230
+
231
+ ```bash
232
+ gh run view <runId> --repo <owner>/<repo> --log-failed
233
+ ```
234
+
235
+ The `<runId>` is the run number that `gh pr checks` shows in the
236
+ failing row's URL. Read the bottom of the log — the gate that exited
237
+ non-zero is named there (e.g. `[Coverage] ❌ REGRESSION in …`).
238
+
239
+ ### Fixing without re-running close-validation
240
+
241
+ For coverage / maintainability / CRAP regressions detected only on CI:
242
+
243
+ 1. Update the relevant baseline file (`baselines/coverage.json`,
244
+ `baselines/maintainability.json`, `baselines/crap.json`) to absorb
245
+ CI's actual numbers. Edit by hand when CI's numbers are within the
246
+ tolerance you'd otherwise accept — don't re-run `npm run … :update`
247
+ locally, because Windows numbers will overwrite CI's Linux numbers
248
+ and the cycle repeats.
249
+ 2. Commit the baseline delta with a `chore(baselines):` message that
250
+ names the CI run that produced the values.
251
+ 3. `git push` to `origin/story-<storyId>` and re-watch.
252
+
253
+ For genuine test failures (a flaky test, a platform-conditional bug):
254
+ fix the code or test, commit, push, re-watch. Keep iterating until
255
+ the watch exits clean.
256
+
257
+ ### When to stop iterating
258
+
259
+ - **Three consecutive failures with the same fix shape** — stop and
260
+ Re-Plan per Anti-Thrashing Protocol. The diagnosis is likely wrong.
261
+ - **Operator-blocking failure** (security scanner, branch-protection
262
+ rule the agent can't change) — transition the Story to
263
+ `agent::blocked`, summarize the blocker on the PR, and yield to the
264
+ operator.
265
+
266
+ ### Idempotence of the loop
267
+
268
+ - The PR stays open across retries; `gh pr create` is a one-shot at
269
+ close, the loop only pushes new commits.
270
+ - Auto-merge stays armed across retries — pushing a new commit does
271
+ not disarm `gh pr merge --auto`.
272
+ - If the operator manually merges or disables auto-merge mid-loop,
273
+ exit the loop and report.
274
+
275
+ ---
276
+
277
+ ## Step 5 — Merge confirmation detail
278
+
279
+ `single-story-confirm-merge.js` re-reads the live PR state (`gh pr view
280
+ --json state,mergedAt`, probing `gh pr list --head story-<id> --state all`
281
+ when `--pr` is omitted) and:
282
+
283
+ - **PR `MERGED`** → flips `agent::closing → agent::done`, closing the
284
+ issue, and fires the `story-merged` notify. Prints
285
+ `{ action: 'done', merged: true, ... }`.
286
+ - **PR still open / closed-without-merge** → leaves the Story at
287
+ `agent::closing` (issue stays OPEN) and prints
288
+ `{ action: 'pending', reason: 'pr-open' | 'pr-not-merged' | 'no-pr' }`.
289
+ Re-run after the merge lands.
290
+ - **Story already `agent::done` / issue already closed** → idempotent
291
+ `{ action: 'noop', reason: 'already-done' }`.
292
+
293
+ This is the standalone counterpart to the epic path's post-merge
294
+ `agent::done` flip in `post-merge-close.js` (#2155): the issue closes
295
+ exactly when the work has merged, never at PR-open.
296
+
297
+ ---
298
+
299
+ ## Step 5.5 — Re-assert Status column detail
300
+
301
+ The GitHub Projects v2 built-in workflows `Pull request merged` and
302
+ `Pull request linked to issue` are enabled by default on most boards
303
+ and fire ~minutes *after* auto-merge lands. They overwrite the Status
304
+ field as a side-effect, clobbering the `Done` value
305
+ `single-story-confirm-merge.js` set at the `agent::done` flip in Step 5
306
+ and leaving closed Stories stuck at `In Progress` on the board
307
+ (reproduced on Story #2813). The confirmation step has already exited by
308
+ then, so the bot gets the last write.
309
+
310
+ `resync-status-column.js`:
311
+
312
+ - Reads the ticket's current `agent::*` label set (now `agent::done`).
313
+ - Re-fires the same `ColumnSync` mutation `transitionTicketState` used
314
+ at close, overwriting the bot's late write.
315
+ - **Polls the live Status for ~15 s after the initial write** and
316
+ re-fires on drift (Story #2876). Without this loop, a one-shot
317
+ mutation routinely lost the race against the bot's asynchronous
318
+ fire (reproduced on Story #2871 / PR #2872).
319
+ - Prints a single-line JSON envelope:
320
+ `{ ticketId, status, column?, reason?, attempts? }`. `attempts > 1`
321
+ means the helper had to fight a bot overwrite; `status: 'drifted'`
322
+ means the bot won every attempt in the poll budget (rare; usually
323
+ signals operator should reap the conflicting workflows).
324
+
325
+ Tuning flags (rarely needed):
326
+
327
+ - `--poll-attempts <n>` — total mutation attempts including the
328
+ initial sync. Default `4`. Pass `1` to disable the poll loop
329
+ (fastest, matches pre-#2876 behaviour).
330
+ - `--poll-delay-ms <ms>` — delay between drift checks. Default `5000`.
331
+
332
+ Idempotent: re-running on a ticket whose Status already matches the
333
+ target returns the same envelope. No-op skips (`no-project`,
334
+ `no-meta`, `not-on-project`) exit 0 with the reason in the envelope
335
+ so the workflow can continue.
336
+
337
+ **Canonical operator fix:** run
338
+ `node .agents/scripts/agents-bootstrap-github.js --reap-conflicting-workflows`
339
+ once per project to delete the conflicting bot workflows entirely.
340
+ This eliminates the race source; the poll loop becomes pure
341
+ defense-in-depth against re-enabled or future workflows.
342
+
343
+ ---
344
+
345
+ ## Step 6 — Local branch cleanup detail
346
+
347
+ GitHub deletes the **remote** branch on auto-merge (via the
348
+ `--delete-branch` flag `single-story-close.js` passes to `gh pr merge`).
349
+ The **local** `story-<storyId>` ref, however, lingers in the main
350
+ checkout until something prunes it — `single-story-init.js` runs a
351
+ merged-sweep at the start of every *subsequent* `/single-story-deliver`
352
+ invocation, but that's next-run cleanup, not end-of-run cleanup. Stale
353
+ local refs accumulate between sessions, clutter `git branch`, and shadow
354
+ the lessons the sweep is meant to surface.
355
+
356
+ **Why local `main` goes stale:** `single-story-init.js` seeds new
357
+ `story-<id>` branches from the **local** `baseBranch` ref (default
358
+ `main`). Auto-merge updates **`origin/main`** on GitHub; nothing in
359
+ close or the old Step 6 command updated **local `main`**. The next init
360
+ then forked from a tip six merges behind until you manually pulled.
361
+ `single-story-init` also attempts the same fast-forward after `git fetch`
362
+ when the main checkout is clean (defense in depth if Step 6 was skipped).
363
+ Step 6 must still run `--fast-forward-main` so local `main` is current
364
+ before the next session — init may skip when the tree is dirty or the
365
+ operator is mid-checkout on another branch.
366
+
367
+ What the Step 6 cleanup command does:
368
+
369
+ - **`--fast-forward-main`** fetches `origin/<baseBranch>` and
370
+ `git merge --ff-only` on the main checkout when the tree is clean and
371
+ the local base is strictly behind remote. Skipped when already current,
372
+ dirty, or diverged (see `/git-cleanup`).
373
+ - **`--branches`** reaps the merged `story-<storyId>` ref (worktree,
374
+ local branch, stale `origin/` tracking ref). Does not run
375
+ `--prune-remotes` or `--stashes` unless you add those flags.
376
+ - **`--include "story-<storyId>"`** scopes the branch reap to this
377
+ Story's ref only — sibling stories in flight are untouched.
378
+ - **`--execute --remote --yes`** actually deletes the local ref, prunes
379
+ the matching `origin/` tracking ref, and runs non-interactively.
380
+
381
+ The sweep is idempotent. It is safe to run before `state: "MERGED"`
382
+ confirms (it will skip a not-yet-merged branch), and safe to re-run
383
+ after a successful cleanup (it reports "no merged branches to clean
384
+ up").
385
+
386
+ ---
387
+
388
+ ## Step 7 — Return-contract detail
389
+
390
+ **The auto-merge wait does not produce a fourth status.** There is no
391
+ "pending" or "waiting" terminal — the CI/auto-merge wait is handled
392
+ *internally* by blocking on `gh pr checks --watch` (Step 4) and confirming
393
+ the merge (Step 5). You return **only** when you have reached a genuinely
394
+ terminal state:
395
+
396
+ - **`status: "done"`** — the PR is confirmed `state: "MERGED"` (Step 5),
397
+ the Story carries `agent::done`, and Steps 5.5 / 6 have run. `phase: "done"`,
398
+ `branchDeleted: true`.
399
+ - **`status: "blocked"`** — you transitioned the Story to `agent::blocked`
400
+ and posted a `friction` comment (acceptance self-eval block in Step 1a, a
401
+ base-sync conflict, or an operator-blocking CI failure / Anti-Thrashing
402
+ stop in Step 4). `phase: "blocked"`, `blockerCommentId` set.
403
+ - **`status: "failed"`** — an unrecoverable failure outside the blocked
404
+ protocol. `phase` reflects where it died.
405
+
406
+ A turn that ends with prose ("I'll wait for the watch task…", "the next event
407
+ will be its completion notification…") and an **unconfirmed merge** is a
408
+ **contract violation** (the Story #1553 / PR #1554 failure mode): the parent
409
+ wave loop cannot distinguish "still working" from "done but silent", and the
410
+ Story strands at `agent::closing`. If you genuinely cannot confirm the merge,
411
+ that is a `blocked` or `failed` outcome with the JSON contract — not a
412
+ prose hand-off.
413
+
414
+ > **Handoff discipline — report state, not process.** Populate the envelope
415
+ > with essential terminal state only (mirroring the fields
416
+ > `single-story-close.js` / `story-phase.js` already emit). Do not narrate the
417
+ > steps you took, and do not prescribe how the next stage should work. Prose
418
+ > process commentary only bloats the hydrated prompt
419
+ > (`delivery.maxTokenBudget` elision). When run **interactively** (no parent
420
+ > aggregator), this JSON envelope is optional — relay terminal state to the
421
+ > operator in prose instead — but the **no-park rule still holds**: never end
422
+ > an interactive turn with an unconfirmed merge either; block on the watch,
423
+ > confirm, and report the merged outcome.