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
@@ -7,6 +7,15 @@ description:
7
7
 
8
8
  # /single-story-deliver #[Story ID]
9
9
 
10
+ > **Runtime core.** This file is the always-ingested standalone-Story path:
11
+ > step flow, commands, gate contracts, and the return contract. The lease /
12
+ > sweep mechanics, the worktree-scope safety warning, the CI-recovery
13
+ > procedures, and the Status-column reconciliation live in the sibling
14
+ > [`single-story-deliver-reference.md`](single-story-deliver-reference.md);
15
+ > each moved procedure keeps a one-line pointer at its trigger point below.
16
+ > The reference is not projected to `.claude/commands/` — it is consulted on
17
+ > demand.
18
+
10
19
  ## Overview
11
20
 
12
21
  `/single-story-deliver` is the standalone counterpart to
@@ -59,7 +68,7 @@ node .agents/scripts/single-story-init.js --story <storyId>
59
68
  ```
60
69
 
61
70
  Flags: `--dry-run` (no git/ticket mutation), `--steal` (forcibly transfer a
62
- foreign Story lease to this operator — see the fail-closed lease note below).
71
+ foreign Story lease to this operator — see the lease note below).
63
72
 
64
73
  > **Execution mode.** Like `story-init.js`, this command can take 3–6
65
74
  > minutes when the worktree's per-tree install runs. Invoke synchronously
@@ -71,81 +80,22 @@ The script validates `type::story`, **acquires the Story lease**, fetches
71
80
  `origin`, seeds `story-<id>` from `baseBranch`, materializes a worktree
72
81
  (when `delivery.worktreeIsolation.enabled` is true), upserts a
73
82
  `story-init` structured comment carrying `standalone: true`, and flips
74
- the Story to `agent::executing`.
75
-
76
- > **Lease preflight (Story #3483).** Before any git mutation, init takes an
77
- > exclusive, time-bounded **lease** on the Story ticket via the
78
- > assignee-as-lease primitive (`lib/orchestration/ticket-lease.js`). The
79
- > single assignee *is* the lease owner (resolved from
80
- > `github.operatorHandle`). The standalone path has no Epic-scoped dispatch
81
- > manifest to serialise two operators driving the same Story, so this lease
82
- > is the only guard against a concurrent `single-story-init` clobbering an
83
- > in-flight run.
84
- >
85
- > **Fail-closed (audit #3513).** Unlike `/deliver`, the standalone path
86
- > has **no Epic-scoped lifecycle ledger** to read a per-owner
87
- > `story.heartbeat` from, so there is no live-heartbeat source to decide
88
- > whether a foreign claim is stale. Rather than silently reclaim every
89
- > foreign assignee (which would leave the guard inert), the standalone lease
90
- > **fails closed**: a foreign assignee is treated as a *live* claim. Outcomes:
91
- >
92
- > - **Unclaimed / self-held** → init proceeds (a self-held claim is
93
- > re-affirmed without re-writing assignees).
94
- > - **Any foreign assignee** → init **exits non-zero** with a message naming
95
- > the current owner. Coordinate with that operator, or pass **`--steal`** to
96
- > forcibly transfer the claim once you have confirmed the other run is dead.
97
- >
98
- > `--dry-run` skips the lease (no assignee mutation). The matching release
99
- > runs in `single-story-close.js` (Step 3).
100
-
101
- Init is also idempotent on the Story branch itself:
102
-
103
- > **Branch reuse (Story #3483).** When a `story-<id>` branch already exists
104
- > locally, init **reuses** it rather than re-creating it (re-running
105
- > `git branch` on an existing ref throws `branch already exists`). The
106
- > seed decision (`reuse` / `fetch` / `create`) keys off local + remote ref
107
- > presence, so re-running init on a partially-initialized Story is
108
- > idempotent.
109
-
110
- Between the fetch and the branch-seed step, the script also runs a
111
- **merged-`story-*` sweep**: it invokes the same primitive as
112
- `<agentRoot>/scripts/git-cleanup.js` (`<agentRoot>` resolves
113
- from `project.paths.agentRoot`, default `.agents`) scoped to `story-*`
114
- only, in `--execute --remote` mode, with the current run's
115
- `story-<id>` branch excluded from the candidate list. Local refs, the
116
- matching `origin/` ref, and stale tracking refs for any merged sibling
117
- stories are reaped in one pass. The sweep never blocks init — failures
118
- are logged and the new story is initialized regardless.
119
-
120
- The sweep applies two hardening layers (Story #2011):
121
-
122
- - **Per-candidate protection.** Each merged-PR candidate is filtered
123
- through three guards before reaching `executeCleanup`:
124
- - `unpushed-work` — branch HEAD SHA differs from the PR's
125
- `headRefOid`, meaning the operator has commits the merge didn't
126
- capture.
127
- - `dirty-tree` — the attached worktree (if any) has uncommitted
128
- changes.
129
- - `ticket-not-done` — the parent Story ticket isn't closed and
130
- doesn't carry `agent::done`.
131
- Protected candidates are skipped, listed in the sweep result envelope
132
- under `protected[]`, and named in the `CLEANUP` log line so the
133
- operator can see what was preserved.
134
- - **Cross-session lock.** The sweep acquires a process-scoped lockfile
135
- at `<tempRoot>/single-story-sweep.lock` before planning. On
136
- contention (another `/single-story-deliver` already in the sweep
137
- step), this run's sweep is **skipped** with a warn log; init
138
- continues normally. Stale lockfiles (mtime older than the timeout)
139
- are treated as expired. The timeout defaults to 60 seconds and is
140
- overridable via `delivery.worktreeIsolation.sweepLockMs` in
141
- `.agentrc.json`.
142
-
143
- Both layers are non-fatal — sweep failure / skip never blocks init, and
144
- the new story is always created.
83
+ the Story to `agent::executing`. It also reuses an existing `story-<id>`
84
+ branch (idempotent re-init) and runs a **merged-`story-*` sweep** between
85
+ fetch and branch-seed.
86
+
87
+ > **Lease preflight, branch reuse, and merged-sweep.** The standalone lease
88
+ > **fails closed** on a foreign assignee (there is no Epic-scoped
89
+ > heartbeat ledger to judge staleness) coordinate or pass `--steal`. The
90
+ > sweep is guarded (per-candidate protection + cross-session lock) and
91
+ > never blocks init. See
92
+ > [`single-story-deliver-reference.md` § Step 0 — Lease preflight and merged-sweep](single-story-deliver-reference.md#step-0--lease-preflight-and-merged-sweep)
93
+ > for the fail-closed outcomes, the `--steal` contract, and the sweep
94
+ > hardening layers.
145
95
 
146
96
  Capture `workCwd` from the result envelope. Add `--dry-run` to inspect
147
97
  the planned actions without git or ticket mutations (dry-run also skips
148
- the sweep).
98
+ the lease and the sweep).
149
99
 
150
100
  ### Step 0.5 — `cd` into the workCwd
151
101
 
@@ -156,18 +106,11 @@ cd "<workCwd from Step 0 result>"
156
106
  All subsequent commands run from this directory.
157
107
 
158
108
  > **Worktree scope is not just the Bash cwd.** `cd <workCwd>` steers the
159
- > **Bash** tool's working directory, but it does **not** scope the
160
- > path-based **Edit/Write/Read** tools — those resolve **absolute paths**
161
- > and ignore the shell cwd. On Windows especially, an agent whose shell
162
- > sits in the worktree can still silently edit the **main checkout** if it
163
- > resolves a main-checkout absolute path. To stay in the worktree you MUST
164
- > prefix **every Edit/Write/Read path with the absolute worktree root**
165
- > (the `workCwd` value from Step 0), not merely `cd` into it. Never edit
166
- > files under the bare main-checkout root. `single-story-close.js` runs a
167
- > **wrong-tree guard** (Story #3364) that aborts close and posts a
168
- > `friction` comment if it finds uncommitted tracked-path edits in the main
169
- > checkout while the worktree is the active work tree — but that is a
170
- > backstop, not a substitute for prefixing paths correctly.
109
+ > Bash tool's cwd but does **not** scope the path-based Edit/Write/Read
110
+ > tools — you MUST prefix every such path with the absolute `workCwd` root or
111
+ > risk silently editing the main checkout. Close's wrong-tree guard (Story
112
+ > #3364) is a backstop, not a substitute. See
113
+ > [`single-story-deliver-reference.md` § Worktree scope is not just the Bash cwd](single-story-deliver-reference.md#worktree-scope-is-not-just-the-bash-cwd).
171
114
 
172
115
  ---
173
116
 
@@ -181,6 +124,17 @@ Story body.
181
124
  Operator/agent responsibilities while in the worktree:
182
125
 
183
126
  1. Read the Story body. Treat its acceptance criteria as the contract.
127
+
128
+ **Docs context — digest-first.** A standalone Story has no parent Epic,
129
+ so there is usually no per-Epic docs digest to read. If the caller
130
+ provides a `docsDigestPath` (the per-Epic docs digest at
131
+ `temp/epic-<epicId>/docs-digest.md`), read that compact outline instead
132
+ of re-reading the full `project.docsContextFiles` set, and pull
133
+ individual docs files on demand (jump to the section at the line number
134
+ the digest names). When no digest path is provided, read a full doc
135
+ only when the Story's own context points you at one — do not ingest the
136
+ whole docs set up front. See
137
+ [`.agents/instructions.md` § 3](../../instructions.md).
184
138
  2. Implement the changes.
185
139
  3. Commit on the Story branch. Conventional-commit format is encouraged
186
140
  but not enforced — the PR title carries the canonical summary.
@@ -265,76 +219,29 @@ the worktree):
265
219
  node <main-repo>/.agents/scripts/single-story-close.js --story <storyId> --cwd <main-repo>
266
220
  ```
267
221
 
268
- The script:
269
-
270
- 1. Runs the close-validation gates against `baseBranch` as the baseline.
271
- On any gate failure it throws the operator fixes and re-runs close.
272
- 1a. **Syncs the Story branch from `origin/<baseBranch>`** before push
273
- (Story #2580). Runs `git fetch origin <baseBranch>` followed by
274
- `git merge --no-edit origin/<baseBranch>` inside the worktree. This
275
- defends against the parallel-`/single-story-deliver` race: when
276
- multiple sessions run in parallel, the Story that auto-merges first
277
- bumps `baseBranch`, and without this sync the lagging Stories open
278
- PRs that are "behind base" and stall against branch-protection's
279
- `up-to-date branch` rule. Outcomes:
280
- - **No-op / fast-forward / clean merge-commit** close proceeds to
281
- push.
282
- - **Merge conflict** the merge is aborted, a `friction` structured
283
- comment is posted on the Story (conflicting file list + recovery
284
- command set), the Story flips to `agent::blocked`, and close
285
- throws. Resolve in the worktree (`git merge origin/<base>` + fix
286
- conflicts + `git commit --no-edit`) and re-run
287
- `/single-story-deliver`.
288
- - **Fetch failed**close throws with the git stderr; no label
289
- transition.
290
-
291
- Note: the merge queue (when enabled) re-tests each PR against the
292
- queue tip before merging, so this sync + merge queue is the complete
293
- defence against the parallel race. Without merge queue, the sync
294
- closes the PR-open-time race but a residual race remains between PR
295
- open and auto-merge fire.
296
- 2. Pushes `story-<id>` to `origin`.
297
- 3. Probes for an existing open PR with `head = story-<id>`. If none
298
- exists, opens one via `gh pr create --base <baseBranch>`. The PR
299
- body carries `Closes #<storyId>` so the GitHub merge auto-closes the
300
- issue.
301
- 3a. **Enables GitHub native auto-merge by default** via
302
- `gh pr merge <prNumber> --auto --squash --delete-branch`. Once CI's
303
- required checks turn green, GitHub squash-merges the PR and deletes
304
- the source branch — the operator does not need to babysit the merge
305
- button. Mirrors the `/deliver` finalize path. Failure is
306
- non-fatal: the operator retains the manual merge surface in the
307
- GitHub UI. Pass `--no-auto-merge` to opt out when the PR needs a
308
- pre-merge eyeball.
309
- 4. Flips the Story to **`agent::closing`** (NOT `agent::done`) and leaves
310
- the GitHub issue **OPEN** (Story #3385). Auto-merge completes
311
- asynchronously *after* this script exits, so closing the issue here
312
- would strand a CLOSED issue with no merged work if the PR later failed
313
- CI, went `BEHIND` base, or was closed without merging. The Story rests
314
- at `agent::closing` while the PR is open with auto-merge armed; the
315
- `agent::done` flip (which closes the issue) is deferred to Step 5.5's
316
- `single-story-confirm-merge.js`. This brings the standalone path to
317
- parity with the epic path (#2155), where a Story only reaches
318
- `agent::done` once its merge into `epic/<id>` is confirmed.
319
- 5. Reaps the worktree when `delivery.worktreeIsolation.reapOnSuccess`
320
- is enabled.
321
- 6. **Releases the Story lease** (Story #3483). Clears the Story assignment
322
- that init claimed so the next `/single-story-deliver` run sees an
323
- unclaimed ticket. The release is a no-op when the operator no longer
324
- holds the claim (a later run took over via reclaim/steal), so a late
325
- close never yanks a live claim away from its current owner. Best-effort:
326
- a release failure is logged but does not fail an otherwise-clean close —
327
- the lease goes stale via TTL regardless. The close result carries
328
- `leaseReleased: <boolean>`.
329
-
330
- `--skip-validation` bypasses Step 1 (gates). Use only when re-running
331
- close after a fixed gate failure that's already known to pass.
332
-
333
- `--skip-sync` bypasses Step 1a (base-sync). Use only when re-running
334
- close after a hand-resolved sync, or in tests.
335
-
336
- `--no-auto-merge` disables Step 3a. Use when the PR materially changes
337
- behaviour and warrants pre-merge review.
222
+ The script runs the close-validation gates against `baseBranch`, syncs the
223
+ Story branch from `origin/<baseBranch>` (Story #2580 — the parallel-race
224
+ defence), pushes `story-<id>`, opens (or reuses) a PR against `baseBranch`
225
+ with a `Closes #<storyId>` footer, enables GitHub native auto-merge
226
+ (`--auto --squash --delete-branch`), flips the Story to **`agent::closing`**
227
+ (NOT `agent::done` the issue stays OPEN until Step 5 confirms the merge,
228
+ Story #3385), reaps the worktree, and releases the Story lease.
229
+
230
+ Flags:
231
+
232
+ - `--skip-validation` bypass the gates (Step 1). Use only when re-running
233
+ close after a fixed gate failure that's already known to pass.
234
+ - `--skip-sync` bypass the base-sync (Story #2580). Use only after a
235
+ hand-resolved sync, or in tests.
236
+ - `--no-auto-merge` disable auto-merge. Use when the PR materially changes
237
+ behaviour and warrants a pre-merge eyeball; the operator then merges via
238
+ the GitHub UI.
239
+
240
+ > **Full close pipeline (base-sync outcomes, `agent::closing` rationale,
241
+ > lease release).** For the numbered close pipeline, the base-sync outcome
242
+ > table (no-op / conflict`agent::blocked` / fetch-failed), and why the
243
+ > issue stays OPEN at `agent::closing`, see
244
+ > [`single-story-deliver-reference.md` § Step 3 — Close pipeline detail](single-story-deliver-reference.md#step-3--close-pipeline-detail).
338
245
 
339
246
  ---
340
247
 
@@ -342,31 +249,19 @@ behaviour and warrants pre-merge review.
342
249
 
343
250
  The Story is **not done** when `single-story-close.js` returns. Auto-merge
344
251
  only fires when every required CI check turns green. Local close-validation
345
- gates pass on the dev host's environment (Windows, particular Node patch,
346
- particular concurrency), but CI runs on a different OS and concurrency —
347
- coverage rounding, platform-conditional branches, and timing-sensitive
348
- tests routinely drift between the two. The agent owns the green-CI
349
- outcome, not just the push.
252
+ gates pass on the dev host's environment; CI runs on a different OS and
253
+ concurrency, and coverage rounding, platform-conditional branches, and
254
+ timing-sensitive tests routinely drift between the two. The agent owns the
255
+ green-CI outcome, not just the push.
350
256
 
351
257
  > **The auto-merge wait is an internally-blocking step, not a reason to end
352
- > your turn.** This is the single most important contract of this workflow,
353
- > and the seam where a worker most often misbehaves: it delivers up to arming
354
- > auto-merge, then ends its turn with **free-form prose** e.g. "I'll wait
355
- > for the background watch task to complete" or "the next event will be its
356
- > completion notification" leaving the merge unconfirmed and the Story
357
- > stranded at `agent::closing` (observed on Story #1553 / PR #1554). **Do not
358
- > do this.** `gh pr checks <prNumber> --watch` *blocks the current turn* until
359
- > CI resolves — that is the mechanism by which you wait. You MUST keep your
360
- > turn alive across the wait: watch → (fix + push + re-watch on red) → confirm
361
- > the merge (Step 5) → flip `agent::done` → run the post-merge steps → and
362
- > only then return the terminal JSON status contract (Step 4 of
363
- > [`deliver-stories.md` § 2c](deliver-stories.md), mirrored in
364
- > [§ Return contract](#return-contract) for the standalone caller). The CI
365
- > wait NEVER terminates your turn; **only** a confirmed-`MERGED` PR (→
366
- > `status: "done"`), an `agent::blocked` transition (→ `status: "blocked"`),
367
- > or an unrecoverable failure (→ `status: "failed"`) does. Ending your turn
368
- > with prose and an unconfirmed merge is a contract violation — it is the very
369
- > bug this workflow exists to prevent.
258
+ > your turn.** `gh pr checks <prNumber> --watch` blocks the current turn
259
+ > until CI resolves that IS how you wait. Keep the turn alive: watch →
260
+ > (fix + push + re-watch on red) confirm the merge (Step 5) flip
261
+ > `agent::done` post-merge steps return the terminal JSON contract.
262
+ > Ending the turn with prose and an unconfirmed merge is a contract
263
+ > violation (the Story #1553 / PR #1554 failure mode). See
264
+ > [`single-story-deliver-reference.md` § The auto-merge wait is an internally-blocking step](single-story-deliver-reference.md#the-auto-merge-wait-is-an-internally-blocking-step).
370
265
 
371
266
  After `single-story-close.js` succeeds, enter the watch + fix loop:
372
267
 
@@ -377,80 +272,19 @@ gh pr checks <prNumber> --watch
377
272
  When the watch exits:
378
273
 
379
274
  - **All checks ✓** — auto-merge will fire (or has already). The Story is
380
- still at `agent::closing` with its issue OPEN at this point (Step 3
381
- deferred the `agent::done` flip). The `Closes #<id>` footer closes the
382
- Story issue when the merge lands; Step 5 confirms the merge and Step 5.5
383
- flips the Story to `agent::done`. **Proceed to Step 5 within the same
384
- turn** — do not end your turn here. Green CI is the *start* of the
385
- merge-confirm sequence, not a terminal state (see Step 7's no-park rule).
275
+ still at `agent::closing` with its issue OPEN. **Proceed to Step 5 within
276
+ the same turn** green CI is the *start* of the merge-confirm sequence,
277
+ not a terminal state.
386
278
  - **Any check ✗** — diagnose, fix, and push a new commit on
387
279
  `story-<storyId>`, then re-watch. Auto-merge stays enabled across
388
280
  retries; no need to re-arm it. The Story stays at `agent::closing`
389
281
  throughout, so a failed/abandoned PR never strands a CLOSED issue.
390
282
 
391
- ### Resurrecting the worktree after `reapOnSuccess`
392
-
393
- `single-story-close.js` reaps the worktree on success when
394
- `delivery.worktreeIsolation.reapOnSuccess` is enabled (the default). To
395
- fix CI you must re-attach a worktree to the existing remote branch:
396
-
397
- ```bash
398
- cd <main-repo>
399
- git fetch origin story-<storyId>
400
- git worktree add .worktrees/story-<storyId> story-<storyId>
401
- cd .worktrees/story-<storyId>
402
- ```
403
-
404
- Do **not** re-run `single-story-init.js` — it would reset the branch
405
- state and lose the close commit's structured comment.
406
-
407
- ### Diagnosing the failure
408
-
409
- Pull the failing job log via:
410
-
411
- ```bash
412
- gh run view <runId> --repo <owner>/<repo> --log-failed
413
- ```
414
-
415
- The `<runId>` is the run number that `gh pr checks` shows in the
416
- failing row's URL. Read the bottom of the log — the gate that exited
417
- non-zero is named there (e.g. `[Coverage] ❌ REGRESSION in …`).
418
-
419
- ### Fixing without re-running close-validation
420
-
421
- For coverage / maintainability / CRAP regressions detected only on CI:
422
-
423
- 1. Update the relevant baseline file (`baselines/coverage.json`,
424
- `baselines/maintainability.json`, `baselines/crap.json`) to absorb
425
- CI's actual numbers. Edit by hand when CI's numbers are within the
426
- tolerance you'd otherwise accept — don't re-run `npm run … :update`
427
- locally, because Windows numbers will overwrite CI's Linux numbers
428
- and the cycle repeats.
429
- 2. Commit the baseline delta with a `chore(baselines):` message that
430
- names the CI run that produced the values.
431
- 3. `git push` to `origin/story-<storyId>` and re-watch.
432
-
433
- For genuine test failures (a flaky test, a platform-conditional bug):
434
- fix the code or test, commit, push, re-watch. Keep iterating until
435
- the watch exits clean.
436
-
437
- ### When to stop iterating
438
-
439
- - **Three consecutive failures with the same fix shape** — stop and
440
- Re-Plan per Anti-Thrashing Protocol. The diagnosis is likely wrong.
441
- - **Operator-blocking failure** (security scanner, branch-protection
442
- rule the agent can't change) — transition the Story to
443
- `agent::blocked`, summarize the blocker on the PR, and yield to the
444
- operator.
445
-
446
- ### Idempotence of the loop
447
-
448
- - The PR stays open across retries; `gh pr create` is a one-shot at
449
- close, the loop only pushes new commits.
450
- - Auto-merge stays armed across retries — pushing a new commit does
451
- not disarm `gh pr merge --auto`.
452
- - If the operator manually merges or disables auto-merge mid-loop,
453
- exit the loop and report.
283
+ > **CI recovery procedures.** For resurrecting the worktree after
284
+ > `reapOnSuccess`, pulling the failing job log, fixing coverage/CRAP
285
+ > baselines without re-running close-validation, and the when-to-stop
286
+ > Anti-Thrashing rules, see
287
+ > [`single-story-deliver-reference.md` § Step 4 CI watch + fix recovery](single-story-deliver-reference.md#step-4--ci-watch--fix-recovery).
454
288
 
455
289
  ---
456
290
 
@@ -467,117 +301,55 @@ gh pr view <prNumber> --json state,mergedAt,mergeCommit
467
301
  ```
468
302
 
469
303
  Expect `state: "MERGED"`. With `--no-auto-merge`, the PR is the merge
470
- gate. The operator reviews and merges via the GitHub UI; the same
304
+ gate the operator reviews and merges via the GitHub UI; the same
471
305
  `Closes #<id>` auto-close fires when the merge lands on `main`.
472
306
 
473
307
  **Then flip the Story to `agent::done`.** Step 3 deferred this flip
474
- (Story #3385) so the Story rested at `agent::closing` with its issue OPEN
475
- while the PR was open. Now that the merge is confirmed, drive the
308
+ (Story #3385); now that the merge is confirmed, drive the
476
309
  `agent::closing → agent::done` transition (which closes the issue) via:
477
310
 
478
311
  ```bash
479
312
  node .agents/scripts/single-story-confirm-merge.js --story <storyId> --cwd <main-repo>
480
313
  ```
481
314
 
482
- The confirmation script re-reads the live PR state (`gh pr view --json
483
- state,mergedAt`, probing `gh pr list --head story-<id> --state all` when
484
- `--pr` is omitted) and:
485
-
486
- - **PR `MERGED`** flips `agent::closing agent::done`, closing the
487
- issue, and fires the `story-merged` notify. Prints
488
- `{ action: 'done', merged: true, ... }`.
489
- - **PR still open / closed-without-merge** → leaves the Story at
490
- `agent::closing` (issue stays OPEN) and prints
491
- `{ action: 'pending', reason: 'pr-open' | 'pr-not-merged' | 'no-pr' }`.
492
- Re-run after the merge lands.
493
- - **Story already `agent::done` / issue already closed** → idempotent
494
- `{ action: 'noop', reason: 'already-done' }`.
495
-
496
- This is the standalone counterpart to the epic path's post-merge
497
- `agent::done` flip in `post-merge-close.js` (#2155): the issue closes
498
- exactly when the work has merged, never at PR-open.
315
+ > **Confirmation outcomes.** `single-story-confirm-merge.js` re-reads the
316
+ > live PR state and flips to `agent::done` only on a confirmed `MERGED` PR;
317
+ > it is idempotent and safe to re-run while the PR is still open (returns
318
+ > `pending`). See
319
+ > [`single-story-deliver-reference.md` § Step 5 Merge confirmation detail](single-story-deliver-reference.md#step-5--merge-confirmation-detail).
499
320
 
500
321
  ---
501
322
 
502
323
  ## Step 5.5 — Re-assert Status column (**required, not optional**)
503
324
 
504
- The GitHub Projects v2 built-in workflows `Pull request merged` and
505
- `Pull request linked to issue` are enabled by default on most boards
506
- and fire ~minutes *after* auto-merge lands. They overwrite the Status
507
- field as a side-effect, clobbering the `Done` value
508
- `single-story-confirm-merge.js` set at the `agent::done` flip in Step 5
509
- and leaving closed Stories stuck at `In Progress` on the board
510
- (reproduced on Story #2813). The confirmation step has already exited by
511
- then, so the bot gets the last write.
512
-
325
+ GitHub Projects v2 built-in workflows fire minutes *after* auto-merge lands
326
+ and clobber the `Done` Status the confirm step set, stranding closed
327
+ Stories at `In Progress` on the board (reproduced on Story #2813).
513
328
  Re-assert authority once the merge confirms:
514
329
 
515
330
  ```bash
516
331
  node .agents/scripts/resync-status-column.js --story <storyId>
517
332
  ```
518
333
 
519
- What this does:
520
-
521
- - Reads the ticket's current `agent::*` label set (now `agent::done`).
522
- - Re-fires the same `ColumnSync` mutation `transitionTicketState` used
523
- at close, overwriting the bot's late write.
524
- - **Polls the live Status for ~15 s after the initial write** and
525
- re-fires on drift (Story #2876). Without this loop, a one-shot
526
- mutation routinely lost the race against the bot's asynchronous
527
- fire (reproduced on Story #2871 / PR #2872).
528
- - Prints a single-line JSON envelope:
529
- `{ ticketId, status, column?, reason?, attempts? }`. `attempts > 1`
530
- means the helper had to fight a bot overwrite; `status: 'drifted'`
531
- means the bot won every attempt in the poll budget (rare; usually
532
- signals operator should reap the conflicting workflows).
533
-
534
- Tuning flags (rarely needed):
535
-
536
- - `--poll-attempts <n>` — total mutation attempts including the
537
- initial sync. Default `4`. Pass `1` to disable the poll loop
538
- (fastest, matches pre-#2876 behaviour).
539
- - `--poll-delay-ms <ms>` — delay between drift checks. Default `5000`.
540
-
541
- Idempotent: re-running on a ticket whose Status already matches the
542
- target returns the same envelope. No-op skips (`no-project`,
543
- `no-meta`, `not-on-project`) exit 0 with the reason in the envelope
544
- so the workflow can continue.
545
-
546
- **Canonical operator fix:** run
547
- `node .agents/scripts/agents-bootstrap-github.js --reap-conflicting-workflows`
548
- once per project to delete the conflicting bot workflows entirely.
549
- This eliminates the race source; the poll loop becomes pure
550
- defense-in-depth against re-enabled or future workflows.
551
-
552
- Skip Step 5.5 only when the operator opted out of auto-merge AND has
553
- not yet merged the PR (no `agent::done` to re-assert yet) — run it
334
+ The helper re-fires the `ColumnSync` mutation and **polls for ~15 s** to win
335
+ the race against the bot's late write (Story #2876). It is idempotent and
336
+ no-op-safe (`no-project` / `not-on-project` exit 0). Skip Step 5.5 only when
337
+ the operator opted out of auto-merge AND has not yet merged the PR run it
554
338
  after the manual merge instead.
555
339
 
340
+ > **Status-column detail + tuning flags + operator fix.** For the poll-loop
341
+ > flags (`--poll-attempts`, `--poll-delay-ms`), the `attempts` / `drifted`
342
+ > envelope semantics, and the canonical
343
+ > `--reap-conflicting-workflows` operator fix, see
344
+ > [`single-story-deliver-reference.md` § Step 5.5 — Re-assert Status column detail](single-story-deliver-reference.md#step-55--re-assert-status-column-detail).
345
+
556
346
  ---
557
347
 
558
348
  ## Step 6 — Local branch cleanup (**required, not optional**)
559
349
 
560
- GitHub deletes the **remote** branch on auto-merge (via the
561
- `--delete-branch` flag `single-story-close.js` passes to `gh pr merge`).
562
- The **local** `story-<storyId>` ref, however, lingers in the main
563
- checkout until something prunes it — `single-story-init.js` runs a
564
- merged-sweep at the start of every *subsequent* `/single-story-deliver`
565
- invocation, but that's next-run cleanup, not end-of-run cleanup. Stale
566
- local refs accumulate between sessions, clutter `git branch`, and shadow
567
- the lessons the sweep is meant to surface.
568
-
569
- **Why local `main` goes stale:** `single-story-init.js` seeds new
570
- `story-<id>` branches from the **local** `baseBranch` ref (default
571
- `main`). Auto-merge updates **`origin/main`** on GitHub; nothing in
572
- close or the old Step 6 command updated **local `main`**. The next init
573
- then forked from a tip six merges behind until you manually pulled.
574
- `single-story-init` also attempts the same fast-forward after `git fetch`
575
- when the main checkout is clean (defense in depth if Step 6 was skipped).
576
- Step 6 must still run `--fast-forward-main` so local `main` is current
577
- before the next session — init may skip when the tree is dirty or the
578
- operator is mid-checkout on another branch.
579
-
580
- After Step 5 confirms `state: "MERGED"`, prune the story ref **and**
350
+ GitHub deletes the **remote** branch on auto-merge, but the **local**
351
+ `story-<storyId>` ref lingers in the main checkout until something prunes
352
+ it. After Step 5 confirms `state: "MERGED"`, prune the story ref **and**
581
353
  fast-forward local `main` (or `project.baseBranch`):
582
354
 
583
355
  ```bash
@@ -590,29 +362,17 @@ node .agents/scripts/git-cleanup.js \
590
362
  --include "story-<storyId>"
591
363
  ```
592
364
 
593
- What this does:
594
-
595
- - **`--fast-forward-main`** fetches `origin/<baseBranch>` and
596
- `git merge --ff-only` on the main checkout when the tree is clean and
597
- the local base is strictly behind remote. Skipped when already current,
598
- dirty, or diverged (see `/git-cleanup`).
599
- - **`--branches`** reaps the merged `story-<storyId>` ref (worktree,
600
- local branch, stale `origin/` tracking ref). Does not run
601
- `--prune-remotes` or `--stashes` unless you add those flags.
602
- - **`--include "story-<storyId>"`** scopes the branch reap to this
603
- Story's ref only sibling stories in flight are untouched.
604
- - **`--execute --remote --yes`** actually deletes the local ref, prunes
605
- the matching `origin/` tracking ref, and runs non-interactively.
606
-
607
- The sweep is idempotent. It is safe to run before `state: "MERGED"`
608
- confirms (it will skip a not-yet-merged branch), and safe to re-run
609
- after a successful cleanup (it reports "no merged branches to clean
610
- up").
611
-
612
- Skip Step 6 only when the operator has explicitly opted out via
613
- `--no-auto-merge` AND has not yet merged the PR — in that case, leave
614
- the branch in place until the manual merge lands, then run the
615
- cleanup.
365
+ `--fast-forward-main` brings local `main` current (the next init seeds from
366
+ it), `--branches` + `--include` reap only this Story's ref, and
367
+ `--execute --remote --yes` run the deletes non-interactively. The sweep is
368
+ idempotent and safe to run before `MERGED` confirms. Skip Step 6 only when
369
+ the operator opted out via `--no-auto-merge` AND has not yet merged the PR —
370
+ run the cleanup after the manual merge lands.
371
+
372
+ > **Why local `main` goes stale + per-flag behaviour.** For the stale-`main`
373
+ > mechanism and the full `--fast-forward-main` / `--branches` / `--include`
374
+ > flag semantics, see
375
+ > [`single-story-deliver-reference.md` § Step 6 Local branch cleanup detail](single-story-deliver-reference.md#step-6--local-branch-cleanup-detail).
616
376
 
617
377
  ---
618
378
 
@@ -640,40 +400,17 @@ mandates; this section is its single-homed restatement for the standalone
640
400
  worker so the contract is self-contained when this workflow is the entry
641
401
  point.
642
402
 
643
- **The auto-merge wait does not produce a fourth status.** There is no
644
- "pending" or "waiting" terminal the CI/auto-merge wait is handled
645
- *internally* by blocking on `gh pr checks --watch` (Step 4) and confirming
646
- the merge (Step 5). You return **only** when you have reached a genuinely
647
- terminal state:
648
-
649
- - **`status: "done"`** the PR is confirmed `state: "MERGED"` (Step 5),
650
- the Story carries `agent::done`, and Steps 5.5 / 6 have run. `phase: "done"`,
651
- `branchDeleted: true`.
652
- - **`status: "blocked"`** — you transitioned the Story to `agent::blocked`
653
- and posted a `friction` comment (acceptance self-eval block in Step 1a, a
654
- base-sync conflict, or an operator-blocking CI failure / Anti-Thrashing
655
- stop in Step 4). `phase: "blocked"`, `blockerCommentId` set.
656
- - **`status: "failed"`** — an unrecoverable failure outside the blocked
657
- protocol. `phase` reflects where it died.
658
-
659
- A turn that ends with prose ("I'll wait for the watch task…", "the next event
660
- will be its completion notification…") and an **unconfirmed merge** is a
661
- **contract violation** (the Story #1553 / PR #1554 failure mode): the parent
662
- wave loop cannot distinguish "still working" from "done but silent", and the
663
- Story strands at `agent::closing`. If you genuinely cannot confirm the merge,
664
- that is a `blocked` or `failed` outcome with the JSON contract above — not a
665
- prose hand-off.
666
-
667
- > **Handoff discipline — report state, not process.** Populate the envelope
668
- > with essential terminal state only (mirroring the fields
669
- > `single-story-close.js` / `story-phase.js` already emit). Do not narrate the
670
- > steps you took, and do not prescribe how the next stage should work. Prose
671
- > process commentary only bloats the hydrated prompt
672
- > (`delivery.maxTokenBudget` elision). When run **interactively** (no parent
673
- > aggregator), this JSON envelope is optional — relay terminal state to the
674
- > operator in prose instead — but the **no-park rule still holds**: never end
675
- > an interactive turn with an unconfirmed merge either; block on the watch,
676
- > confirm, and report the merged outcome.
403
+ There is **no fourth "pending" status** the CI/auto-merge wait is handled
404
+ internally by blocking on `gh pr checks --watch` (Step 4) and confirming the
405
+ merge (Step 5). Return **only** on a confirmed `MERGED` PR (`status: "done"`),
406
+ an `agent::blocked` transition (`status: "blocked"`), or an unrecoverable
407
+ failure (`status: "failed"`).
408
+
409
+ > **No-park rule + per-status contract + handoff discipline.** For the full
410
+ > terminal-status contract (what each status requires), why a prose hand-off
411
+ > with an unconfirmed merge is the very bug this workflow prevents, and the
412
+ > report-state-not-process handoff discipline, see
413
+ > [`single-story-deliver-reference.md` § Step 7 Return-contract detail](single-story-deliver-reference.md#step-7--return-contract-detail).
677
414
 
678
415
  ---
679
416
 
@@ -705,9 +442,6 @@ safe.
705
442
  `workCwd` root — the `cd` alone does not scope the path-based tools (see
706
443
  Step 0.5). Editing a bare main-checkout path lands the change in the wrong
707
444
  tree; close's wrong-tree guard (Story #3364) aborts when it detects this.
708
- - **Always** pass `--cwd <main-repo>` to `single-story-close.js` when
709
- invoking from inside a worktree (worktree-local branch deletion fails
710
- when run from inside the worktree).
711
445
  - **Handoff discipline — report state, not process.** When you hand back to
712
446
  your caller (the `/deliver` aggregator or the interactive operator),
713
447
  report essential terminal state only: the Story branch, the closing commit
@@ -730,3 +464,5 @@ safe.
730
464
  - [`/deliver`](deliver-stories.md) — several standalone Stories at
731
465
  once (dependency-aware waves).
732
466
  - [`/deliver`](deliver-epic.md) — full Epic wave loop.
467
+ - [`single-story-deliver-reference.md`](single-story-deliver-reference.md) —
468
+ lease, sweep, CI-recovery, and Status-column reference detail.