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.
- package/.agents/README.md +41 -0
- package/.agents/docs/SDLC.md +4 -2
- package/.agents/docs/agentrc-reference.json +10 -2
- package/.agents/docs/execution-reference.md +52 -0
- package/.agents/instructions.md +76 -38
- package/.agents/schemas/agentrc.schema.json +31 -3
- package/.agents/schemas/qa-ledger.schema.json +2 -2
- package/.agents/scripts/epic-deliver-prepare.js +41 -1
- package/.agents/scripts/lib/config/explain.js +4 -1
- package/.agents/scripts/lib/config-settings-schema.js +25 -1
- package/.agents/scripts/lib/epic-body-sections.js +88 -0
- package/.agents/scripts/lib/findings/promote-finding.js +3 -3
- package/.agents/scripts/lib/findings/severity.js +5 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +65 -2
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +96 -11
- package/.agents/scripts/lib/orchestration/doc-reader.js +29 -0
- package/.agents/scripts/lib/orchestration/docs-digest.js +134 -0
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +15 -1
- package/.agents/scripts/lib/qa/console-allowlist.js +5 -4
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +144 -8
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +7 -5
- package/.agents/skills/core/epic-plan-consolidate/examples.md +51 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +4 -22
- package/.agents/skills/core/epic-plan-decompose-author/examples.md +47 -0
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +9 -8
- package/.agents/skills/core/epic-plan-premortem/examples.md +53 -0
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +21 -81
- package/.agents/skills/core/epic-plan-spec-author/examples.md +91 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +52 -38
- package/.agents/workflows/helpers/deliver-epic-reference.md +514 -0
- package/.agents/workflows/helpers/deliver-epic.md +164 -469
- package/.agents/workflows/helpers/epic-deliver-story.md +35 -11
- package/.agents/workflows/helpers/plan-epic-reference.md +136 -0
- package/.agents/workflows/helpers/plan-epic.md +56 -186
- package/.agents/workflows/helpers/plan-story.md +31 -61
- package/.agents/workflows/helpers/qa-run-scenario.md +194 -0
- package/.agents/workflows/helpers/scope-triage-gate.md +97 -0
- package/.agents/workflows/helpers/single-story-deliver-reference.md +423 -0
- package/.agents/workflows/helpers/single-story-deliver.md +128 -392
- package/.agents/workflows/qa-explore.md +63 -32
- package/.agents/workflows/qa-run.md +293 -130
- package/docs/CHANGELOG.md +14 -0
- package/package.json +1 -1
- 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.
|