mandrel 2.24.0 → 2.26.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 (189) hide show
  1. package/.agents/agents/acceptance-critic.md +19 -21
  2. package/.agents/agents/auditor.md +16 -19
  3. package/.agents/agents/story-worker.md +41 -54
  4. package/.agents/audit-checklists/architecture.md +5 -1
  5. package/.agents/audit-checklists/baselines.md +21 -0
  6. package/.agents/audit-checklists/quality.md +3 -0
  7. package/.agents/docs/agentrc-reference.json +2 -1
  8. package/.agents/docs/configuration.md +2 -1
  9. package/.agents/docs/quality-gates.md +80 -18
  10. package/.agents/docs/workflows.md +4 -2
  11. package/.agents/instructions.md +1 -1
  12. package/.agents/rules/ci-remediation.md +68 -3
  13. package/.agents/schemas/agentrc.schema.json +6 -1
  14. package/.agents/schemas/audit-rules.json +15 -0
  15. package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
  16. package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
  17. package/.agents/schemas/baselines/crap.schema.json +13 -1
  18. package/.agents/schemas/crap-report.schema.json +37 -0
  19. package/.agents/schemas/model-attribution.schema.json +4 -0
  20. package/.agents/scripts/acceptance-eval.js +124 -15
  21. package/.agents/scripts/audit-baselines.js +136 -0
  22. package/.agents/scripts/audit-labels-bootstrap.js +16 -95
  23. package/.agents/scripts/audit-to-stories.js +33 -3
  24. package/.agents/scripts/check-arch-cycles.js +12 -93
  25. package/.agents/scripts/check-baseline-drift.js +16 -3
  26. package/.agents/scripts/check-baselines.js +19 -3
  27. package/.agents/scripts/check-context-budget.js +95 -14
  28. package/.agents/scripts/check-cyclomatic.js +214 -0
  29. package/.agents/scripts/check-schema-references.js +392 -0
  30. package/.agents/scripts/check-test-temp-hygiene.js +121 -1
  31. package/.agents/scripts/check-workflow-timeouts.js +291 -0
  32. package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
  33. package/.agents/scripts/deliver-light.js +3 -4
  34. package/.agents/scripts/deliver-recover.js +13 -0
  35. package/.agents/scripts/diagnose-friction.js +85 -19
  36. package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
  37. package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
  38. package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
  39. package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
  40. package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
  41. package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
  42. package/.agents/scripts/lib/audit-baselines/read.js +87 -0
  43. package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
  44. package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
  45. package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
  46. package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
  47. package/.agents/scripts/lib/audit-suite/index.js +0 -5
  48. package/.agents/scripts/lib/audit-suite/selector.js +9 -62
  49. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
  50. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
  51. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
  52. package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
  53. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
  54. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
  55. package/.agents/scripts/lib/baseline-loader.js +0 -0
  56. package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
  57. package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
  58. package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
  59. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  60. package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
  61. package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
  62. package/.agents/scripts/lib/baselines/reader.js +10 -0
  63. package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
  64. package/.agents/scripts/lib/baselines/scope.js +39 -90
  65. package/.agents/scripts/lib/baselines/writer.js +16 -7
  66. package/.agents/scripts/lib/changed-files.js +8 -1
  67. package/.agents/scripts/lib/cli-args.js +115 -1
  68. package/.agents/scripts/lib/close-validation/runner.js +70 -25
  69. package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
  70. package/.agents/scripts/lib/crap-engine.js +124 -27
  71. package/.agents/scripts/lib/crap-method-identity.js +153 -0
  72. package/.agents/scripts/lib/crap-utils.js +86 -13
  73. package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
  74. package/.agents/scripts/lib/env-loader.js +46 -16
  75. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
  76. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
  77. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
  78. package/.agents/scripts/lib/findings/route-finding.js +123 -17
  79. package/.agents/scripts/lib/findings/severity.js +80 -2
  80. package/.agents/scripts/lib/git/cached-fetch.js +0 -0
  81. package/.agents/scripts/lib/git-utils.js +136 -80
  82. package/.agents/scripts/lib/import-graph.js +156 -0
  83. package/.agents/scripts/lib/label-constants.js +17 -0
  84. package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
  85. package/.agents/scripts/lib/observability/source-classifier.js +175 -2
  86. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
  87. package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
  88. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
  89. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
  90. package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
  91. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
  92. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
  94. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
  95. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
  96. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
  97. package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
  98. package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
  99. package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
  100. package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
  101. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
  102. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
  103. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
  104. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
  105. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
  106. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
  107. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
  108. package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
  109. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  110. package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
  111. package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
  112. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
  118. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
  119. package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
  120. package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
  121. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
  122. package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
  123. package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
  124. package/.agents/scripts/lib/reserved-test-ids.js +77 -0
  125. package/.agents/scripts/lib/story-adjacency.js +3 -3
  126. package/.agents/scripts/lib/story-plan.js +137 -42
  127. package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
  128. package/.agents/scripts/lib/test-runner-contract.js +134 -0
  129. package/.agents/scripts/lib/test-tiers.js +11 -2
  130. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  131. package/.agents/scripts/lib/util/concurrent-map.js +17 -0
  132. package/.agents/scripts/lib/util/parse-id-list.js +103 -0
  133. package/.agents/scripts/lib/util/poll-loop.js +18 -2
  134. package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
  135. package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
  136. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
  137. package/.agents/scripts/lib/workers/crap-worker.js +8 -13
  138. package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
  139. package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
  140. package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
  141. package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
  142. package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
  143. package/.agents/scripts/lib/worktree-manager.js +14 -0
  144. package/.agents/scripts/plan-run-epilogue.js +17 -5
  145. package/.agents/scripts/pr-watch-with-update.js +568 -213
  146. package/.agents/scripts/providers/github/tickets.js +33 -10
  147. package/.agents/scripts/provision-git-hooks.js +85 -0
  148. package/.agents/scripts/quality-preview.js +137 -28
  149. package/.agents/scripts/resolve-stories.js +4 -1
  150. package/.agents/scripts/run-coverage.js +86 -35
  151. package/.agents/scripts/run-lint.js +20 -0
  152. package/.agents/scripts/run-tests.js +36 -36
  153. package/.agents/scripts/single-story-close.js +34 -2
  154. package/.agents/scripts/single-story-confirm-merge.js +22 -6
  155. package/.agents/scripts/single-story-init.js +7 -0
  156. package/.agents/scripts/stories-wave-tick.js +308 -47
  157. package/.agents/scripts/story-plan.js +65 -9
  158. package/.agents/scripts/update-coverage-baseline.js +34 -4
  159. package/.agents/scripts/update-crap-baseline.js +42 -4
  160. package/.agents/scripts/update-duplication-baseline.js +209 -83
  161. package/.agents/scripts/update-maintainability-baseline.js +31 -4
  162. package/.agents/scripts/validate-docs-freshness.js +1 -0
  163. package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
  165. package/.agents/skills/skills.index.json +2 -2
  166. package/.agents/templates/single-story-body.md +16 -8
  167. package/.agents/workflows/audit-architecture.md +69 -13
  168. package/.agents/workflows/audit-baselines.md +289 -0
  169. package/.agents/workflows/audit-navigability.md +5 -4
  170. package/.agents/workflows/audit-quality.md +26 -0
  171. package/.agents/workflows/audit-to-stories.md +30 -4
  172. package/.agents/workflows/deliver.md +92 -97
  173. package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
  174. package/.agents/workflows/helpers/audit-lens-core.md +25 -4
  175. package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
  176. package/.agents/workflows/helpers/deliver-digest.md +41 -21
  177. package/.agents/workflows/helpers/deliver-reference.md +95 -4
  178. package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
  179. package/.agents/workflows/helpers/deliver-story.md +104 -119
  180. package/.agents/workflows/helpers/plan-reference.md +49 -5
  181. package/.agents/workflows/memory-consolidate.md +116 -0
  182. package/.agents/workflows/plan.md +62 -75
  183. package/README.md +13 -6
  184. package/docs/CHANGELOG.md +93 -0
  185. package/lib/cli/update.js +14 -11
  186. package/lib/cli/version-check.js +9 -1
  187. package/package.json +9 -4
  188. package/.agents/schemas/friction-event.schema.json +0 -56
  189. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
@@ -12,8 +12,50 @@ Reference-only detail split out of [`deliver.md`](../deliver.md) so the
12
12
  always-resident spine stays lean. Nothing here is a new MUST —
13
13
  it is the mechanics an operator consults when the matching lever is engaged.
14
14
 
15
+ ## Ranges (`4922 - 4926`) {#ranges}
16
+
17
+ A contiguous span is how an operator reads a plan run, so the dash range is a
18
+ first-class id shape rather than prose to interpret — `/deliver 4922 - 4926`
19
+ means exactly the five ids in it.
20
+
21
+ **Pass the span through; never expand it by hand.** Every id-list flag on the
22
+ delivery path takes range tokens — `resolve-stories.js --ids`,
23
+ `stories-wave-tick.js --stories` and `--dispatched`, and
24
+ `plan-run-epilogue.js --stories`. Normalize the operator's spacing away and hand
25
+ the scripts one unspaced token (`--ids 4922-4926`), mixed freely with singles
26
+ and commas (`--ids 4901,4922-4926`); overlaps dedupe. A hand-typed enumeration
27
+ is where an id gets dropped or invented, and the drop is silent.
28
+
29
+ The shared expander (`lib/util/parse-id-list.js`) refuses rather than guesses,
30
+ so a typo fails where it was typed instead of resolving the wrong set:
31
+
32
+ | Input | Outcome |
33
+ | --- | --- |
34
+ | `4922-4926`, `4922 - 4926` | Expands to the inclusive span. En and em dashes, and a `#` on either endpoint, are accepted too. |
35
+ | `4926-4922` | Refused — write it low-to-high. |
36
+ | `1-4926` | Refused — above the 50-id span cap (`MAX_RANGE_SPAN`). |
37
+ | `4922-`, `-4926`, `4922-4923-4924` | Refused as a malformed token. |
38
+
39
+ The cap is per range token, not per run: a genuine 60-Story delivery is still
40
+ expressible as two ranges, but a slipped digit cannot fan out into a live
41
+ resolution sweep of thousands of issues.
42
+
15
43
  ## Sequencing edge cases (`stories-wave-tick.js`)
16
44
 
45
+ **What "discovered, not declared" means concretely.** `resolve-stories.js` reads
46
+ the graph from live state as the union of the Story bodies' `depends_on` edges
47
+ and GitHub's native `blocked_by` edges, resolving each blocker against its real
48
+ issue state rather than against anything you hand it. That is why there is no
49
+ batch label to pass and why a blocker that landed in an unrelated run is simply
50
+ seen as done.
51
+
52
+ **Resuming an exit-4 `blocked`.** Read the friction comment with
53
+ `gh issue view <id> --comments`, and resume only once the operator has
54
+ unblocked it:
55
+ `node .agents/scripts/update-ticket-state.js --ticket <id> --state agent::ready`.
56
+ Do not poll the label yourself while waiting — the HITL pause is the operator's
57
+ turn, not a slow beat.
58
+
17
59
  Each beat re-probes live state: it re-resolves the graph, classifies **done**
18
60
  (`agent::done` or a closed issue — including foreign blockers that landed in
19
61
  another run), and derives **in-flight** from live `agent::executing` /
@@ -49,6 +91,18 @@ different operator holds, unless you pass `--steal`. Assignee-based withholding
49
91
  needs `github.operatorHandle` set (in `.agentrc.local.json`); without it the
50
92
  probe logs a warning and leans on init's lease refusal alone.
51
93
 
94
+ **Overlapping footprints are reserved across beats, not just within one.** A
95
+ Story sharing a **concrete** path with a still-implementing Story is withheld
96
+ and named in `inFlightReservation: { available, withheld: [{ id, blockedBy,
97
+ reason }], note }`, where `reason` is `in-flight-earlier-beat` or
98
+ `foreign-lease`. Like `foreignHeld` this is neither a failure nor a wedge — the
99
+ Story re-admits automatically once its blocker leaves the in-flight set — and
100
+ it exists so an unfilled slot is explained rather than mysterious. A **glob**
101
+ footprint (or the UNKNOWN sentinel for an unparseable body) reserves nothing
102
+ across beats; it still serializes its own beat. Reservation needs the in-flight
103
+ Stories' footprints, so it is a `--probe-live` capability: under `--dag` the
104
+ report is `available: false` and selection de-conflicts within the beat only.
105
+
52
106
  ## Dispatch mechanics (role-scoped by default)
53
107
 
54
108
  **A single-Story run executes inline.** Sub-agent isolation is
@@ -82,15 +136,28 @@ footprint keeps the fresh acceptance critic), and the `route::lite` label
82
136
  remains a human-visible hint only, never the control signal — a lost or
83
137
  never-written label cannot misroute delivery.
84
138
 
139
+ **Issue a beat's spawns in one turn.** A wave tick hands you a ready set, not a
140
+ queue: those Stories have no dependency edge between them (the resolver already
141
+ withheld any that do) and no shared write paths (each owns its own worktree and
142
+ branch). Dispatch them the way
143
+ [`parallel-tooling.md`](parallel-tooling.md) Rule 3 prescribes — **N `Agent`
144
+ calls issued together in a single assistant turn**, one per ready Story, not
145
+ `Agent` → wait → `Agent`. Serial dispatch is compliant with every other rule on
146
+ this page and costs the run a full Story's implementation time per sibling for
147
+ nothing; the wave aggregator is built for the parallel shape. Respect
148
+ `delivery.deliverRunner.concurrencyCap`: when the ready set exceeds it, slice
149
+ into batches of `cap` and dispatch each batch in its own turn.
150
+
85
151
  **Dispatch each `ready` Story (role-scoped by default).** When
86
152
  `delivery.routing.roleScopedAgents` is enabled (the **default**) and the host
87
153
  exposes agent dispatch, spawn each ready Story as its own
88
154
  `subagent_type: story-worker` sub-agent — it boots on the role-scoped
89
155
  [`story-worker`](../../agents/story-worker.md) context (its own system prompt, no
90
156
  `CLAUDE.md` @-closure) carrying the load-bearing delivery MUSTs standalone. The
91
- sub-agent executes [`deliver-story.md`](deliver-story.md) end to end
92
- (init → implement → acceptance self-eval → close-and-land). Thread into its
93
- prompt: `storyId`; `docsDigestPath` (the per-run docs digest, null when
157
+ sub-agent executes [`deliver-story.md`](deliver-story.md) Steps 0–2.5
158
+ (init → implement → acceptance self-eval → **push**) and stops there; **you**
159
+ own Step 3, serialized — see `/deliver` § Closing what the workers hand back.
160
+ Thread into its prompt: `storyId`; `docsDigestPath` (the per-run docs digest, null when
94
161
  `project.docsContextFiles` is unset); `checklistPath` (the footprint-matched
95
162
  write-time audit checklist, produced at dispatch, below); and the
96
163
  **change-set discipline** — the worker computes the change set once with
@@ -220,8 +287,32 @@ A slow-CI consumer can opt the close into `"async"` mode so the merge wait
220
287
  probes once for ~60s (catching an instant merge or an instantly-red required
221
288
  check) and then returns `pending` instead of burning ~5 minutes of the host
222
289
  tool slot polling a merge that lands after the wait would have expired anyway.
223
- When a worker returns that `pending` envelope, launch its `nextCommand` as a
290
+ When a close returns that `pending` envelope, launch its `nextCommand` as a
224
291
  **background** invocation (host background Bash — its completion re-invokes the
225
292
  agent) and move on to the next Story; `single-story-confirm-merge.js` is
226
293
  idempotent and owns the whole tail. Do not foreground-poll the merge. The
227
294
  default `"sync"` behaviour is unchanged.
295
+
296
+ **On a multi-Story run, pass `--merge-watch-mode async` on every close.** Close
297
+ sees one Story and cannot see run topology, so it cannot make this call for
298
+ itself — you can. Implementation runs in parallel but the close tail is
299
+ serialized one at a time, and under `sync` each of those closes holds the
300
+ foreground for its full merge wait before the next Story's close may start.
301
+ That is the run's dominant serialized cost, and it is paid per sibling:
302
+
303
+ ```bash
304
+ node <main-repo>/.agents/scripts/single-story-close.js \
305
+ --story <storyId> --cwd <main-repo> --merge-watch-mode async
306
+ ```
307
+
308
+ The flag overrides `delivery.mergeWatch.mode` for that invocation only — the
309
+ config default stays `"sync"`, which is right for the solo delivery that has no
310
+ sibling waiting behind it. It composes with `--max-wait-seconds`: pass both and
311
+ the explicit bound still wins over the async probe cap. An unrecognized value
312
+ exits non-zero before any phase runs, so a typo cannot silently drop the run
313
+ back onto synchronous waiting. Expect a `pending` envelope from each async
314
+ close — that is the designed ending here, not a failure; background its
315
+ `nextCommand` and move to the next Story's close immediately.
316
+
317
+ A one-Story run should keep the `sync` default: there is no sibling to unblock,
318
+ and the foreground wait is the cheapest path to `landed`.
@@ -27,7 +27,7 @@ caller: helpers/deliver-story.md
27
27
 
28
28
  Before any git mutation, init takes an exclusive, time-bounded **lease** on
29
29
  the Story ticket via the assignee-as-lease primitive
30
- (`lib/orchestration/ticket-lease.js`). The single assignee *is* the lease
30
+ (`lib/orchestration/ticket-lease.js`). The single assignee _is_ the lease
31
31
  owner (resolved from `github.operatorHandle`). The standalone path has no
32
32
  Epic-scoped dispatch manifest to serialise two operators driving the same
33
33
  Story, so this lease is the only guard against a concurrent
@@ -38,7 +38,7 @@ has **no Epic-scoped lifecycle ledger** to read a per-owner
38
38
  `story.heartbeat` from, so there is no live-heartbeat source to decide
39
39
  whether a foreign claim is stale. Rather than silently reclaim every
40
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:
41
+ **fails closed**: a foreign assignee is treated as a _live_ claim. Outcomes:
42
42
 
43
43
  - **Unclaimed / self-held** → init proceeds (a self-held claim is
44
44
  re-affirmed without re-writing assignees).
@@ -80,9 +80,9 @@ The sweep applies two hardening layers:
80
80
  changes.
81
81
  - `ticket-not-done` — the parent Story ticket isn't closed and
82
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.
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
86
  - **Cross-session lock.** The sweep acquires a process-scoped lockfile
87
87
  at `<tempRoot>/single-story-sweep.lock` before planning. On
88
88
  contention (another `/deliver-story` already in the sweep
@@ -114,18 +114,24 @@ not a substitute for prefixing paths correctly.
114
114
 
115
115
  ## Engine invariants and the lite route
116
116
 
117
+ **Prerequisites before Step 0.** A `type::story` issue, a clean
118
+ `gh auth status`, and `project.baseBranch` present both locally and on
119
+ `origin` — init seeds the Story branch from the base branch and probes the
120
+ remote, so a missing or unauthenticated remote surfaces as a
121
+ `remoteVerified: false` block rather than a useful error.
122
+
117
123
  The v2 engine's trait table:
118
124
 
119
- | Trait | v2 `/deliver-story` |
120
- | --- | --- |
121
- | Ticket type | `type::story` only |
122
- | Branch | `story-<id>` seeded from `project.baseBranch` (`main`) |
123
- | Merge target | `main` via PR (squash + required checks) |
124
- | Spec / slices | Folded `## Spec` + optional `## Slicing` checkpoints in-session |
125
- | Ceremony | Per-Story, routed off the derived change level via `ceremony-routing.js` |
125
+ | Trait | v2 `/deliver-story` |
126
+ | ------------- | ------------------------------------------------------------------------ |
127
+ | Ticket type | `type::story` only |
128
+ | Branch | `story-<id>` seeded from `project.baseBranch` (`main`) |
129
+ | Merge target | `main` via PR (squash + required checks) |
130
+ | Spec / slices | Folded `## Spec` + optional `## Slicing` checkpoints in-session |
131
+ | Ceremony | Per-Story, routed off the derived change level via `ceremony-routing.js` |
126
132
 
127
133
  **Ceremony-lite Stories still land through this engine unchanged.** A
128
- lite-routed Story collapses only the *advisory* plan/deliver
134
+ lite-routed Story collapses only the _advisory_ plan/deliver
129
135
  ceremony — the fresh-critic / Tech-Spec authoring a one-artifact scope does
130
136
  not earn. It does **not** get a cheaper landing: the close-validation gates
131
137
  (lint / test / format / coverage / CRAP / maintainability), the PR to `main`,
@@ -135,7 +141,7 @@ record of those non-negotiables; there is no lite-specific gate bypass.
135
141
 
136
142
  **Deliver derives the route from the Story body's shape — and the
137
143
  dispatch mode from the run.** Persist stamps a lite cohort's Stories with the
138
- `route::lite` label as a *human-visible hint only* (and ledgers the authored
144
+ `route::lite` label as a _human-visible hint only_ (and ledgers the authored
139
145
  verdict — recorded reason plus per-Story shape evidence — on the
140
146
  `story-plan-state` checkpoint); the label is never the control signal.
141
147
  `/deliver` computes the route from the fetched Story body via
@@ -197,6 +203,29 @@ round cap, proceed / redraft / block — not an independent additional pass
197
203
  over the criteria. The M4-B floor holds: one verdict per cluster, the
198
204
  cluster count owned by `acceptance-clusters.js` alone.
199
205
 
206
+ **One round = N cluster critics → ONE merged verdict → ONE gate call.** The
207
+ clusters are how a round is _authored_; they are not how it is _scored_.
208
+ Concatenate every cluster's records into a single `criteria[]` ordered by
209
+ `index` — exactly one per `acceptance[]` item, under one `storyId`,
210
+ `schemaVersion`, `round` and `commitSha` — and hand that merged file to the
211
+ gate once, with `--expected-criteria` set to the Story's `acceptance[]` count:
212
+
213
+ ```bash
214
+ node <main-repo>/.agents/scripts/acceptance-eval.js \
215
+ --story <storyId> --verdict <merged-verdict-path> \
216
+ --expected-criteria <acceptance[] count>
217
+ ```
218
+
219
+ The flag is what makes the merge enforceable: `assertCriteriaCoverage` returns
220
+ early on the `null` default, so **omitting it leaves the guard inert** and a
221
+ single cluster's verdict handed over unmerged scores a fraction of the criteria
222
+ and still reports `proceed`. A length mismatch is rejected before scoring and
223
+ consumes no round. Calling the gate once per cluster instead spends a round
224
+ _per cluster_ — a Story past the cluster ceiling would burn its whole redraft
225
+ budget on cluster arithmetic — and N concurrent calls race the Story-scoped
226
+ round ledger. Full per-round mechanics, including the parallel dispatch and the
227
+ merge shape: [`acceptance-self-eval.md`](acceptance-self-eval.md).
228
+
200
229
  **Critic evidence-share.** When the critic runs a `verify[]`
201
230
  command that is byte-identical to a close gate (`lint` / `typecheck`), it
202
231
  records the pass into the Story evidence keyspace via `--standalone` so
@@ -261,6 +290,31 @@ removed. Hard gates are untouched.
261
290
 
262
291
  ## Step 3 — Merge wait, async mode, and flags
263
292
 
293
+ **Step 3 is the orchestrator's, and it is serialized.** A dispatched
294
+ `story-worker` ends its turn at a pushed branch (spine § Step 2.5); the session
295
+ that dispatched it runs close. Two reasons, both measured rather than
296
+ theoretical:
297
+
298
+ 1. **A sub-agent cannot resume itself.** It gets no notification when a
299
+ backgrounded close finishes, so a worker that backgrounds close and ends its
300
+ turn strands the envelope in a turn nobody reads — three of five workers in
301
+ one measured wave did exactly that despite an explicit foreground-close
302
+ instruction. Moving the seam removes the failure instead of re-wording the
303
+ prohibition. The parent, by contrast, is still live and _does_ observe and
304
+ retry its own close.
305
+ 2. **Closes contend; implementation does not.** Close syncs from
306
+ `origin/<baseBranch>`, pushes, opens a PR and arms auto-merge — two of those
307
+ in flight race on the base branch, the merge queue and the shared checkout.
308
+ So implementation may fan out across the wave, but the tail runs **one Story
309
+ at a time**: a worker that hands back while another close is running waits in
310
+ the orchestrator's queue.
311
+
312
+ A worker therefore returns a hand-off report, not a terminal envelope, and that
313
+ is the expected shape — only close mints an envelope. Never answer a missing
314
+ envelope with a re-dispatch: `single-story-init.js` re-run under a live branch
315
+ is how one Story ends up with two closes. Close the pushed branch, or probe with
316
+ `deliver-recover.js` and run the one command it prints.
317
+
264
318
  **What close does internally.** The script runs the close-validation gates
265
319
  against `baseBranch`, syncs the Story branch from `origin/<baseBranch>`
266
320
  (the parallel-race defence), pushes `story-<id>`, opens (or
@@ -280,19 +334,30 @@ separate (`delivery.mergeWatch.*`):
280
334
  it when your host has no such ceiling and you want to land in one block.
281
335
  - **`maxBudgetSeconds`** (default 3600) bounds the **cumulative** wait across
282
336
  resumes, anchored at the PR's `createdAt` so resuming does not restart the
283
- clock. Exhausting *this* is the genuine give-up → `blocked`.
337
+ clock. Exhausting _this_ is the genuine give-up → `blocked`.
284
338
 
285
339
  The wait probes the checks every poll: a red required check fails fast as
286
340
  `checks-failed` instead of burning the budget, and a PR that falls behind its
287
341
  base is brought up to date within `updateAttempts` tries.
288
342
 
289
343
  **Async merge-confirm mode (`delivery.mergeWatch.mode: "async"`).** Under
290
- the default `"sync"` the merge wait runs in the foreground as
291
- described above. When a consumer's CI routinely takes longer than the host
292
- tool ceiling (~10 min) can hold a single close invocation, the foreground
293
- wait almost always expires `pending` after burning ~5 minutes of the slot
294
- so `"async"` makes that async confirm a designed mode instead of an expiry
295
- accident. In async mode the close arms auto-merge, runs one short **~60s
344
+ the default `"sync"` the merge wait runs in the foreground as described above.
345
+
346
+ **On a multi-Story run, async is the posture pass `--merge-watch-mode async`
347
+ on every close.** This is not a slow-CI opt-in. Implementation fans out, but
348
+ the close tail is serialized one Story at a time, and under `sync` each close
349
+ holds the foreground for its full merge wait before the next may start; that
350
+ is the run's dominant serialized cost, paid once per sibling. Close sees one
351
+ Story and cannot see run topology, so the orchestrator — which can — makes the
352
+ call per invocation while the config default stays `"sync"`, which is right for
353
+ a solo delivery with no sibling waiting behind it
354
+ ([`deliver-reference.md`](deliver-reference.md) § Async merge-confirm mode).
355
+ A slow-CI consumer reaches for the same mode for the separate reason that a
356
+ foreground wait longer than the host tool ceiling (~10 min) almost always
357
+ expires `pending` after burning ~5 minutes of the slot — `"async"` makes that
358
+ confirm a designed ending instead of an expiry accident.
359
+
360
+ In async mode the close arms auto-merge, runs one short **~60s
296
361
  probe window** (long enough to catch an instant merge and, via the
297
362
  head-anchored required-check predicate, an instantly-red required check),
298
363
  then returns the standard `pending` terminal with a `nextCommand`. When you
@@ -312,7 +377,7 @@ checks pass. Under `"strict"`, the close **does not arm auto-merge** — the
312
377
  PR opens and waits for an **operator merge**, exactly as `--no-auto-merge`
313
378
  does per-run.
314
379
 
315
- **When to reach for a close flag.** What each one *does* is in
380
+ **When to reach for a close flag.** What each one _does_ is in
316
381
  `node .agents/scripts/single-story-close.js --help`; below is only the
317
382
  judgment that help text cannot carry.
318
383
 
@@ -331,6 +396,13 @@ judgment that help text cannot carry.
331
396
  - `--max-wait-seconds <n>` — from a headless caller with no host
332
397
  tool-invocation ceiling, to keep single-block semantics
333
398
  without editing the consumer's config.
399
+ - `--merge-watch-mode <sync|async>` — the per-invocation override of
400
+ `delivery.mergeWatch.mode`. **Pass `async` on every close of a multi-Story
401
+ run** (above); leave it off for a solo delivery. It composes with
402
+ `--max-wait-seconds` — pass both and the explicit bound still wins over the
403
+ async probe cap. An unrecognized value is refused before any phase runs, so a
404
+ typo cannot silently drop the run back onto synchronous waiting; the refusal
405
+ reports a `failed` terminal envelope at `phase: init`, mutating nothing.
334
406
 
335
407
  ---
336
408
 
@@ -346,7 +418,7 @@ The `single-story-close.js` script, in order:
346
418
  captured tail inline so the evidence is in front of you without opening a
347
419
  file. Read the artifact when you need the full text — or re-run under
348
420
  `AGENT_LOG_LEVEL=verbose` for live streaming.
349
- 1a. **Syncs the Story branch from `origin/<baseBranch>`** before push.
421
+ 1a. **Syncs the Story branch from `origin/<baseBranch>`** before push.
350
422
  Runs `git fetch origin <baseBranch>` followed by
351
423
  `git merge --no-edit origin/<baseBranch>` inside the worktree. This
352
424
  defends against the parallel-`/deliver-story` race: when
@@ -370,12 +442,13 @@ The `single-story-close.js` script, in order:
370
442
  defence against the parallel race. Without merge queue, the sync
371
443
  closes the PR-open-time race but a residual race remains between PR
372
444
  open and auto-merge fire.
445
+
373
446
  2. Pushes `story-<id>` to `origin`.
374
447
  3. Probes for an existing open PR with `head = story-<id>`. If none
375
448
  exists, opens one via `gh pr create --base <baseBranch>`. The PR
376
449
  body carries `Closes #<storyId>` so the GitHub merge auto-closes the
377
450
  issue.
378
- 3a. **Enables GitHub native auto-merge by default** via
451
+ 3a. **Enables GitHub native auto-merge by default** via
379
452
  `gh pr merge <prNumber> --auto --squash --delete-branch`. Once CI's
380
453
  required checks turn green, GitHub squash-merges the PR and deletes
381
454
  the source branch — the operator does not need to babysit the merge
@@ -385,7 +458,7 @@ The `single-story-close.js` script, in order:
385
458
  pre-merge eyeball.
386
459
  4. Flips the Story to **`agent::closing`** (NOT `agent::done`) and leaves
387
460
  the GitHub issue **OPEN**. Auto-merge completes
388
- asynchronously *after* this script exits, so closing the issue here
461
+ asynchronously _after_ this script exits, so closing the issue here
389
462
  would strand a CLOSED issue with no merged work if the PR later failed
390
463
  CI, went `BEHIND` base, or was closed without merging. The Story rests
391
464
  at `agent::closing` while the PR is open with auto-merge armed; the
@@ -430,9 +503,10 @@ close-validation gates pass on the dev host's environment; CI runs on a
430
503
  different OS and concurrency, and coverage rounding, platform-conditional
431
504
  branches, and timing-sensitive tests routinely drift between the two.
432
505
 
433
- Fix the failure and push a new commit on `story-<storyId>` — auto-merge stays
434
- armed across retries, so you do not re-arm — then resume the land with the
435
- envelope's `nextCommand`.
506
+ Fix the failure and push a new commit on `story-<storyId>` — the watcher
507
+ **disarmed native auto-merge on the first red** and re-arms it
508
+ only when the checks go green on a **new head SHA**, so the fix must be a real
509
+ commit — then resume the land with the envelope's `nextCommand`.
436
510
 
437
511
  To watch the checks on the red path, drive `pr-watch-with-update.js` — the
438
512
  **single CI-watch mechanism**. It polls the required checks to a
@@ -444,29 +518,54 @@ node <agentRoot>/scripts/pr-watch-with-update.js --pr <prNumber> --story <storyI
444
518
  ```
445
519
 
446
520
  `--story` is what keys the red-path CI digest
447
- (`temp/story-<id>-ci-digest.{json,md}` — failing check name, run id, and a
448
- `gh run view --log-failed` tail). Omit it and a red check writes no digest.
521
+ (`temp/story-<id>-ci-digest.{json,md}` — failing check name, the PR head SHA,
522
+ run id + run link, and a `gh run view --log-failed` tail). Omit it and a red
523
+ check writes no digest — and with no digest the no-rerun guard has nothing to
524
+ adjudicate the next green against, so always pass it.
449
525
  Poll cadence and caps come from `delivery.ci.watch.*` (`pollIntervalMs`,
450
- `maxPolls`, `maxResumes`); pass `--poll-interval-ms`, `--max-polls`, or
451
- `--max-resumes` to override for one run.
526
+ `maxPolls`, `maxResumes`, `attachWindowMs`); pass `--poll-interval-ms`,
527
+ `--max-polls`, `--max-resumes`, or `--attach-window-ms` to override for one run.
528
+ `attachWindowMs` (default 20 min) is how long the watch keeps re-resolving an
529
+ **empty** required-check set before it stops waiting for a context to attach —
530
+ a required context that is an aggregator job gated on every other tier is the
531
+ last check to appear, measured at 16m52s on this repository.
532
+
533
+ Add `--repo owner/repo` only when the cwd is not the target repository; it
534
+ reaches `gh` as a real flag. There is no `<owner/repo>#<number>` ref form —
535
+ `gh` parses that as a branch name.
452
536
 
453
537
  When the watch exits, branch on the exit code:
454
538
 
455
539
  - **Exit 0 (all checks ✓)** — auto-merge will fire (or has already). The Story
456
540
  is still at `agent::closing` with its issue OPEN. **Proceed to merge
457
- confirmation (§ Step 5) within the same turn** — green CI is the *start* of
541
+ confirmation (§ Step 5) within the same turn** — green CI is the _start_ of
458
542
  the merge-confirm sequence, not a terminal state.
459
- - **Exit 1 (a check genuinely failed)** — diagnose, fix, and push a new commit
460
- on `story-<storyId>`, then re-watch. Auto-merge stays enabled across retries;
461
- no need to re-arm it. The Story stays at `agent::closing` throughout, so a
462
- failed/abandoned PR never strands a CLOSED issue. If the same failure class
463
- recurs, hand convergence off to a self-paced host loop (`/loop`) that re-runs
464
- the failing check and applies the smallest fix until it exits green.
465
- - **Exit 2 (still-running — slow CI, not red)** the poll cap fired with checks
466
- still pending and the watcher exhausted its resume budget with nothing red.
467
- This is **never** a failure. Hand the wait off to the host's interval loop
543
+ - **Exit 1 (a check genuinely failed, the green was a forbidden re-run, or the
544
+ PR itself could not be read)**
545
+ diagnose, fix at source, and push a new commit on `story-<storyId>`, then
546
+ re-watch: the watcher disarmed auto-merge on the red and re-arms it only for
547
+ a green on a **new head SHA**. The Story stays at `agent::closing`
548
+ throughout, so a failed/abandoned PR never strands a CLOSED issue. If the
549
+ same failure class recurs, hand convergence off to a self-paced host loop
550
+ (`/loop`) that applies the smallest fix and pushes a new commit each pass —
551
+ **never** a bare re-run of the failed job. A green the guard rejects as a
552
+ re-run of the same commit flips the Story to `agent::blocked` with a
553
+ `friction` comment; clear it per
554
+ [`ci-remediation.md`](../../rules/ci-remediation.md) § Verifier.
555
+ - **Exit 2 (slow, not red)** — one of three slow conditions, **never** a
556
+ failure and never a green. Hand the wait off to the host's interval loop
468
557
  rather than ending your turn: `/loop 5m` polling `gh pr checks` until the
469
- checks settle.
558
+ checks settle. The envelope names which:
559
+ - **still-running** — the poll cap fired with checks still pending and the
560
+ watcher exhausted its resume budget with nothing red.
561
+ - **not-yet-started** (`notYetStarted: true`) — the attach window was spent
562
+ and **no** required context ever attached, while the PR kept reading back
563
+ fine. CI has not started; there is no failing check and no CI digest to
564
+ read. Do **not** treat it as red — nothing needs fixing, and re-watching
565
+ (or raising `attachWindowMs`) is the whole remediation.
566
+ - **unresolved** (`reconciliation.reconciled: false`) — every observed
567
+ required check is green but the repository still refuses the merge, so the
568
+ green verdict is withheld.
470
569
 
471
570
  **Triage authority.** How to classify and remediate a red (or repeatedly slow)
472
571
  check — the root-cause-only decision tree for infra/transient and flaky failures
@@ -485,7 +584,7 @@ then ends its turn with **free-form prose** — e.g. "I'll wait for the
485
584
  background watch task to complete" or "the next event will be its completion
486
585
  notification" — leaving the merge unconfirmed and the Story stranded at
487
586
  `agent::closing`. **Do not do this.**
488
- `pr-watch-with-update.js --pr <prNumber>` *blocks the current turn* until CI
587
+ `pr-watch-with-update.js --pr <prNumber>` _blocks the current turn_ until CI
489
588
  resolves — that is the mechanism by which you wait. You MUST keep your turn alive
490
589
  across the wait: watch → (fix + push + re-watch on red) → confirm the merge
491
590
  (Step 5) → flip `agent::done` → run the post-merge steps → and only then
@@ -551,12 +650,13 @@ the watch exits clean.
551
650
  `agent::blocked`, summarize the blocker on the PR, and yield to the
552
651
  operator.
553
652
 
554
- ### Idempotence of the loop
653
+ ### Idempotence of the loop {#idempotence}
555
654
 
556
655
  - The PR stays open across retries; `gh pr create` is a one-shot at
557
656
  close, the loop only pushes new commits.
558
- - Auto-merge stays armed across retries pushing a new commit does
559
- not disarm `gh pr merge --auto`.
657
+ - Auto-merge is disarmed by the watcher on the first red and re-armed
658
+ when the checks go green on a new head SHA; pushing a new commit is
659
+ what re-opens the merge path.
560
660
  - If the operator manually merges or disables auto-merge mid-loop,
561
661
  exit the loop and report.
562
662
 
@@ -612,7 +712,7 @@ no-op-safe (`no-project` / `not-on-project` exit 0).
612
712
 
613
713
  The GitHub Projects v2 built-in workflows `Pull request merged` and
614
714
  `Pull request linked to issue` are enabled by default on most boards
615
- and fire ~minutes *after* auto-merge lands. They overwrite the Status
715
+ and fire ~minutes _after_ auto-merge lands. They overwrite the Status
616
716
  field as a side-effect, clobbering the `Done` value
617
717
  `single-story-confirm-merge.js` set at the `agent::done` flip in Step 5
618
718
  and leaving closed Stories stuck at `In Progress` on the board. The
@@ -680,7 +780,7 @@ GitHub deletes the **remote** branch on auto-merge (via the
680
780
  `--delete-branch` flag `single-story-close.js` passes to `gh pr merge`).
681
781
  The **local** `story-<storyId>` ref, however, lingers in the main
682
782
  checkout until something prunes it — `single-story-init.js` runs a
683
- merged-sweep at the start of every *subsequent* `/deliver-story`
783
+ merged-sweep at the start of every _subsequent_ `/deliver-story`
684
784
  invocation, but that's next-run cleanup, not end-of-run cleanup. Stale
685
785
  local refs accumulate between sessions, clutter `git branch`, and shadow
686
786
  the lessons the sweep is meant to surface.
@@ -717,6 +817,27 @@ up").
717
817
 
718
818
  ---
719
819
 
820
+ ## Idempotence and the standing constraints
821
+
822
+ Every script in the chain no-ops safely on re-run: `single-story-init.js`
823
+ re-prints `workCwd` for an already-initialized Story; `single-story-close.js`
824
+ and `single-story-confirm-merge.js` short-circuit on a closed or `agent::done`
825
+ Story; the PR probe reuses an open PR rather than opening a second one. That is
826
+ what makes the recovery router safe to walk more than once.
827
+
828
+ The four constraints the spine states without arguing for them:
829
+
830
+ - **Never push the Story branch directly to `main`.** The PR is the only merge
831
+ surface — a direct push bypasses required checks and the squash title
832
+ release-please parses.
833
+ - **Always prefix path-based tools with the absolute `workCwd` root.** `cd`
834
+ scopes Bash, not Edit/Write/Read; close's wrong-tree guard is a backstop for
835
+ the mistake, not a licence to make it.
836
+ - **Report state, not process.** Mirror the close envelope's fields; step
837
+ narration reads as progress while telling the caller nothing it can branch on.
838
+ - **Drive every `agent::*` transition through `update-ticket-state.js`** so the
839
+ label, the Projects Status column and the lifecycle event stay in one motion.
840
+
720
841
  ## Step 7 — Return-contract detail
721
842
 
722
843
  The field-level contract is the shipped schema
@@ -724,7 +845,7 @@ The field-level contract is the shipped schema
724
845
  — not this file, and not
725
846
  [`agents/story-worker.md`](../../agents/story-worker.md). All three used to
726
847
  carry their own prose version; the schema is now the only definition. What
727
- follows is the *judgement* around it, which a schema cannot express.
848
+ follows is the _judgement_ around it, which a schema cannot express.
728
849
 
729
850
  ### `pending` is a real status — and it is not a park
730
851
 
@@ -767,8 +888,9 @@ persists the validated envelope to
767
888
  - **It is best-effort.** A failed write returns null and changes nothing about
768
889
  the emitted envelope or the exit code — a landed PR must never become a
769
890
  crash because a temp directory was unwritable.
770
- - **It is a fallback, not a licence.** A worker still holds its turn until the
771
- envelope arrives; see [`agents/story-worker.md`](../../agents/story-worker.md).
891
+ - **It is a fallback, not a licence.** The orchestrator running close still
892
+ holds its turn until the envelope arrives; see § Step 3 above and
893
+ [`agents/story-worker.md`](../../agents/story-worker.md).
772
894
 
773
895
  `deliver-recover.js` reads the same artifact, plus the freshness of
774
896
  `close-gates-<storyId>.log`, to split the one genuinely ambiguous row of its