mandrel 1.85.0 โ†’ 1.87.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 (41) hide show
  1. package/.agents/instructions.md +7 -0
  2. package/.agents/rules/git-conventions.md +45 -0
  3. package/.agents/scripts/boot-sweep.js +215 -0
  4. package/.agents/scripts/epic-deliver-prepare.js +55 -0
  5. package/.agents/scripts/git-cleanup.js +8 -0
  6. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +107 -30
  7. package/.agents/scripts/lib/epic-plan-ideation.js +24 -3
  8. package/.agents/scripts/lib/framework-version.js +210 -0
  9. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +7 -22
  10. package/.agents/scripts/lib/orchestration/epic-cleanup.js +330 -6
  11. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +34 -3
  12. package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +102 -7
  13. package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes-ff.js +83 -30
  14. package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +85 -1
  15. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +34 -3
  16. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +71 -4
  17. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +8 -3
  18. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +54 -6
  19. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +35 -4
  20. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +75 -0
  21. package/.agents/scripts/lib/single-story-sweep.js +239 -57
  22. package/.agents/scripts/lib/story-body/story-body.js +81 -4
  23. package/.agents/scripts/providers/github/tickets.js +18 -1
  24. package/.agents/scripts/single-story-init.js +7 -51
  25. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +7 -2
  26. package/.agents/skills/core/epic-plan-premortem/SKILL.md +8 -2
  27. package/.agents/skills/skills.index.json +3 -3
  28. package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +36 -8
  29. package/.agents/workflows/git-cleanup.md +72 -18
  30. package/.agents/workflows/git-deliver.md +36 -0
  31. package/.agents/workflows/helpers/acceptance-self-eval.md +23 -1
  32. package/.agents/workflows/helpers/deliver-epic-reference.md +19 -13
  33. package/.agents/workflows/helpers/deliver-epic.md +47 -3
  34. package/.agents/workflows/helpers/deliver-stories.md +16 -3
  35. package/.agents/workflows/helpers/epic-audit.md +60 -2
  36. package/.agents/workflows/helpers/parallel-tooling.md +9 -2
  37. package/.agents/workflows/helpers/plan-epic.md +32 -14
  38. package/.agents/workflows/loops/nightly-audit.md +9 -1
  39. package/.agents/workflows/plan.md +32 -4
  40. package/docs/CHANGELOG.md +27 -0
  41. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { parseBlockedBy, parseBlocks } from '../../lib/dependency-parser.js';
25
+ import { stampFrameworkVersion } from '../../lib/framework-version.js';
25
26
  import { Logger } from '../../lib/Logger.js';
26
27
  import { TYPE_LABELS } from '../../lib/label-constants.js';
27
28
  import { addIssueToBoard } from './board-add.js';
@@ -56,11 +57,20 @@ const SEARCH_PAGE_CAP = 10;
56
57
  * arrays on the Story body authored by the decomposer; there is no
57
58
  * server-side rendering of a four-section payload at create time.
58
59
  *
60
+ * Story #4382 โ€” this is also where a Story body is stamped, once, with the
61
+ * running Mandrel framework version and authoring date (hidden `mandrel_version`
62
+ * / `authored_at` meta field + a visible `> ๐Ÿท๏ธ Authored with Mandrel โ€ฆ`
63
+ * marker) via {@link stampFrameworkVersion}. The stamp is immutable: a body
64
+ * that already carries a version (e.g. a reconciler re-create) is preserved
65
+ * verbatim. The `stamp` override exists for deterministic tests; production
66
+ * callers omit it so the running version and today's date are used.
67
+ *
59
68
  * @param {{
60
69
  * body: string,
61
70
  * parentId: number,
62
71
  * epicId?: number,
63
72
  * dependencies?: number[],
73
+ * stamp?: { version?: string, authoredAt?: string } | false,
64
74
  * }} opts
65
75
  * @returns {string}
66
76
  *
@@ -75,8 +85,15 @@ export function composeStoryBody({
75
85
  parentId,
76
86
  epicId,
77
87
  dependencies = [],
88
+ stamp,
78
89
  }) {
79
- const head = typeof body === 'string' ? body : '';
90
+ const rawHead = typeof body === 'string' ? body : '';
91
+ // `stamp === false` โ†’ footer-only recomposition: the caller (the reconciler
92
+ // UPDATE/diff path) owns stamp preservation itself and must NOT introduce a
93
+ // fresh authoring stamp, which would churn or bump the version on every
94
+ // reconcile. Every other call is a create โ€” stamp once (immutably).
95
+ const head =
96
+ stamp === false ? rawHead : stampFrameworkVersion(rawHead, stamp ?? {});
80
97
  const lines = ['---', `parent: #${parentId}`];
81
98
  if (epicId !== undefined && epicId !== null) {
82
99
  lines.push(`Epic: #${epicId}`);
@@ -35,7 +35,6 @@
35
35
  * @see .agents/workflows/helpers/single-story-deliver.md
36
36
  */
37
37
 
38
- import { spawnSync as defaultSpawnSync } from 'node:child_process';
39
38
  import { existsSync } from 'node:fs';
40
39
  import path from 'node:path';
41
40
  import { parseSprintArgs } from './lib/cli-args.js';
@@ -67,12 +66,18 @@ import {
67
66
  upsertStructuredComment,
68
67
  } from './lib/orchestration/ticketing.js';
69
68
  import { createProvider } from './lib/provider-factory.js';
69
+ import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
70
70
  // `sweepMergedStoryBranches` is imported dynamically below โ€” its transitive
71
71
  // graph reaches `picomatch` (via `git-cleanup.js`). Loading it statically
72
72
  // would crash module resolution before `assertDepsInstalled()` can emit a
73
73
  // friendly "run npm install" message.
74
74
  import { WorktreeManager } from './lib/worktree-manager.js';
75
75
 
76
+ // `makeGhRunner` moved to the shared `single-story-sweep/protection-ctx.js`
77
+ // module (Story #4373) so the three boot callers build an identical
78
+ // protection ctx. Re-exported here to preserve its existing import path.
79
+ export { makeGhRunner } from './lib/single-story-sweep/protection-ctx.js';
80
+
76
81
  /**
77
82
  * Fail fast with a clear, actionable message when project deps are missing.
78
83
  * Uses only Node builtins so it stays loadable when `node_modules/` is empty.
@@ -97,50 +102,6 @@ function assertDepsInstalled(projectRoot) {
97
102
 
98
103
  const progress = Logger.createProgress('single-story-init', { stderr: true });
99
104
 
100
- /**
101
- * Build the synchronous `gh` runner the single-story sweep uses for its
102
- * candidate-protection checks. Exported for testing.
103
- *
104
- * Story #2990: the sweep protection-ctx ghRunner stays on raw
105
- * `spawnSync('gh', โ€ฆ)` (not the `lib/gh-exec.js` async facade) because
106
- * `executeCleanup` invokes the protection checks inside a synchronous
107
- * candidate-filter loop. The runner contract is the legacy
108
- * `(args, opts) => stdout string` shape; converting it to async would
109
- * ripple into the single-story-sweep planner, which is intentionally out
110
- * of scope for the callers-only provider migration.
111
- *
112
- * Story #4073: the `spawnImpl` seam injects the `spawnSync` boundary so the
113
- * runner's success/error handling can be unit-tested without a live `gh`
114
- * binary. It defaults to `child_process.spawnSync` (mirroring the
115
- * `spawnImpl` seam in `lib/gh-exec.js` and the `runner` seam in
116
- * `lib/bootstrap/gh-preflight.js`), so the production CLI path is unchanged.
117
- * The synchronous `spawnSync` shape is preserved deliberately โ€” the
118
- * candidate-filter loop in `executeCleanup` is synchronous, so converting
119
- * this to the async `lib/gh-exec.js` facade would ripple into the
120
- * single-story-sweep planner.
121
- *
122
- * @param {string} cwd Repo root used as the default spawn cwd.
123
- * @param {typeof defaultSpawnSync} [spawnImpl] Injectable spawn boundary โ€”
124
- * defaults to `child_process.spawnSync`. Tests pass a fake to assert the
125
- * error/exit-code handling without spawning a real child process.
126
- * @returns {(args: string[], opts?: { cwd?: string }) => string}
127
- */
128
- export function makeGhRunner(cwd, spawnImpl = defaultSpawnSync) {
129
- return (args, opts) => {
130
- const result = spawnImpl('gh', args, {
131
- cwd: opts?.cwd ?? cwd,
132
- encoding: 'utf-8',
133
- shell: false,
134
- });
135
- if (result.status !== 0) {
136
- throw new Error(
137
- `gh ${args.join(' ')} exit ${result.status}: ${result.stderr ?? ''}`,
138
- );
139
- }
140
- return result.stdout ?? '';
141
- };
142
- }
143
-
144
105
  /**
145
106
  * Validate that the fetched ticket is a standalone Story this script can
146
107
  * deliver. Throws with the canonical operator-facing message otherwise.
@@ -222,12 +183,7 @@ export async function reapMergedStoryBranches({
222
183
  info: (m) => progress('CLEANUP', m),
223
184
  warn: (m) => progress('CLEANUP', `โš ๏ธ ${m}`),
224
185
  },
225
- protectionCtx: {
226
- repoRoot: cwd,
227
- gitSpawn,
228
- ghRunner: makeGhRunner(cwd),
229
- getTicket: (id) => provider.getTicket(id),
230
- },
186
+ protectionCtx: buildProtectionCtx({ cwd, provider }),
231
187
  lockPath,
232
188
  lockTimeoutMs,
233
189
  });
@@ -32,6 +32,11 @@ allowed_tools:
32
32
  Senior Project Manager + Orchestrator, acting as a **holistic critic** with
33
33
  fresh context โ€” deliberately *separate* from `epic-plan-decompose-author` (the
34
34
  generator) so the pass is a fresh-context review, not a same-pass self-critique.
35
+ The `/plan` workflow delivers that fresh context by **dispatching this skill
36
+ inside a genuine sub-agent** (`Agent` tool, `subagent_type: general-purpose`) at
37
+ Phase 8.3, rather than activating it inline in the authoring turn โ€” the
38
+ sub-agent does not inherit the conversation that authored the draft, so the
39
+ critic cannot grade its own homework.
35
40
 
36
41
  > **Read [`examples.md`](./examples.md) on demand** for the extended rationale:
37
42
  > why this critic runs with fresh context, why scope conservation is your
@@ -50,8 +55,8 @@ emit a plan the validator would reject.
50
55
 
51
56
  ## Inputs
52
57
 
53
- The dispatcher passes the Epic ID as the Skill argument. The Skill itself
54
- reads:
58
+ The `/plan` workflow dispatches this skill inside a fresh-context sub-agent,
59
+ passing the Epic ID as the Skill argument. The Skill itself reads:
55
60
 
56
61
  - `temp/epic-<Epic_ID>/tickets.json` โ€” the **draft** Story array the
57
62
  `epic-plan-decompose-author` Skill wrote. This is the consolidation input.
@@ -31,7 +31,12 @@ allowed_tools:
31
31
  Senior Engineer + Architect, acting as a **fresh-context pre-mortem critic** โ€”
32
32
  deliberately *separate* from `epic-plan-decompose-author` (the generator) and
33
33
  `epic-plan-consolidate` (the scope-preserving merge critic) so it is a
34
- fresh-context, code-reading review, not a same-pass self-critique.
34
+ fresh-context, code-reading review, not a same-pass self-critique. The `/plan`
35
+ workflow delivers that fresh context by **dispatching this skill inside a
36
+ genuine sub-agent** (`Agent` tool, `subagent_type: general-purpose`) at Phase
37
+ 8.5, rather than activating it inline in the authoring turn โ€” the sub-agent does
38
+ not inherit the authoring conversation, so its code-reading review is
39
+ independent of the draft it grades.
35
40
 
36
41
  > **Read [`examples.md`](./examples.md) on demand** for the extended rationale:
37
42
  > why this critic opens the actual cited code, why it is additive-recommendation
@@ -52,7 +57,8 @@ surfaces reaches GitHub unreviewed.
52
57
 
53
58
  ## Inputs
54
59
 
55
- The workflow passes the Epic ID as the Skill argument. The Skill itself reads:
60
+ The `/plan` workflow dispatches this skill inside a fresh-context sub-agent,
61
+ passing the Epic ID as the Skill argument. The Skill itself reads:
56
62
 
57
63
  - `temp/epic-<Epic_ID>/tickets.json` โ€” the **draft** (or consolidated) Story
58
64
  array. This is the pre-mortem subject.
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-07-04T23:12:38.376Z",
2
+ "generatedAt": "2026-07-08T12:24:41.101Z",
3
3
  "generator": "generate-skills-index.js@1",
4
4
  "skills": [
5
5
  {
@@ -377,8 +377,8 @@
377
377
  "tier": "stack",
378
378
  "category": "architecture",
379
379
  "path": ".agents/skills/stack/architecture/subagent-orchestration/SKILL.md",
380
- "description": "Coordinates complex tasks via task-isolated subagents. Use when one objective is too large for a single agent or when independent work streams should run concurrently with minimal context bleed. One objective per subagent; summarize before returning to keep the main context window clean.",
381
- "policyCapsuleBullets": 7,
380
+ "description": "Coordinates complex tasks via task-isolated subagents. Use when one objective is too large for a single agent or when independent work streams should run concurrently with minimal context bleed. One objective per subagent; summarize before returning to keep the orchestrator's context window clean. Applies recursively โ€” an orchestrator at any supported nesting depth applies the same policy to its own children.",
381
+ "policyCapsuleBullets": 8,
382
382
  "allowedTools": null,
383
383
  "vendor": null
384
384
  },
@@ -4,11 +4,33 @@ description:
4
4
  Coordinates complex tasks via task-isolated subagents. Use when one objective
5
5
  is too large for a single agent or when independent work streams should run
6
6
  concurrently with minimal context bleed. One objective per subagent;
7
- summarize before returning to keep the main context window clean.
7
+ summarize before returning to keep the orchestrator's context window clean.
8
+ Applies recursively โ€” an orchestrator at any supported nesting depth applies
9
+ the same policy to its own children.
8
10
  ---
9
11
 
10
12
  # Skill: Subagent Orchestration
11
13
 
14
+ ## Recursive orchestration model
15
+
16
+ This skill describes **recursive orchestration**, not a fixed two-tier
17
+ "main agent vs. subagents" split. An **orchestrator** is any agent that
18
+ dispatches sub-agents; a sub-agent is itself an orchestrator over its own
19
+ children. The Claude Code harness carries the `Agent` tool into sub-agents
20
+ (verified nesting depth 2, announced max depth 5; see
21
+ [#2870](https://github.com/dsj1984/mandrel/issues/2870)), so the same
22
+ one-objective / verify / parallelize policy applies **at every level** โ€”
23
+ substitute "orchestrator" for "main agent" and "child" for "subagent"
24
+ throughout and the rules hold unchanged. Keeping a given dispatch level
25
+ flat remains a legitimate **design choice** (e.g. the `/deliver` wave
26
+ loop), but it is no longer forced by a harness limitation.
27
+
28
+ The cost caution compounds with depth: every nesting level re-pays the
29
+ full always-loaded context, so an orchestrator MUST weigh the depth it
30
+ opens against its budget (see
31
+ [`instructions.md` ยง 4](../../../../instructions.md)) and stay within the
32
+ supported depth envelope.
33
+
12
34
  ## Policy Capsule
13
35
 
14
36
  - Dispatch one objective per subagent; never bundle unrelated goals into a single delegation.
@@ -16,11 +38,13 @@ description:
16
38
  - Specify the expected return format explicitly (JSON summary, diff, bullet list) in every handoff.
17
39
  - Verify the subagent's output before incorporating it; treat returned artifacts as untrusted until checked.
18
40
  - Run non-dependent subagents in parallel; serialize only when one subagent's output is required input for another.
19
- - Require a concise summary back from each subagent to keep the main context window clean.
41
+ - Require a concise summary back from each subagent to keep the orchestrator's context window clean.
20
42
  - Investigate subagent failures rather than retrying blindly with the same prompt.
43
+ - Respect the nesting depth budget; each level opened re-pays the always-loaded context, so orchestrate deeper only when the isolation or parallelism gain justifies the cost.
21
44
 
22
45
  Internal protocol for managing complex tasks through the creation and
23
- coordination of subagents.
46
+ coordination of subagents, applied recursively by the orchestrator at any
47
+ supported depth.
24
48
 
25
49
  ## 1. Core Principles
26
50
 
@@ -28,8 +52,11 @@ coordination of subagents.
28
52
  with multiple unrelated tasks.
29
53
  - **Minimal Context:** Provide only the necessary context (files, docs, specific
30
54
  goal) to keep the subagent focused and token-efficient.
31
- - **Verification:** The main agent must always verify the subagent's output
32
- before incorporating it into the final solution.
55
+ - **Verification:** The orchestrator must always verify each child's output
56
+ before incorporating it into its own result โ€” at every level of the tree.
57
+ - **Depth Awareness:** Orchestration is recursive; before opening a deeper
58
+ level, confirm the work justifies re-paying the always-loaded context and
59
+ that the nesting stays within the supported depth envelope.
33
60
 
34
61
  ## 2. Operation Standards
35
62
 
@@ -38,11 +65,12 @@ coordination of subagents.
38
65
  - **Error Handling:** If a subagent fails or returns an ambiguous result,
39
66
  investigate the failure rather than retrying blindly.
40
67
  - **Parallelism:** Use subagents to perform non-dependent tasks concurrently
41
- (e.g., auditing three different modules simultaneously).
68
+ (e.g., auditing three different modules simultaneously). A child that is
69
+ itself an orchestrator may parallelize its own sub-units the same way.
42
70
 
43
71
  ## 3. Best Practices
44
72
 
45
- - **State Sync:** Ensure the main agent's mental model remains the source of
73
+ - **State Sync:** Ensure the orchestrator's mental model remains the source of
46
74
  truth if multiple subagents modify the codebase.
47
75
  - **Summarization:** Require subagents to provide a concise summary of their
48
- findings to prevent the main context window from being flooded.
76
+ findings to prevent the orchestrator's context window from being flooded.
@@ -18,11 +18,17 @@ confirmation:
18
18
  3. **reap merged local branches** โ€” the existing squash-aware
19
19
  `gh pr list --state merged` + `git branch --merged <base>` sweep,
20
20
  with attached worktrees removed first. Optionally also deletes the
21
- `origin/<branch>` ref when `--remote` is passed. With `--remote`,
22
- the planner additionally enumerates `refs/remotes/origin/*` and
23
- reaps any **remote-only** merged branches โ€” branches whose local
24
- ref is already gone (or never existed) but whose `origin/<branch>`
25
- still points at a merged PR.
21
+ `origin/<branch>` ref when `--remote` is passed. Every default run
22
+ also **enumerates** `refs/remotes/origin/*` and reports any
23
+ **remote-only** merged branches โ€” branches whose local ref is
24
+ already gone (or never existed) but whose `origin/<branch>` still
25
+ points at a merged PR โ€” even without `--remote`; `--remote` is still
26
+ required to *delete* them. A third branch, whose content already
27
+ landed in `<base>` by another route (a squash-merged Epic PR, a
28
+ cherry-pick, a manual `merge --squash`), is caught by a
29
+ **content-equivalence probe** (`git merge-tree --write-tree`,
30
+ git โ‰ฅ 2.38) even when it has no merged PR of its own and is not a
31
+ git ancestor of `<base>`.
26
32
  4. **triage `git stash` entries** โ€” list every stash and prompt for
27
33
  `drop / keep / quit` per entry (or pass `--drop-stashes <ref>` for
28
34
  non-interactive use).
@@ -150,6 +156,10 @@ programmatic consumption:
150
156
  "detectedBy": "gh"
151
157
  }
152
158
  ],
159
+ "skipped": [
160
+ { "branch": "story-4200", "reason": "not-merged", "lastCommitAt": "2026-05-01T00:00:00Z" }
161
+ ],
162
+ "ghDegraded": false,
153
163
  "worktrees": [{ "path": "C:/repo/.worktrees/fix-foo", "ok": true, "dirty": false }],
154
164
  "local": [{ "branch": "fix/foo", "ok": true, "alreadyGone": false }],
155
165
  "remote": [{ "branch": "fix/foo", "ok": true, "alreadyGone": true }],
@@ -200,18 +210,51 @@ follow-up prune when `--remote` is set, so passing both is idempotent
200
210
 
201
211
  ### branches
202
212
 
203
- The merged-branch sweep semantics:
204
-
205
- - A branch is a candidate iff it is not `<base>`, not the current
206
- HEAD, not in `git config branch.protectedBranches`, and either has a
207
- merged PR (`gh pr list --head <branch> --state merged`) or appears in
208
- `git branch --merged <base>`.
213
+ The merged-branch sweep recognizes three detection signals, in order:
214
+
215
+ 1. **`detectedBy: 'gh'`** โ€” the branch has a merged PR
216
+ (`gh pr list --head <branch> --state all`, classified by the
217
+ **latest** PR's state).
218
+ 2. **`detectedBy: 'git-merged'`** โ€” the branch is a git ancestor of
219
+ `<base>` (`git branch --merged <base>`), or of `origin/<base>` when
220
+ that remote-tracking ref exists (unioned so a stale local `<base>` โ€”
221
+ fast-forward phase skipped, or `--branches` run alone โ€” no longer
222
+ hides a branch already merged on the remote).
223
+ 3. **`detectedBy: 'content-merged'`** (Story #4395) โ€” the branch has no
224
+ reapable PR verdict and is not an ancestor of `<base>` under either
225
+ anchor, but simulating the merge via
226
+ `git merge-tree --write-tree <base> <branch>` (git โ‰ฅ 2.38) produces a
227
+ tree identical to `<base>`'s own tree โ€” i.e. applying the branch's
228
+ changes on top of `<base>` is a content no-op. This catches
229
+ `story-<id>` branches merged into `epic/<id>` whose Epic PR
230
+ **squash-merged** to `main` (the story commits are not ancestors of
231
+ `main` and the story branch usually has no PR of its own), and any
232
+ other branch whose content landed via a different route (a renamed
233
+ head, a cherry-pick, a manual `merge --squash`). When git rejects
234
+ `--write-tree` (git < 2.38) or the simulated merge conflicts, the
235
+ probe is inconclusive and the branch keeps its existing `not-merged`
236
+ skip โ€” the signal never guesses. `content-merged` candidates render
237
+ with a "weaker signal โ€” verify before deleting" annotation in the
238
+ dry-run list and are called out separately in the confirmation
239
+ prompt, since โ€” unlike a merged PR or git ancestry โ€” no CI or GitHub
240
+ merge check ever validated this branch's exact diff.
241
+
242
+ Other candidate semantics:
243
+
244
+ - A branch is a candidate iff it is not `<base>`, not the current HEAD,
245
+ not in `git config branch.protectedBranches`, and matches one of the
246
+ three signals above.
209
247
  - When a candidate has an attached worktree, the worktree is removed
210
248
  (force if dirty) **before** `git branch -D`, mirroring the pattern in
211
249
  [`worktree-lifecycle.md`](helpers/worktree-lifecycle.md).
212
250
  - `--remote` is required on top of `--execute` to touch `origin/`.
251
+ - A throwing `gh` runner (auth failure, rate limit, missing binary) no
252
+ longer aborts the run: the branches phase logs one warning and
253
+ continues with the git-only signals (ancestry + content-equivalence).
254
+ The JSON envelope's `ghDegraded: true` records that this happened for
255
+ the run, and the dry-run text carries a matching warning line.
213
256
 
214
- The skip taxonomy distinguishes two unreapable cases:
257
+ The skip taxonomy:
215
258
 
216
259
  - `reason: 'protected'` โ€” the base branch or a name in
217
260
  `git config branch.protectedBranches`. Not reapable; ignore.
@@ -219,15 +262,26 @@ The skip taxonomy distinguishes two unreapable cases:
219
262
  `git checkout <base>`. The dry-run output surfaces a remediation
220
263
  hint so the operator sees the recovery path without having to look
221
264
  in the JSON envelope.
222
-
223
- The `--remote` flag also opts the planner into a **remote-only
265
+ - `reason: 'tip-diverged-from-merge'` โ€” the latest PR merged, but the
266
+ branch's tip has since moved past the merged commit (a post-merge
267
+ force-push). The dry-run line names both SHAs and a remediation hint
268
+ (delete manually via `git branch -D <branch>`, or push the follow-up
269
+ commit).
270
+ - `reason: 'not-merged'` โ€” none of the three detection signals matched.
271
+ Previously silent; the dry-run output now lists every surviving
272
+ `not-merged` branch as a one-line-per-branch summary with its
273
+ last-commit age, so the operator can see why a leftover branch isn't
274
+ reaped instead of hunting for it by hand.
275
+
276
+ Every default run also opts the planner into a **remote-only
224
277
  enumeration pass**: in addition to walking `refs/heads/*`, the planner
225
278
  also walks `refs/remotes/origin/*` and emits candidates for any branch
226
279
  that exists on `origin` with a merged PR but has no local ref. These
227
- candidates carry `detectedBy: 'remote-only'` and `localExists: false`,
228
- and the executor runs only the `git push --delete origin/<branch>`
229
- path for them (no local `git branch -D` is attempted โ€” there is no
230
- local branch to delete).
280
+ candidates carry `detectedBy: 'remote-only'` and `localExists: false`
281
+ and are always shown in the dry-run list; **deleting** them (via the
282
+ `git push --delete origin/<branch>` path โ€” no local `git branch -D` is
283
+ attempted, since there is no local branch) still requires `--remote` on
284
+ top of `--execute`, unchanged.
231
285
 
232
286
  ### stashes
233
287
 
@@ -70,6 +70,33 @@ it is about to run** before it acts.
70
70
 
71
71
  ---
72
72
 
73
+ ## Boot sweep
74
+
75
+ Before detecting the git setup, run the **protected boot sweep** so
76
+ `/git-deliver` starts from a tidy local checkout โ€” a feature branch this
77
+ command opened and pushed on a prior run, once its PR has merged, is reaped
78
+ here rather than left to accumulate:
79
+
80
+ ```bash
81
+ node .agents/scripts/boot-sweep.js \
82
+ --include 'feat/*' --include 'fix/*' --include 'chore/*' \
83
+ --include 'docs/*' --include 'refactor/*' \
84
+ --current "$(git rev-parse --abbrev-ref HEAD)"
85
+ ```
86
+
87
+ This is the **safe subset** of the `/git-cleanup` phases: it fast-forwards the
88
+ base branch (`main`), prunes stale remote-tracking refs, and reaps every local
89
+ branch whose PR is **merged** and whose HEAD matches the merged `headRefOid`.
90
+ It **never** touches the stash stack, and its `evaluateProtection` partition
91
+ skips (never reaps) any candidate with unpushed work, a dirty worktree, or a
92
+ still-open parent ticket; `--current` always excludes the branch you are on.
93
+ The sweep is **silent on a no-op** โ€” nothing merged, `main` already current โ†’
94
+ one summary line (`[boot-sweep] reaped 0 local + 0 remote; protected 0.`). Its
95
+ exit code is always `0`; a failed sweep is reported in that summary, never
96
+ allowed to fail the delivery run.
97
+
98
+ ---
99
+
73
100
  ## Step 0 โ€” Detect Git Setup & Resolve Level
74
101
 
75
102
  1. Resolve `[BASE_BRANCH]` from `--base` or `.agentrc.json` โ†’
@@ -248,6 +275,15 @@ Do **not** poll CI. That is the `/deliver` Phase 7 job and is overkill for
248
275
  ad-hoc changes. The operator (or GitHub's email notification) is the next
249
276
  watcher.
250
277
 
278
+ > **Local ref left behind โ€” reaped at the next boot.** At the **pr** level
279
+ > this command leaves a local feature branch behind after the PR merges; it
280
+ > does **not** reap it inline (the merge happens later, out of band). You do
281
+ > not need to run `/git-cleanup` by hand: the next time `/git-deliver` (or
282
+ > `/plan`) runs, its **Boot sweep** step reaps that merged ad-hoc branch and
283
+ > fast-forwards `main` automatically. The delivering flow owns tidying its own
284
+ > refs on the next boot โ€” see
285
+ > [`.agents/rules/git-conventions.md` ยง Local checkout hygiene](../rules/git-conventions.md).
286
+
251
287
  ---
252
288
 
253
289
  ## Troubleshooting
@@ -35,7 +35,29 @@ per-criterion, mid-delivery, and evaluates the actual work product.
35
35
  1. **Eval pass (fresh context, independent of the author).** Run a **separate
36
36
  critic pass** โ€” a fresh-context sub-agent (`Agent` tool,
37
37
  `subagent_type: general-purpose`), *not* a continuation of your implementing
38
- turn โ€” so the evaluator does not grade its own homework. The critic:
38
+ turn โ€” so the evaluator does not grade its own homework.
39
+
40
+ > **Inline-critic fallback (nesting-absent harness).** Dispatching the
41
+ > critic as a nested `Agent` is the preferred shape โ€” it gives genuine
42
+ > fresh-context isolation โ€” and works on any harness that carries `Agent`
43
+ > into sub-agents (Claude Code โ‰ฅ 2.1.202; see
44
+ > [#2870](https://github.com/dsj1984/mandrel/issues/2870)). This eval loop
45
+ > itself runs inside a Story delivery sub-agent, so the nested critic sits
46
+ > at nesting depth 2. If the host does **not** support nested `Agent`
47
+ > dispatch at that depth โ€” the tool is absent, or a spawn attempt returns
48
+ > an unsupported-capability error โ€” do **not** stall the Story. Fall back
49
+ > to authoring the verdict **inline**: in a deliberately scoped,
50
+ > self-critical pass (re-read only the diff, the `acceptance[]` /
51
+ > `verify[]` arrays, and the `verify[]` command output โ€” treat the
52
+ > implementation reasoning as untrusted and score against the criteria
53
+ > afresh), write the same verdict file described below and hand it to the
54
+ > same `acceptance-eval.js` gate. The fresh-context isolation is weaker in
55
+ > the inline path, but the gate, the schema, the round cap, and the
56
+ > proceed / redraft / block decision are identical โ€” a Story is **never**
57
+ > stranded on a nesting-absent harness. Note in the blocked/friction
58
+ > comment (if you block) that the inline fallback was used.
59
+
60
+ The critic:
39
61
  - Inspects the working diff (`git diff origin/<baseBranch>...HEAD`) and the
40
62
  Story's inline `acceptance[]` / `verify[]` arrays.
41
63
  - **Runs the `verify[]` commands** and consumes their output as **required
@@ -509,20 +509,26 @@ node .agents/scripts/lifecycle-emit.js --epic <epicId> \
509
509
 
510
510
  If Phase 8.5 fell back to the operator-merges-button path (`gh pr
511
511
  merge --auto` was declined), the `epic.merge.armed` event never fires
512
- inside this run and Phase 9 will not run automatically. After the
513
- operator merges the PR, `epic/<epicId>` and each `story-<id>` ref can
514
- be reaped manually:
512
+ inside this run and Phase 9 will not run automatically. **Do not** hand-reap
513
+ the refs with a raw `git branch -D` sequence โ€” drive the same
514
+ `BranchCleaner`-backed reap the auto-merge path uses by firing
515
+ `epic.merge.armed` after the operator merges the PR:
515
516
 
516
517
  ```bash
517
- git checkout main
518
- git pull --ff-only origin main
519
- git branch -D epic/<epicId>
520
- git branch -D story-<id1> story-<id2> ...
521
- git remote prune origin
518
+ node .agents/scripts/lifecycle-emit.js --epic <epicId> \
519
+ --event epic.merge.armed --pr-url <prUrl>
522
520
  ```
523
521
 
524
- Note that `git-cleanup.js` alone will not catch `story-<id>` refs in
525
- this case because the epic PR squash-merges break the `git branch
526
- --merged main` signal and the stories never had their own PRs. Wiring
527
- a CLI surface that drives the BranchCleaner listener for this
528
- fallback is tracked as follow-up to Story #2398.
522
+ That single emit reaps `epic/<epicId>` and every `story-<id>` ref from the
523
+ checkpoint, prunes stale tracking refs, and fast-forwards local `main` to
524
+ `origin/main` โ€” the whole Phase 9 reap, not a partial hand-roll. A plain
525
+ `git-cleanup.js` sweep alone will **not** catch the `story-<id>` refs here,
526
+ because the epic PR squash-merge breaks the `git branch --merged main` signal
527
+ and the stories never had their own PRs; the lifecycle-emit surface above is
528
+ the correct driver.
529
+
530
+ Re-running `/deliver <epicId>` reaches the same outcome without the manual
531
+ emit: the idempotent-resume auto-arm
532
+ (`detectMergedUncleanedEpic` โ†’ `armCleanupIfMerged` in
533
+ [`epic-cleanup.js`](../../scripts/lib/orchestration/epic-cleanup.js)) detects
534
+ the merged-but-uncleaned Epic and fires `epic.merge.armed` for you.
@@ -89,9 +89,17 @@ Every other runtime modifier is sourced from the Epic's labels or from
89
89
  - **Single pause point.** Only `agent::blocked` halts execution. No
90
90
  clarifying questions โ€” if stuck, flip to `agent::blocked`, post a
91
91
  friction comment, park.
92
- - **Two-level dispatch.** Host LLM fans out per-Story Agent calls
93
- directly with `subagent_type: general-purpose`. Sub-agents do not
94
- carry the `Agent` tool, so this stays flat.
92
+ - **Flat Story dispatch by design.** Host LLM fans out per-Story Agent
93
+ calls directly with `subagent_type: general-purpose`. Keeping Story
94
+ dispatch flat โ€” the host owns the single fan-out level โ€” is a
95
+ **design choice**, not a harness constraint: the wave aggregator, idle
96
+ watchdog, and merge-lock all assume one host-owned dispatch level. As of
97
+ Claude Code 2.1.202 a level-1 sub-agent **does** carry the `Agent` tool
98
+ and can nest further (verified depth 2, announced max depth 5; see
99
+ [#2870](https://github.com/dsj1984/mandrel/issues/2870)), so a Story
100
+ worker may itself fan out for its own sub-work within that depth budget โ€”
101
+ the Epic wave loop nonetheless stays flat by choice, not because nesting
102
+ is unavailable.
95
103
  - **Operator-merges-PR exit.** Phase 7 opens the PR; the workflow
96
104
  never merges to `main` itself. Phase 8.5 may fire auto-merge when
97
105
  every signal is clean.
@@ -481,6 +489,17 @@ therefore auto-runs its mapped lenses (e.g. a `security`-axis Epic runs
481
489
  low-risk Epic adds nothing. Findings are persisted as an `audit-results`
482
490
  structured comment on the Epic.
483
491
 
492
+ The helper walks the selected roster **serially in-context by default**; when
493
+ the roster carries more than one lens it **may delegate the walk to a single
494
+ audit-orchestrator sub-agent** that fans the already-selected lenses out as
495
+ parallel level-2 agents and returns only the aggregated `audit-results` (see
496
+ [`epic-audit.md` ยง "Optional: delegate the roster walk to an audit-orchestrator
497
+ sub-agent"](epic-audit.md), within the sub-agent depth budget noted under
498
+ "Flat Story dispatch by design" above). The roster stays fixed upstream, every
499
+ per-lens cost gate is preserved, and the seven sequential-only lenses are **not**
500
+ batch-converted โ€” the fan-out parallelizes across lenses only and never changes
501
+ how any single lens runs internally.
502
+
484
503
  - **Any ๐Ÿ”ด Critical Blocker** โ€” STOP. Relay to the operator.
485
504
  - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข** โ€” log as non-blocking and continue.
486
505
  - **Selector reports `degraded: true`** โ€” STOP. Propagate the
@@ -734,6 +753,18 @@ On an arming decision the predicate emits `epic.merge.ready`; the downstream
734
753
  `epic.merge.blocked` with the disqualifying reasons and exits without merging
735
754
  โ€” the operator merges manually.
736
755
 
756
+ **Blocked-path output (operator merges the button).** When arming is
757
+ declined, `epic.merge.armed` never fires inside this run, so Phase 9 does not
758
+ reap automatically. Surface the exact one-liner the operator runs **after**
759
+ they merge the PR by hand so local refs are reaped and `main` is
760
+ fast-forwarded (the idempotent-resume path below runs this automatically on
761
+ the next `/deliver <epicId>`):
762
+
763
+ ```bash
764
+ node .agents/scripts/lifecycle-emit.js --epic <epicId> \
765
+ --event epic.merge.armed --pr-url <prUrl>
766
+ ```
767
+
737
768
  Close the phase wrapper by emitting `epic.automerge.end` (records the arm
738
769
  outcome on the ledger; `merged: true` once GitHub completes the squash,
739
770
  `merged: false` with a reason otherwise):
@@ -784,6 +815,19 @@ via `helpers/epic-deliver-story`'s own checkpointing). The PR from Phase 7 is
784
815
  updated in place on subsequent runs. The authoritative live view is
785
816
  the `epic-run-progress` structured comment.
786
817
 
818
+ **Resume auto-arm for a merged-but-uncleaned Epic.** When `/deliver` resumes
819
+ against an Epic whose PR already merged (operator merged the button in a prior
820
+ session) but whose local `epic/<id>` / `story-<id>` refs still linger, the
821
+ resume path detects the merged-but-uncleaned state
822
+ (`detectMergedUncleanedEpic` in
823
+ [`epic-cleanup.js`](../../scripts/lib/orchestration/epic-cleanup.js)) and fires
824
+ `epic.merge.armed` automatically so Phase 9 reaps โ€” no manual command. The
825
+ detection is idempotent: an already-reaped Epic (no local refs) is a clean
826
+ no-op, and an unmerged Epic never arms. It resolves the merged PR's URL for the
827
+ required `epic.merge.armed` payload and fails closed (does **not** arm) on any
828
+ indeterminate `gh` probe. The one-liner under Phase 8.5 / Phase 9 is the manual
829
+ equivalent for the case where the operator does not re-run `/deliver`.
830
+
787
831
  ---
788
832
 
789
833
  ## Constraints
@@ -318,11 +318,24 @@ Print a final run summary listing every delivered Story in completion order:
318
318
  All Stories delivered. PRs opened, auto-merge armed. CI will merge each
319
319
  PR when checks pass; each child then confirms the merge and flips its
320
320
  Story to `agent::done` (Story #3385 โ€” until the merge confirms, a Story
321
- rests at `agent::closing` with its issue OPEN). Run
322
- `git-cleanup --fast-forward-main` after the last merge to bring local
323
- main up to date.
321
+ rests at `agent::closing` with its issue OPEN).
324
322
  ```
325
323
 
324
+ Then **fast-forward `main` yourself** โ€” do not instruct the operator to run it
325
+ after the last merge. The delivering flow owns bringing the local base branch
326
+ up to whatever has merged so far:
327
+
328
+ ```bash
329
+ node .agents/scripts/git-cleanup.js --fast-forward-main --execute --yes
330
+ ```
331
+
332
+ This runs only the fast-forward-main phase (`git fetch origin main` โ†’
333
+ `git merge --ff-only`); it is idempotent and a no-op when `main` is already
334
+ current, so it is safe to run even while some PRs are still queued in
335
+ auto-merge. Report its one-line result in the summary. Merged Story branches
336
+ themselves are reaped by the boot sweep at the next `/plan` / `/deliver` boot โ€”
337
+ see [`.agents/rules/git-conventions.md` ยง Local checkout hygiene](../../rules/git-conventions.md).
338
+
326
339
  When some Stories are blocked or failed, list them explicitly with the
327
340
  `blockerCommentId` or failure detail so the operator knows where to look.
328
341