pan-wizard 3.24.0 → 3.25.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/pan-conductor.md
CHANGED
|
@@ -192,7 +192,7 @@ This agent file ships to all runtimes (keeps the installer uniform), but only ge
|
|
|
192
192
|
When invoked by `/pan:army` (ADR-0033), you are **Mission Control** for a whole-project campaign, not a single phase — same harness, wider scope. The differences:
|
|
193
193
|
|
|
194
194
|
- **You delegate to squads, not bare agents.** Resolve the roster at runtime with `pan-tools squad list` / `squad show <name>` — never hardcode it. Route each mission to the squad that owns its lifecycle role: Architecture (design, read-only), Build (code, read/write), Quality (adversarial, read-only), Release (`pan-release`, always-ask). Workers (document_code, distiller) take narrow, high-volume jobs; they are the agents the `budget` profile drops to the `fast` tier, and run at the inherited reasoning tier otherwise.
|
|
195
|
-
- **Build parallelizes by worktree.** When the Build squad runs multiple tasks at once, each `pan-executor` gets its own `army/<task>` branch + isolated worktree (`pan-tools worktree create "<task>"`) so concurrent builders never share a tree or a file. The spawn cap and budget ceiling still bound the fan-out.
|
|
195
|
+
- **Build parallelizes by worktree — and you tear the worktrees down.** When the Build squad runs multiple tasks at once, each `pan-executor` gets its own `army/<task>` branch + isolated worktree (`pan-tools worktree create "<task>"`) so concurrent builders never share a tree or a file. The spawn cap and budget ceiling still bound the fan-out. Those worktrees are scaffolding: after each task's squash-merge lands, `pan-tools worktree remove <path> --branch army/<task>`; at campaign end or on any abort, `pan-tools worktree cleanup` sweeps the strays (`--force` to also discard dirty or unintegrated ones). Leaving `pan-army-*` sibling directories behind is a campaign defect, not residue (P-1815).
|
|
196
196
|
- **Integration is human-gated.** You never merge to a protected branch. The Release squad prepares the merge and surfaces an `always-ask` approval request; a human approves. Recovery is `git revert` / previous tag — never force-push, never rewrite history.
|
|
197
197
|
- **The loop carries learnings.** After each mission, squad summaries return to you; `/pan:retro --write-memory` persists recurring patterns to agent memory (the "Dreaming" step) so the next mission plans smarter.
|
|
198
198
|
|
package/commands/pan/army.md
CHANGED
|
@@ -133,6 +133,8 @@ Spawn the Quality squad on the built tree (parallel, read-only). Merge findings
|
|
|
133
133
|
### Phase 5 — Integrate (Release, human-gated)
|
|
134
134
|
Spawn `pan-release`. It prepares the squash-merge, runs the configured `verification`, and surfaces an **always-ask** approval request. A human approves the merge to the protected branch; release then tags and records the rollback target. `--push` pushes the approved result.
|
|
135
135
|
|
|
136
|
+
**Teardown after the merge lands (P-1815):** worktrees are scaffolding, not deliverables — the squash-merge is the permanent artifact. Once the approval lands, remove each integrated task's worktree **and** its branch in one call: `pan-tools worktree remove ../pan-army-<slug> --branch army/<slug>`. The `--branch` half is not optional: a squash-merged branch never looks merged to git, so nothing else will ever clean it. At campaign end — and on **any** abort path — sweep whatever remains: `pan-tools worktree cleanup` (`--force` also discards dirty worktrees and unintegrated branches; without it the sweep keeps anything that might hold the only copy of work, and says exactly why and how to delete it). A campaign that ends with `pan-tools worktree list` non-empty has skipped this step.
|
|
137
|
+
|
|
136
138
|
**Phase report (opt-in build deliverable):** when `workflow.phase_reports.enabled` is `true`, generate the mission's self-contained per-phase HTML report **in the built tree, before staging the squash-merge** — `pan-tools report phase <N>` — so the report rides along in the merge as a phase deliverable. **Never run `report index` inside a squad worktree:** the timeline index is a single shared file that aggregates *all* phases, so a worktree would see only its own phase and concurrent squads would race on it. The index is a single-writer, post-merge concern (Phase 6). Never opens a browser.
|
|
137
139
|
|
|
138
140
|
### Phase 6 — Learn (Dreaming)
|
|
@@ -142,6 +144,18 @@ Squad summaries return to Mission Control. Run `/pan:retro --write-memory` (and
|
|
|
142
144
|
|
|
143
145
|
---
|
|
144
146
|
|
|
147
|
+
<output_paths>
|
|
148
|
+
|
|
149
|
+
- the project tree + `.planning/orchestration/` — mission state, reports, loop-state (permanent)
|
|
150
|
+
- `<parent-of-project>/pan-army-<task-slug>/` — one worktree per Build task, a **sibling of the project directory** (temporary — removed per task by the Phase 5 teardown; strays swept by `pan-tools worktree cleanup`)
|
|
151
|
+
- branches `army/<task-slug>` — the worktrees' branches (temporary — deleted with their worktree once the squash-merge lands; squash-merged branches never look merged to git and must be deleted explicitly)
|
|
152
|
+
|
|
153
|
+
Nothing under `pan-army-*` or on an `army/*` branch is a deliverable: the squash-merge into the protected branch is the permanent artifact, same doctrine as what-if's disposable worktrees.
|
|
154
|
+
|
|
155
|
+
</output_paths>
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
145
159
|
## Scheduled, self-resuming campaigns (ADR-0034)
|
|
146
160
|
|
|
147
161
|
PAN is not a daemon — it cannot wake itself while the session is closed. `--schedule` arms a campaign and lets an external trigger drive it; the human merge gate is never relaxed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.0",
|
|
4
4
|
"description": "Command a bot army for your codebase: an Opus Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pan-wizard": "bin/install.js"
|
|
@@ -89,6 +89,91 @@ function listArmyWorktrees(cwd) {
|
|
|
89
89
|
return out;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Sweep every army worktree and orphaned army/ branch (P-1815, PanLoop
|
|
94
|
+
* finding 13). The what-if subsystem always had this (`whatif cleanup`); the
|
|
95
|
+
* army path created worktrees that nothing removed — sibling `pan-army-*`
|
|
96
|
+
* directories and `army/*` branches accumulated after every campaign.
|
|
97
|
+
*
|
|
98
|
+
* Safety posture (the L3 lesson — never trade clutter for silent data loss):
|
|
99
|
+
* - A DIRTY worktree is kept unless `force` — git refuses, we surface why.
|
|
100
|
+
* - A branch is deleted only when its tip is reachable from HEAD (truly
|
|
101
|
+
* integrated). Squash-merged and aborted branches are NOT reachable and may
|
|
102
|
+
* hold the only copy of real work, so by default they are KEPT and listed
|
|
103
|
+
* with the exact command to delete them; `force` sweeps them too.
|
|
104
|
+
* (The per-task Phase 5 teardown — `worktree remove --branch` right after
|
|
105
|
+
* the merge lands — deletes unconditionally; at that moment the deletion is
|
|
106
|
+
* the documented intent. The sweeper is the abort/orphan tool, so it errs
|
|
107
|
+
* the other way.)
|
|
108
|
+
* @param {string} cwd - main project root
|
|
109
|
+
* @param {Object} [opts] - { force: boolean }
|
|
110
|
+
* @returns {{removed_worktrees, deleted_branches, kept, pruned, clean}|{error}}
|
|
111
|
+
*/
|
|
112
|
+
function cleanupArmyWorktrees(cwd, opts) {
|
|
113
|
+
if (!isGitRepo(cwd)) return { error: 'Not a git repo' };
|
|
114
|
+
const force = opts?.force === true;
|
|
115
|
+
const removedWorktrees = [];
|
|
116
|
+
const deletedBranches = [];
|
|
117
|
+
const kept = [];
|
|
118
|
+
|
|
119
|
+
const branchIsIntegrated = (branch) =>
|
|
120
|
+
execGit(cwd, ['merge-base', '--is-ancestor', branch, 'HEAD']).exitCode === 0;
|
|
121
|
+
|
|
122
|
+
const deleteBranch = (branch, hadWorktree) => {
|
|
123
|
+
if (force || branchIsIntegrated(branch)) {
|
|
124
|
+
const del = execGit(cwd, ['branch', '-D', branch]);
|
|
125
|
+
if (del.exitCode === 0) deletedBranches.push(branch);
|
|
126
|
+
else kept.push({ branch, reason: `branch -D failed: ${del.stderr.trim()}` });
|
|
127
|
+
} else {
|
|
128
|
+
kept.push({
|
|
129
|
+
branch,
|
|
130
|
+
reason: `carries commits not reachable from HEAD (squash-merged or aborted work${hadWorktree ? '' : '; no worktree attached'}) — rerun with --force, or: git branch -D ${branch}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 1. Registered army worktrees. Track every branch step 1 has already
|
|
136
|
+
// decided on — deleted OR deliberately kept — so the orphan scan below
|
|
137
|
+
// does not re-process (and double-report) it.
|
|
138
|
+
const handledBranches = new Set();
|
|
139
|
+
for (const t of listArmyWorktrees(cwd)) {
|
|
140
|
+
const rmArgs = ['worktree', 'remove'];
|
|
141
|
+
if (force) rmArgs.push('--force');
|
|
142
|
+
rmArgs.push(t.worktree);
|
|
143
|
+
const rm = execGit(cwd, rmArgs);
|
|
144
|
+
if (rm.exitCode !== 0) {
|
|
145
|
+
kept.push({ worktree: t.worktree, branch: t.branch, reason: `worktree remove refused: ${rm.stderr.trim()} — pass --force to discard uncommitted changes` });
|
|
146
|
+
if (t.branch) handledBranches.add(t.branch);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
removedWorktrees.push(t.worktree);
|
|
150
|
+
if (t.branch) {
|
|
151
|
+
handledBranches.add(t.branch);
|
|
152
|
+
deleteBranch(t.branch, true);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Drop stale registrations (a manually deleted directory leaves one).
|
|
157
|
+
const pruned = execGit(cwd, ['worktree', 'prune']).exitCode === 0;
|
|
158
|
+
|
|
159
|
+
// 3. Orphaned army/ branches — a worktree removed without its branch.
|
|
160
|
+
const stillAttached = new Set(listArmyWorktrees(cwd).map(t => t.branch));
|
|
161
|
+
const ls = execGit(cwd, ['branch', '--list', `${ARMY_BRANCH_PREFIX}*`, '--format=%(refname:short)']);
|
|
162
|
+
if (ls.exitCode === 0) {
|
|
163
|
+
for (const branch of ls.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
164
|
+
if (!stillAttached.has(branch) && !handledBranches.has(branch)) deleteBranch(branch, false);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
removed_worktrees: removedWorktrees,
|
|
170
|
+
deleted_branches: deletedBranches,
|
|
171
|
+
kept,
|
|
172
|
+
pruned,
|
|
173
|
+
clean: kept.length === 0,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
92
177
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
93
178
|
|
|
94
179
|
function cmdWorktreeList(cwd, raw) {
|
|
@@ -112,12 +197,25 @@ function cmdWorktreeRemove(cwd, worktreePath, branch, raw, opts) {
|
|
|
112
197
|
output(r, raw, r.warnings.length ? r.warnings.join('\n') : 'removed');
|
|
113
198
|
}
|
|
114
199
|
|
|
200
|
+
function cmdWorktreeCleanup(cwd, raw, opts) {
|
|
201
|
+
const r = cleanupArmyWorktrees(cwd, opts);
|
|
202
|
+
if (r.error) return error(r.error);
|
|
203
|
+
const lines = [
|
|
204
|
+
...r.removed_worktrees.map(w => `removed worktree ${w}`),
|
|
205
|
+
...r.deleted_branches.map(b => `deleted branch ${b}`),
|
|
206
|
+
...r.kept.map(k => `KEPT ${k.worktree || k.branch}: ${k.reason}`),
|
|
207
|
+
];
|
|
208
|
+
output(r, raw, lines.length ? lines.join('\n') : 'nothing to clean');
|
|
209
|
+
}
|
|
210
|
+
|
|
115
211
|
module.exports = {
|
|
116
212
|
ARMY_BRANCH_PREFIX,
|
|
117
213
|
createTaskWorktree,
|
|
118
214
|
removeTaskWorktree,
|
|
119
215
|
listArmyWorktrees,
|
|
216
|
+
cleanupArmyWorktrees,
|
|
120
217
|
cmdWorktreeList,
|
|
121
218
|
cmdWorktreeCreate,
|
|
122
219
|
cmdWorktreeRemove,
|
|
220
|
+
cmdWorktreeCleanup,
|
|
123
221
|
};
|
|
@@ -1191,8 +1191,10 @@ async function main() {
|
|
|
1191
1191
|
worktree.cmdWorktreeCreate(cwd, args[2], raw, { base: getArgValue(args, '--base') });
|
|
1192
1192
|
} else if (subcommand === 'remove') {
|
|
1193
1193
|
worktree.cmdWorktreeRemove(cwd, args[2], getArgValue(args, '--branch'), raw, { force: args.includes('--force') });
|
|
1194
|
+
} else if (subcommand === 'cleanup') {
|
|
1195
|
+
worktree.cmdWorktreeCleanup(cwd, raw, { force: args.includes('--force') });
|
|
1194
1196
|
} else {
|
|
1195
|
-
error('Unknown worktree subcommand. Available: list, create, remove');
|
|
1197
|
+
error('Unknown worktree subcommand. Available: list, create, remove, cleanup');
|
|
1196
1198
|
}
|
|
1197
1199
|
break;
|
|
1198
1200
|
}
|