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.
- package/.agents/instructions.md +7 -0
- package/.agents/rules/git-conventions.md +45 -0
- package/.agents/scripts/boot-sweep.js +215 -0
- package/.agents/scripts/epic-deliver-prepare.js +55 -0
- package/.agents/scripts/git-cleanup.js +8 -0
- package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +107 -30
- package/.agents/scripts/lib/epic-plan-ideation.js +24 -3
- package/.agents/scripts/lib/framework-version.js +210 -0
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +7 -22
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +330 -6
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +34 -3
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +102 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes-ff.js +83 -30
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +85 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +34 -3
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +71 -4
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +8 -3
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +54 -6
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +35 -4
- package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +75 -0
- package/.agents/scripts/lib/single-story-sweep.js +239 -57
- package/.agents/scripts/lib/story-body/story-body.js +81 -4
- package/.agents/scripts/providers/github/tickets.js +18 -1
- package/.agents/scripts/single-story-init.js +7 -51
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +7 -2
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +8 -2
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +36 -8
- package/.agents/workflows/git-cleanup.md +72 -18
- package/.agents/workflows/git-deliver.md +36 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +23 -1
- package/.agents/workflows/helpers/deliver-epic-reference.md +19 -13
- package/.agents/workflows/helpers/deliver-epic.md +47 -3
- package/.agents/workflows/helpers/deliver-stories.md +16 -3
- package/.agents/workflows/helpers/epic-audit.md +60 -2
- package/.agents/workflows/helpers/parallel-tooling.md +9 -2
- package/.agents/workflows/helpers/plan-epic.md +32 -14
- package/.agents/workflows/loops/nightly-audit.md +9 -1
- package/.agents/workflows/plan.md +32 -4
- package/docs/CHANGELOG.md +27 -0
- 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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
381
|
-
"policyCapsuleBullets":
|
|
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
|
|
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
|
|
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
|
|
32
|
-
before incorporating it into the
|
|
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
|
|
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
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
229
|
-
path
|
|
230
|
-
local branch
|
|
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.
|
|
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.
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
- **
|
|
93
|
-
directly with `subagent_type: general-purpose`.
|
|
94
|
-
|
|
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).
|
|
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
|
|