pan-wizard 3.29.0 → 3.31.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/README.md +40 -40
- package/bin/install-lib.cjs +186 -83
- package/bin/install.js +213 -195
- package/commands/pan/army.md +4 -4
- package/commands/pan/audit-deployment.md +2 -2
- package/commands/pan/cost.md +5 -5
- package/commands/pan/exec-phase.md +2 -0
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/verify-phase.md +8 -9
- package/hooks/dist/pan-check-update.js +53 -2
- package/hooks/dist/pan-context-monitor.js +41 -0
- package/hooks/dist/pan-cost-logger.js +71 -2
- package/hooks/dist/pan-state-reinject.js +158 -0
- package/hooks/dist/pan-stop-guard.js +127 -2
- package/hooks/dist/pan-trace-logger.js +42 -1
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
- package/pan-wizard-core/bin/lib/commands.cjs +20 -4
- package/pan-wizard-core/bin/lib/config.cjs +2 -0
- package/pan-wizard-core/bin/lib/constants.cjs +3 -1
- package/pan-wizard-core/bin/lib/context-budget.cjs +27 -2
- package/pan-wizard-core/bin/lib/core.cjs +249 -27
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
- package/pan-wizard-core/bin/lib/cost.cjs +234 -88
- package/pan-wizard-core/bin/lib/frontmatter.cjs +5 -2
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +12 -6
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +12 -5
- package/pan-wizard-core/bin/lib/phase-report.cjs +4 -1
- package/pan-wizard-core/bin/lib/roadmap.cjs +87 -18
- package/pan-wizard-core/bin/lib/state.cjs +86 -22
- package/pan-wizard-core/bin/lib/suggest.cjs +21 -9
- package/pan-wizard-core/bin/lib/template.cjs +6 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +20 -3
- package/pan-wizard-core/bin/pan-tools.cjs +10 -1
- package/pan-wizard-core/mcp/tool-registry.cjs +14 -4
- package/pan-wizard-core/references/model-profiles.md +4 -4
- package/pan-wizard-core/references/planning-config.md +19 -23
- package/pan-wizard-core/workflows/exec-phase.md +8 -0
- package/pan-wizard-core/workflows/help.md +2 -2
- package/pan-wizard-core/workflows/phase-tests.md +1 -1
- package/pan-wizard-core/workflows/progress.md +1 -1
- package/pan-wizard-core/workflows/settings.md +2 -4
- package/pan-wizard-core/workflows/todo-add.md +1 -1
- package/pan-wizard-core/workflows/todo-check.md +7 -6
- package/scripts/build-agent-plugin.js +4 -0
- package/scripts/build-hooks.js +2 -1
- package/scripts/build-plugin.js +10 -0
- package/scripts/npm-dist-tag.js +47 -0
- package/scripts/plugin-path.js +6 -1
- package/scripts/test-surface.cjs +11 -9
package/bin/install.js
CHANGED
|
@@ -21,7 +21,7 @@ const {
|
|
|
21
21
|
convertClaudeCommandToUnifiedSkill,
|
|
22
22
|
getCopilotSkillAdapterHeader, convertClaudeCommandToCopilotSkill, convertClaudeToCopilotAgent,
|
|
23
23
|
processAttribution, parseJsonc,
|
|
24
|
-
detectModelCapabilities,
|
|
24
|
+
detectModelCapabilities, stripThinkingFrontmatter,
|
|
25
25
|
geminiTransitionNotice,
|
|
26
26
|
convertClaudeAgentToCodexToml, codexTrustNotice,
|
|
27
27
|
buildCopilotHooksConfig,
|
|
@@ -262,7 +262,7 @@ console.log(banner);
|
|
|
262
262
|
|
|
263
263
|
// Show help if requested
|
|
264
264
|
if (hasHelp) {
|
|
265
|
-
console.log(` ${yellow}Usage:${reset} npx pan-wizard [options]\n\n ${yellow}Options:${reset}\n ${cyan}-l, --local${reset} Install locally to current directory (default)\n ${cyan}-g, --global${reset} Install globally to config directory\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for GitHub Copilot CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall PAN (remove all PAN files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--unified-skills${reset} Install commands as one shared .agents/skills/ tree (ADR-0028 alpha)\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime; installs project-level)${reset}\n npx pan-wizard\n\n ${dim}# Install for Claude Code in current project (default, --local implied)${reset}\n npx pan-wizard --claude\n\n ${dim}# Install for all runtimes in current project${reset}\n npx pan-wizard --all --local\n\n ${dim}# Install globally (available in all projects)${reset}\n npx pan-wizard --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx pan-wizard --gemini --global\n\n ${dim}# Install to custom config directory${reset}\n npx pan-wizard --codex --global --config-dir ~/.codex-work\n\n ${dim}# Uninstall PAN from Codex globally${reset}\n npx pan-wizard --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n By default, PAN installs into the current project directory only.\n Use --global to install system-wide (writes to ~/.claude, ~/.gemini, etc.).\n The --config-dir option takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME.\n`);
|
|
265
|
+
console.log(` ${yellow}Usage:${reset} npx pan-wizard [options]\n\n ${yellow}Options:${reset}\n ${cyan}-l, --local${reset} Install locally to current directory (default)\n ${cyan}-g, --global${reset} Install globally to config directory\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for GitHub Copilot CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall PAN (remove all PAN files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--unified-skills${reset} Install commands as one shared .agents/skills/ tree, mirrored to .claude/skills/ for Claude Code (ADR-0028 alpha)\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime; installs project-level)${reset}\n npx pan-wizard\n\n ${dim}# Install for Claude Code in current project (default, --local implied)${reset}\n npx pan-wizard --claude\n\n ${dim}# Install for all runtimes in current project${reset}\n npx pan-wizard --all --local\n\n ${dim}# Install globally (available in all projects)${reset}\n npx pan-wizard --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx pan-wizard --gemini --global\n\n ${dim}# Install to custom config directory${reset}\n npx pan-wizard --codex --global --config-dir ~/.codex-work\n\n ${dim}# Uninstall PAN from Codex globally${reset}\n npx pan-wizard --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n By default, PAN installs into the current project directory only.\n Use --global to install system-wide (writes to ~/.claude, ~/.gemini, etc.).\n The --config-dir option takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME.\n`);
|
|
266
266
|
process.exit(0);
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -619,19 +619,70 @@ function sweepProprietaryCommandSurfaces(targetDir, runtime) {
|
|
|
619
619
|
}
|
|
620
620
|
} catch { /* dir absent — nothing to sweep */ }
|
|
621
621
|
} else {
|
|
622
|
-
// Claude Code & Gemini: nested commands/pan tree (+ Claude skill shims)
|
|
622
|
+
// Claude Code & Gemini: nested commands/pan tree (+ legacy Claude skill shims)
|
|
623
623
|
try { fs.rmSync(path.join(targetDir, 'commands', 'pan'), { recursive: true }); } catch {}
|
|
624
|
-
if (runtime === 'claude') {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
624
|
+
if (runtime === 'claude') sweepClaudePanSkills(targetDir, { pruneEmpty: false });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Remove every PAN-owned entry from a Claude Code `skills/` directory: the flat
|
|
630
|
+
* `pan-*.md` shims v2.10.0 (E-5) wrote, and the `pan-*` SKILL.md directories a
|
|
631
|
+
* --unified-skills install mirrors there. The `pan-` prefix is PAN's namespace
|
|
632
|
+
* here exactly as it is in the Codex and Copilot skill sweeps. Non-PAN skills are
|
|
633
|
+
* never touched, and the directory itself is removed only when PAN's entries were
|
|
634
|
+
* all it held. Returns the number of entries removed.
|
|
635
|
+
*/
|
|
636
|
+
function sweepClaudePanSkills(targetDir, { pruneEmpty = true } = {}) {
|
|
637
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
638
|
+
let entries;
|
|
639
|
+
try { entries = fs.readdirSync(skillsDir, { withFileTypes: true }); } catch { return 0; }
|
|
640
|
+
let removed = 0;
|
|
641
|
+
for (const entry of entries) {
|
|
642
|
+
if (!entry.name.startsWith('pan-')) continue;
|
|
643
|
+
const entryPath = path.join(skillsDir, entry.name);
|
|
644
|
+
try {
|
|
645
|
+
if (entry.isDirectory()) {
|
|
646
|
+
fs.rmSync(entryPath, { recursive: true });
|
|
647
|
+
removed++;
|
|
648
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
649
|
+
fs.unlinkSync(entryPath);
|
|
650
|
+
removed++;
|
|
651
|
+
}
|
|
652
|
+
} catch (err) { pushInstallWarning('staleCleanup', `skills/${entry.name}`, err); }
|
|
653
|
+
}
|
|
654
|
+
if (pruneEmpty) {
|
|
655
|
+
try { if (fs.readdirSync(skillsDir).length === 0) fs.rmdirSync(skillsDir); } catch { /* keep it */ }
|
|
634
656
|
}
|
|
657
|
+
return removed;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Claude Code does not read the shared `.agents/skills/` tree: its skills
|
|
662
|
+
* documentation lists `.claude/skills/<name>/SKILL.md` (personal, project, nested,
|
|
663
|
+
* managed, --add-dir), plugin skills, synced skills and legacy `.claude/commands/`
|
|
664
|
+
* files — and no `.agents/` location. Every other target runtime lists the shared
|
|
665
|
+
* tree. So a --unified-skills install for Claude copies each compiled skill into
|
|
666
|
+
* the runtime's own `skills/`, byte-identical to the shared copy: one compiler, two
|
|
667
|
+
* placements (ADR-0028), and the tree's content still does not depend on which
|
|
668
|
+
* runtime installed it. Returns the number of skills copied.
|
|
669
|
+
*/
|
|
670
|
+
function mirrorUnifiedSkillsForClaude(sharedSkillsDir, targetDir, prefix) {
|
|
671
|
+
sweepClaudePanSkills(targetDir, { pruneEmpty: false });
|
|
672
|
+
const claudeSkillsDir = path.join(targetDir, 'skills');
|
|
673
|
+
let copied = 0;
|
|
674
|
+
for (const name of listCodexSkillNames(sharedSkillsDir, `${prefix}-`)) {
|
|
675
|
+
const srcDir = path.join(sharedSkillsDir, name);
|
|
676
|
+
const destDir = path.join(claudeSkillsDir, name);
|
|
677
|
+
try {
|
|
678
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
679
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
680
|
+
if (entry.isFile()) fs.copyFileSync(path.join(srcDir, entry.name), path.join(destDir, entry.name));
|
|
681
|
+
}
|
|
682
|
+
copied++;
|
|
683
|
+
} catch (err) { pushInstallWarning('claudeSkillMirror', `skills/${name}`, err); }
|
|
684
|
+
}
|
|
685
|
+
return copied;
|
|
635
686
|
}
|
|
636
687
|
|
|
637
688
|
/**
|
|
@@ -1075,25 +1126,15 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
1075
1126
|
console.log(` ${green}✓${reset} Removed commands/pan/`);
|
|
1076
1127
|
}
|
|
1077
1128
|
|
|
1078
|
-
// Claude-only: remove skills/
|
|
1129
|
+
// Claude-only: remove PAN's entries from skills/ — the SKILL.md directories a
|
|
1130
|
+
// --unified-skills install mirrors there (R32) and the flat pan-*.md shims
|
|
1131
|
+
// installs before R33 wrote. The skills/ dir goes only if nothing else is in
|
|
1132
|
+
// it (the user may keep their own skills there).
|
|
1079
1133
|
if (runtime === 'claude') {
|
|
1080
|
-
const
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
1085
|
-
try { fs.unlinkSync(path.join(skillsDir, file)); } catch {}
|
|
1086
|
-
skillCount++;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
if (skillCount > 0) {
|
|
1090
|
-
removedCount++;
|
|
1091
|
-
console.log(` ${green}✓${reset} Removed ${skillCount} PAN skill shims`);
|
|
1092
|
-
}
|
|
1093
|
-
// Remove the skills/ dir only if it's now empty (user may have non-PAN skills)
|
|
1094
|
-
try {
|
|
1095
|
-
if (fs.readdirSync(skillsDir).length === 0) fs.rmdirSync(skillsDir);
|
|
1096
|
-
} catch {}
|
|
1134
|
+
const skillCount = sweepClaudePanSkills(targetDir);
|
|
1135
|
+
if (skillCount > 0) {
|
|
1136
|
+
removedCount++;
|
|
1137
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} PAN entries from skills/`);
|
|
1097
1138
|
}
|
|
1098
1139
|
}
|
|
1099
1140
|
}
|
|
@@ -1177,7 +1218,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
1177
1218
|
// 4. Remove PAN hooks (scripts + Copilot CLI hooks config file)
|
|
1178
1219
|
const hooksDir = path.join(targetDir, 'hooks');
|
|
1179
1220
|
if (fs.existsSync(hooksDir)) {
|
|
1180
|
-
const panHooks = ['pan-statusline.js', 'pan-check-update.js', 'pan-check-update.sh', 'pan-context-monitor.js', 'pan-cost-logger.js', 'pan-trace-logger.js', 'pan-stop-guard.js', 'pan.json'];
|
|
1221
|
+
const panHooks = ['pan-statusline.js', 'pan-check-update.js', 'pan-check-update.sh', 'pan-context-monitor.js', 'pan-cost-logger.js', 'pan-trace-logger.js', 'pan-stop-guard.js', 'pan-state-reinject.js', 'pan.json'];
|
|
1181
1222
|
let hookCount = 0;
|
|
1182
1223
|
for (const hook of panHooks) {
|
|
1183
1224
|
const hookPath = path.join(hooksDir, hook);
|
|
@@ -1428,7 +1469,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
1428
1469
|
}
|
|
1429
1470
|
}
|
|
1430
1471
|
|
|
1431
|
-
// Remove PAN stop guard from Stop (P-1809, v3.
|
|
1472
|
+
// Remove PAN stop guard from Stop (P-1809, v3.24+)
|
|
1432
1473
|
if (settings.hooks && settings.hooks.Stop) {
|
|
1433
1474
|
const before = settings.hooks.Stop.length;
|
|
1434
1475
|
settings.hooks.Stop = settings.hooks.Stop.filter(entry => {
|
|
@@ -1449,6 +1490,17 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
1449
1490
|
}
|
|
1450
1491
|
}
|
|
1451
1492
|
|
|
1493
|
+
// Any other event still running a PAN script: Gemini's AfterAgent stop guard
|
|
1494
|
+
// (R29), or an entry an older install left under a key it has since moved
|
|
1495
|
+
// off. Keyed on the script, so no event list can go stale here again.
|
|
1496
|
+
if (settings.hooks) {
|
|
1497
|
+
const sweptEvents = lib.stripPanHookEntries(settings.hooks, lib.PAN_SETTINGS_HOOKS);
|
|
1498
|
+
if (sweptEvents.length > 0) {
|
|
1499
|
+
settingsModified = true;
|
|
1500
|
+
console.log(` ${green}✓${reset} Removed PAN hooks from ${sweptEvents.join(', ')}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1452
1504
|
// Clean up empty hooks object
|
|
1453
1505
|
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
1454
1506
|
delete settings.hooks;
|
|
@@ -1877,19 +1929,17 @@ function writeManifest(configDir, runtime = 'claude', isGlobal = false) {
|
|
|
1877
1929
|
}
|
|
1878
1930
|
}
|
|
1879
1931
|
}
|
|
1880
|
-
// Claude
|
|
1881
|
-
// runtime's
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
1884
|
-
//
|
|
1885
|
-
|
|
1886
|
-
if (runtime === 'claude' && !unifiedSkills) {
|
|
1932
|
+
// Claude Code cannot read the shared tree, so a --unified-skills install copies
|
|
1933
|
+
// each compiled skill into the runtime's own skills/ (R32). Tracking the copies
|
|
1934
|
+
// lets verifyInstall catch a failed copy and saveLocalPatches back up a user
|
|
1935
|
+
// edit. A default install writes nothing under skills/ (the E-5 flat shims were
|
|
1936
|
+
// retired as never-loaded, R33), so there is nothing to track there.
|
|
1937
|
+
if (runtime === 'claude' && unifiedSkills) {
|
|
1887
1938
|
const claudeSkillsDir = path.join(configDir, 'skills');
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
}
|
|
1939
|
+
for (const skillName of listCodexSkillNames(claudeSkillsDir)) {
|
|
1940
|
+
const skillHashes = generateManifest(path.join(claudeSkillsDir, skillName));
|
|
1941
|
+
for (const [rel, hash] of Object.entries(skillHashes)) {
|
|
1942
|
+
manifest.files[`skills/${skillName}/${rel}`] = hash;
|
|
1893
1943
|
}
|
|
1894
1944
|
}
|
|
1895
1945
|
}
|
|
@@ -1964,8 +2014,10 @@ function saveLocalPatches(configDir) {
|
|
|
1964
2014
|
|
|
1965
2015
|
const fullPath = path.join(configDir, relPath);
|
|
1966
2016
|
if (!fs.existsSync(fullPath)) continue;
|
|
1967
|
-
|
|
1968
|
-
|
|
2017
|
+
// A line-ending-only difference is not a user edit (git autocrlf, editors).
|
|
2018
|
+
let current;
|
|
2019
|
+
try { current = fs.readFileSync(fullPath); } catch { continue; }
|
|
2020
|
+
if (!lib.bytesMatchHashIgnoringEol(current, originalHash)) {
|
|
1969
2021
|
const backupPath = path.join(patchesDir, safeRel);
|
|
1970
2022
|
// Defence in depth: never write outside patchesDir, whatever the manifest says.
|
|
1971
2023
|
const resolvedPatches = path.resolve(patchesDir);
|
|
@@ -2187,7 +2239,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2187
2239
|
// OpenCode uses commands/ (flat), Codex uses skills/, Claude/Gemini use commands/pan/
|
|
2188
2240
|
try {
|
|
2189
2241
|
if (unifiedSkills) {
|
|
2190
|
-
// ADR-0028 Phase 1: every runtime consumes one runtime-neutral
|
|
2242
|
+
// ADR-0028 Phase 1: every runtime consumes one runtime-neutral (Claude via the skills/ mirror below)
|
|
2191
2243
|
// .agents/skills/ tree; the proprietary command surface is swept so
|
|
2192
2244
|
// commands don't resolve twice.
|
|
2193
2245
|
const skillsDir = getCodexSkillsRoot(isGlobal);
|
|
@@ -2233,6 +2285,17 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2233
2285
|
} else {
|
|
2234
2286
|
failures.push('.agents/skills/pan-* (unified)');
|
|
2235
2287
|
}
|
|
2288
|
+
|
|
2289
|
+
// Claude Code is the one target runtime that does not read the shared tree
|
|
2290
|
+
// (R32): without this copy the sweep above leaves it with no PAN commands.
|
|
2291
|
+
if (runtime === 'claude') {
|
|
2292
|
+
const mirrored = mirrorUnifiedSkillsForClaude(skillsDir, targetDir, 'pan');
|
|
2293
|
+
if (mirrored > 0 && mirrored === installedSkillNames.length) {
|
|
2294
|
+
console.log(` ${green}✓${reset} Mirrored ${mirrored} unified skills to skills/ (Claude Code does not read .agents/skills/)`);
|
|
2295
|
+
} else {
|
|
2296
|
+
failures.push('skills/pan-* (Claude copy of the unified tree)');
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2236
2299
|
} else if (isOpencode) {
|
|
2237
2300
|
// OpenCode: flat structure in commands/ directory. Plural since
|
|
2238
2301
|
// OpenCode 2026 releases — singular command/ is back-compat only.
|
|
@@ -2310,45 +2373,20 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2310
2373
|
failures.push('commands/pan');
|
|
2311
2374
|
}
|
|
2312
2375
|
|
|
2313
|
-
//
|
|
2314
|
-
//
|
|
2315
|
-
//
|
|
2376
|
+
// Claude Code discovers a skill only as a skills/<name>/SKILL.md directory:
|
|
2377
|
+
// its skills documentation lists no flat-file form, and the loader it ships
|
|
2378
|
+
// (2.1.280, read 2026-09-23) reads <skills>/<entry>/SKILL.md for every entry
|
|
2379
|
+
// and drops anything else. So the flat skills/pan-*.md shims v2.10.0 (E-5)
|
|
2380
|
+
// wrote here were never loaded. Nothing replaces them: the commands above
|
|
2381
|
+
// already reach the model the way skills do (a command file and a skill
|
|
2382
|
+
// "both create /deploy and work the same way", code.claude.com/docs/en/skills).
|
|
2383
|
+
// The sweep removes the shims older installs left, and the skill
|
|
2384
|
+
// directories a previous --unified-skills install mirrored, so no command
|
|
2385
|
+
// resolves twice (reality check 2026-09-22, R33).
|
|
2316
2386
|
if (runtime === 'claude') {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
// Upgrade path: sweep stale pan-* shims before regenerating, mirroring
|
|
2321
|
-
// the stale-cleanup copyFlattenedCommands does for command trees.
|
|
2322
|
-
// A rename/removal in commands/pan would otherwise leave orphan shims.
|
|
2323
|
-
for (const file of fs.readdirSync(skillsDir)) {
|
|
2324
|
-
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
2325
|
-
try { fs.unlinkSync(path.join(skillsDir, file)); } catch (err) { pushInstallWarning('staleCleanup', file, err); }
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
const workflowsDir = path.join(targetDir, 'workflows');
|
|
2329
|
-
if (fs.existsSync(workflowsDir)) {
|
|
2330
|
-
for (const file of fs.readdirSync(workflowsDir)) {
|
|
2331
|
-
if (file.startsWith('pan-') && file.endsWith('.js')) {
|
|
2332
|
-
try { fs.unlinkSync(path.join(workflowsDir, file)); } catch (err) { pushInstallWarning('staleCleanup', file, err); }
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
let shimCount = 0;
|
|
2337
|
-
for (const file of fs.readdirSync(panDest)) {
|
|
2338
|
-
if (!file.endsWith('.md')) continue;
|
|
2339
|
-
const commandName = file.slice(0, -3);
|
|
2340
|
-
const commandBody = fs.readFileSync(path.join(panDest, file), 'utf-8');
|
|
2341
|
-
const description = lib.extractFrontmatterField(commandBody, 'description')
|
|
2342
|
-
|| `PAN command: ${commandName}`;
|
|
2343
|
-
const shim = buildClaudeSkillShim({ commandName, description });
|
|
2344
|
-
fs.writeFileSync(path.join(skillsDir, `pan-${commandName}.md`), shim, 'utf-8');
|
|
2345
|
-
shimCount += 1;
|
|
2346
|
-
}
|
|
2347
|
-
if (shimCount > 0) {
|
|
2348
|
-
console.log(` ${green}✓${reset} Registered ${shimCount} commands as skills/pan-*.md`);
|
|
2349
|
-
}
|
|
2350
|
-
} catch (e) {
|
|
2351
|
-
console.error(` ${yellow}⚠${reset} Skill shim registration skipped: ${e.message}`);
|
|
2387
|
+
const swept = sweepClaudePanSkills(targetDir);
|
|
2388
|
+
if (swept > 0) {
|
|
2389
|
+
console.log(` ${green}✓${reset} Removed ${swept} legacy PAN entries from skills/`);
|
|
2352
2390
|
}
|
|
2353
2391
|
}
|
|
2354
2392
|
}
|
|
@@ -2588,6 +2626,14 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2588
2626
|
try {
|
|
2589
2627
|
const workflowsDir = path.join(targetDir, 'workflows');
|
|
2590
2628
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
2629
|
+
// Upgrade path: a script renamed or removed since the last install would
|
|
2630
|
+
// otherwise survive the rewrite below. (This sweep used to sit inside the
|
|
2631
|
+
// retired skill-shim step, so --unified-skills installs never ran it.)
|
|
2632
|
+
for (const file of fs.readdirSync(workflowsDir)) {
|
|
2633
|
+
if (file.startsWith('pan-') && file.endsWith('.js')) {
|
|
2634
|
+
try { fs.unlinkSync(path.join(workflowsDir, file)); } catch (err) { pushInstallWarning('staleCleanup', file, err); }
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2591
2637
|
const scripts = lib.buildNativeWorkflowScripts();
|
|
2592
2638
|
for (const { name, content } of scripts) {
|
|
2593
2639
|
fs.writeFileSync(path.join(workflowsDir, name), content);
|
|
@@ -2609,7 +2655,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2609
2655
|
|
|
2610
2656
|
// AGENTS.md universal rules layer (ADR-0028 Phase 3): contribute one
|
|
2611
2657
|
// marker-fenced PAN section to the project's AGENTS.md (read natively by
|
|
2612
|
-
//
|
|
2658
|
+
// Codex, OpenCode and Copilot), and bridge CLAUDE.md to it via @AGENTS.md for the
|
|
2613
2659
|
// Claude runtime. Project-scoped — local installs only; user content
|
|
2614
2660
|
// outside the markers is never touched.
|
|
2615
2661
|
if (!isGlobal) {
|
|
@@ -2695,6 +2741,9 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2695
2741
|
const stopGuardCommand = isGlobal
|
|
2696
2742
|
? buildHookCommand(targetDir, 'pan-stop-guard.js')
|
|
2697
2743
|
: 'node ' + dirName + '/hooks/pan-stop-guard.js';
|
|
2744
|
+
const stateReinjectCommand = isGlobal
|
|
2745
|
+
? buildHookCommand(targetDir, 'pan-state-reinject.js')
|
|
2746
|
+
: 'node ' + dirName + '/hooks/pan-state-reinject.js';
|
|
2698
2747
|
|
|
2699
2748
|
if (isCodex) {
|
|
2700
2749
|
// Codex hooks (2026-06): Claude-compatible PascalCase events in the shared
|
|
@@ -2706,13 +2755,13 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2706
2755
|
let existing = null;
|
|
2707
2756
|
try { existing = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch { /* absent or invalid — start fresh */ }
|
|
2708
2757
|
const merged = lib.mergeCodexHooksConfig(existing, {
|
|
2709
|
-
updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand,
|
|
2758
|
+
updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand, stateReinjectCommand,
|
|
2710
2759
|
});
|
|
2711
2760
|
fs.writeFileSync(hooksJsonPath, JSON.stringify(merged, null, 2) + '\n');
|
|
2712
2761
|
// Print the path we actually wrote. The hardcoded '.codex/hooks.json' was
|
|
2713
2762
|
// wrong for --global and for --config-dir, telling users to inspect a file
|
|
2714
2763
|
// that does not exist while the real one sat elsewhere.
|
|
2715
|
-
console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksJsonPath)}: update check, context monitor, cost + trace loggers)`);
|
|
2764
|
+
console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksJsonPath)}: update check, context monitor, cost + trace loggers, stop guard, state re-injection)`);
|
|
2716
2765
|
} catch (e) {
|
|
2717
2766
|
pushInstallWarning('codexHooks', 'hooks.json', e);
|
|
2718
2767
|
}
|
|
@@ -2724,12 +2773,12 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2724
2773
|
// from any legacy config.json registration.
|
|
2725
2774
|
if (isCopilot) {
|
|
2726
2775
|
const hooksConfigPath = path.join(targetDir, 'hooks', 'pan.json');
|
|
2727
|
-
const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand });
|
|
2776
|
+
const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand });
|
|
2728
2777
|
try {
|
|
2729
2778
|
fs.mkdirSync(path.dirname(hooksConfigPath), { recursive: true });
|
|
2730
2779
|
fs.writeFileSync(hooksConfigPath, JSON.stringify(hooksConfig, null, 2) + '\n');
|
|
2731
2780
|
// Same as the Codex case: a global Copilot install writes to ~/.copilot/, not .github/.
|
|
2732
|
-
console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksConfigPath)}: update check, context monitor, cost + trace loggers)`);
|
|
2781
|
+
console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksConfigPath)}: update check, context monitor, cost + trace loggers, stop guard)`);
|
|
2733
2782
|
} catch (e) {
|
|
2734
2783
|
console.error(` ${yellow}✗${reset} Failed to write Copilot hooks config: ${e.message}`);
|
|
2735
2784
|
}
|
|
@@ -2780,7 +2829,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2780
2829
|
}
|
|
2781
2830
|
|
|
2782
2831
|
// Configure statusline and hooks in settings.json
|
|
2783
|
-
// Claude Code
|
|
2832
|
+
// Claude Code and Gemini use settings.json (OpenCode's is unused; its config is opencode.json)
|
|
2784
2833
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
2785
2834
|
const rawSettings = readSettings(settingsPath);
|
|
2786
2835
|
if (settingsUnusable(rawSettings, settingsPath, 'statusline and hook configuration')) {
|
|
@@ -2801,112 +2850,60 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2801
2850
|
}
|
|
2802
2851
|
}
|
|
2803
2852
|
|
|
2804
|
-
//
|
|
2853
|
+
// Hooks in settings.json (Claude Code, Gemini CLI; OpenCode has no hook system).
|
|
2854
|
+
// Every event name comes from HOOK_EVENT_MAP, which carries each runtime's own
|
|
2855
|
+
// vocabulary. Until 2026-09-23 these blocks wrote Claude's names for both
|
|
2856
|
+
// runtimes, and Gemini CLI skips every key outside its own set with an "Invalid
|
|
2857
|
+
// hook event name" warning — so on Gemini only SessionStart ever ran (R29).
|
|
2805
2858
|
if (!isOpencode) {
|
|
2806
2859
|
if (!settings.hooks) {
|
|
2807
2860
|
settings.hooks = {};
|
|
2808
2861
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
]
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
// v3.4+: SubagentStop hook for automatic cost logging.
|
|
2851
|
-
// Gemini + OpenCode may not implement SubagentStop; we still register
|
|
2852
|
-
// the entry — hosts that don't fire the event simply never trigger it.
|
|
2853
|
-
if (!settings.hooks.SubagentStop) {
|
|
2854
|
-
settings.hooks.SubagentStop = [];
|
|
2855
|
-
}
|
|
2856
|
-
const hasCostLoggerHook = settings.hooks.SubagentStop.some(entry =>
|
|
2857
|
-
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-cost-logger'))
|
|
2858
|
-
);
|
|
2859
|
-
if (!hasCostLoggerHook) {
|
|
2860
|
-
settings.hooks.SubagentStop.push({
|
|
2861
|
-
hooks: [
|
|
2862
|
-
{
|
|
2863
|
-
type: 'command',
|
|
2864
|
-
command: costLoggerCommand
|
|
2865
|
-
}
|
|
2866
|
-
]
|
|
2867
|
-
});
|
|
2868
|
-
console.log(` ${green}✓${reset} Configured cost logger hook`);
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
// v3.5+: SubagentStop hook for circular optimization tracing.
|
|
2872
|
-
// Logs agent completion events to the active trace session (if one is running).
|
|
2873
|
-
const hasTraceLoggerHook = settings.hooks.SubagentStop.some(entry =>
|
|
2874
|
-
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-trace-logger'))
|
|
2875
|
-
);
|
|
2876
|
-
if (!hasTraceLoggerHook) {
|
|
2877
|
-
settings.hooks.SubagentStop.push({
|
|
2878
|
-
hooks: [
|
|
2879
|
-
{
|
|
2880
|
-
type: 'command',
|
|
2881
|
-
command: traceLoggerCommand
|
|
2882
|
-
}
|
|
2883
|
-
]
|
|
2884
|
-
});
|
|
2885
|
-
console.log(` ${green}✓${reset} Configured trace logger hook`);
|
|
2862
|
+
const events = lib.HOOK_EVENT_MAP[runtime] || {};
|
|
2863
|
+
// One row per PAN hook: the table slot it serves, its command, the log label.
|
|
2864
|
+
// The SubagentStop pair stays two separate entries so a host can run them
|
|
2865
|
+
// independently; P-1809's stop guard blocks a stop ONCE when workflow
|
|
2866
|
+
// autonomy is armed, state.md records no failure, and the roadmap has
|
|
2867
|
+
// unbuilt phases — fail-open and inert outside PAN projects.
|
|
2868
|
+
const registrations = [
|
|
2869
|
+
{ slot: 'sessionStart', hook: 'pan-check-update', command: updateCheckCommand, label: 'update check hook' },
|
|
2870
|
+
{ slot: 'postToolUse', hook: 'pan-context-monitor', command: contextMonitorCommand, label: 'context window monitor hook' },
|
|
2871
|
+
{ slot: 'subagentStop', hook: 'pan-cost-logger', command: costLoggerCommand, label: 'cost logger hook' },
|
|
2872
|
+
{ slot: 'subagentStop', hook: 'pan-trace-logger', command: traceLoggerCommand, label: 'trace logger hook' },
|
|
2873
|
+
{ slot: 'stop', hook: 'pan-stop-guard', command: stopGuardCommand, label: 'auto-advance stop guard hook' },
|
|
2874
|
+
{ slot: 'compact', hook: 'pan-state-reinject', command: stateReinjectCommand, label: 'state re-injection hook (after compaction)' },
|
|
2875
|
+
];
|
|
2876
|
+
const unsupported = [];
|
|
2877
|
+
for (const r of registrations) {
|
|
2878
|
+
const event = events[r.slot] || null;
|
|
2879
|
+
// Upgrade path: an entry for this hook under any other event is dead (a key
|
|
2880
|
+
// the runtime does not have) or a stale duplicate. Keyed on the script, so a
|
|
2881
|
+
// hook that moved event is cleaned wherever an older install put it.
|
|
2882
|
+
const removedFrom = lib.stripPanHookEntries(settings.hooks, [r.hook], event);
|
|
2883
|
+
if (removedFrom.length > 0) {
|
|
2884
|
+
console.log(` ${green}✓${reset} Removed the ${r.label} from ${removedFrom.join(', ')}${event ? '' : ` (${runtime} has no event for it)`}`);
|
|
2885
|
+
}
|
|
2886
|
+
if (!event) {
|
|
2887
|
+
unsupported.push(r.hook);
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
if (!Array.isArray(settings.hooks[event])) {
|
|
2891
|
+
settings.hooks[event] = [];
|
|
2892
|
+
}
|
|
2893
|
+
const registered = settings.hooks[event].some(entry =>
|
|
2894
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes(r.hook))
|
|
2895
|
+
);
|
|
2896
|
+
if (!registered) {
|
|
2897
|
+
const matcher = lib.HOOK_SLOT_MATCHERS[r.slot];
|
|
2898
|
+
settings.hooks[event].push({ ...(matcher ? { matcher } : {}), hooks: [{ type: 'command', command: r.command }] });
|
|
2899
|
+
console.log(` ${green}✓${reset} Configured ${r.label}`);
|
|
2900
|
+
}
|
|
2886
2901
|
}
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
// Blocks a session stop ONCE when workflow.auto_advance is armed, state.md
|
|
2890
|
-
// says "ready to plan", and the roadmap has unbuilt phases — the exact
|
|
2891
|
-
// boundary-drop fingerprint from the 2026-08 field runs. Fail-open and
|
|
2892
|
-
// inert outside PAN projects; hosts that never fire Stop never trigger it
|
|
2893
|
-
// (same convention as the SubagentStop registrations above).
|
|
2894
|
-
if (!settings.hooks.Stop) {
|
|
2895
|
-
settings.hooks.Stop = [];
|
|
2902
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
2903
|
+
delete settings.hooks;
|
|
2896
2904
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
);
|
|
2900
|
-
if (!hasStopGuardHook) {
|
|
2901
|
-
settings.hooks.Stop.push({
|
|
2902
|
-
hooks: [
|
|
2903
|
-
{
|
|
2904
|
-
type: 'command',
|
|
2905
|
-
command: stopGuardCommand
|
|
2906
|
-
}
|
|
2907
|
-
]
|
|
2908
|
-
});
|
|
2909
|
-
console.log(` ${green}✓${reset} Configured auto-advance stop guard hook`);
|
|
2905
|
+
if (isGemini && unsupported.length > 0) {
|
|
2906
|
+
console.log(` ${dim}ℹ Gemini CLI has no context-window metric for hooks, no subagent-completion event and no post-compaction event that adds context, so ${unsupported.join(', ')} ${unsupported.length === 1 ? 'is' : 'are'} not registered there${reset}`);
|
|
2910
2907
|
}
|
|
2911
2908
|
}
|
|
2912
2909
|
|
|
@@ -2927,7 +2924,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
2927
2924
|
const isCodex = runtime === 'codex';
|
|
2928
2925
|
const isCopilot = runtime === 'copilot';
|
|
2929
2926
|
|
|
2930
|
-
|
|
2927
|
+
// Gemini CLI has no statusline command: its footer shows built-in items only
|
|
2928
|
+
// (gemini-cli packages/cli/src/config/settingsSchema.ts, read 2026-09-23), so the
|
|
2929
|
+
// `statusLine` block PAN wrote into .gemini/settings.json was never read — and
|
|
2930
|
+
// without it the context monitor has no metrics there either (R29). Remove
|
|
2931
|
+
// PAN's own copy on upgrade; a user's custom block is not PAN's to touch.
|
|
2932
|
+
if (runtime === 'gemini' && settings.statusLine && typeof settings.statusLine.command === 'string'
|
|
2933
|
+
&& /pan-statusline/.test(settings.statusLine.command)) {
|
|
2934
|
+
delete settings.statusLine;
|
|
2935
|
+
console.log(` ${green}✓${reset} Removed the statusline block from Gemini settings (Gemini CLI has no statusline command)`);
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
if (shouldInstallStatusline && !isOpencode && !isCodex && runtime !== 'gemini') {
|
|
2931
2939
|
// Preserve a user's EXISTING custom statusline in THIS runtime's settings.
|
|
2932
2940
|
// finishInstall runs per runtime, so this is the per-runtime check the old
|
|
2933
2941
|
// primary-only guard skipped — it clobbered custom Gemini/Copilot
|
|
@@ -3022,8 +3030,17 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
3022
3030
|
if (runtime === 'opencode') command = '/pan-new-project';
|
|
3023
3031
|
if (runtime === 'codex') command = '$pan-new-project';
|
|
3024
3032
|
if (runtime === 'copilot') command = '/pan-new-project';
|
|
3033
|
+
// Under --unified-skills the nested /pan:<name> tree is swept: Claude Code gets
|
|
3034
|
+
// the hyphenated skill names from its skills/ copy (R32), and Gemini CLI has no
|
|
3035
|
+
// per-skill slash command at all — the model activates a skill when a request
|
|
3036
|
+
// matches its description (geminicli.com/docs/cli/skills, read 2026-09-23).
|
|
3037
|
+
if (unifiedSkills && runtime === 'claude') command = '/pan-new-project';
|
|
3038
|
+
let nextStep = `run ${cyan}${command}${reset}`;
|
|
3039
|
+
if (unifiedSkills && runtime === 'gemini') {
|
|
3040
|
+
nextStep = `ask it to start a new PAN project (skills activate on request; ${cyan}/skills list${reset} shows them)`;
|
|
3041
|
+
}
|
|
3025
3042
|
console.log(`
|
|
3026
|
-
${green}Done!${reset} Open a blank directory in ${program} and
|
|
3043
|
+
${green}Done!${reset} Open a blank directory in ${program} and ${nextStep}.
|
|
3027
3044
|
|
|
3028
3045
|
${cyan}Join the community:${reset} https://discord.gg/pan-wizard
|
|
3029
3046
|
`);
|
|
@@ -3145,7 +3162,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
3145
3162
|
results.push(result);
|
|
3146
3163
|
}
|
|
3147
3164
|
|
|
3148
|
-
|
|
3165
|
+
// Gemini CLI has no statusline command (see finishInstall), so it is not offered one.
|
|
3166
|
+
const statuslineRuntimes = ['claude', 'copilot'];
|
|
3149
3167
|
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
|
|
3150
3168
|
|
|
3151
3169
|
const finalize = (shouldInstallStatusline) => {
|
package/commands/pan/army.md
CHANGED
|
@@ -85,14 +85,14 @@ Every cap the conductor enforces applies to the campaign, scaled up:
|
|
|
85
85
|
| `--source` | `backlog` | Work selection (delegates to focus-auto): `backlog` = ranked roadmap/requirements items; `scan` = category code-scan. |
|
|
86
86
|
| `--max-cycles` | 5 | Mission items landed before stopping. |
|
|
87
87
|
| `--total-budget` | 300 | Cumulative point budget. **Advisory by default** — tracked/surfaced but not a hard stop unless `--enforce-budget` / config `budget.enforce: true`. |
|
|
88
|
-
| `--enforce-budget` | off | Make
|
|
88
|
+
| `--enforce-budget` | off | Make `--total-budget` a hard stop again (also settable via config `budget.enforce: true`). With `--schedule`, also arm the campaign with `pan-tools campaign schedule --enforce-budget`, so `--daily-budget` pauses the day's run once spent. |
|
|
89
89
|
| `--verify-reserve` | 0.15 | Fraction of `--total-budget` (0–0.5) held back for the final Quality re-review so it can't be starved (config `budget.verify_reserve`). Advisory by default (surfaced as `into_verify_reserve`); under `--enforce-budget` it stops taking on new missions early (`budget_reserve_reached`) and the reserved points fund the closing `--clean-seal` re-verification before the last INTEGRATE. |
|
|
90
90
|
| `--squads` | all | Restrict to a subset, e.g. `--squads architecture,build,quality`. |
|
|
91
91
|
| `--no-build-worktrees` | off | Build in the main tree instead of branch-per-agent worktrees (small/serial projects). |
|
|
92
92
|
| `--push` | off | Push approved merges to origin (still human-gated). |
|
|
93
93
|
| `--clean-seal` | off | One clean build + full verification after the last item (commands from config). |
|
|
94
94
|
| `--schedule` | off | Arm a self-resuming campaign at this cadence (`hourly`/`daily`/`weekly`/`Nh`/`Nd`) instead of running once — writes the schedule descriptor (ADR-0034). Pair with `--daily-budget`. |
|
|
95
|
-
| `--daily-budget` | 300 | Per-day point budget for a scheduled campaign. Advisory by default (an indicator of the day's spend); it
|
|
95
|
+
| `--daily-budget` | 300 | Per-day point budget for a scheduled campaign. Advisory by default (an indicator of the day's spend); it pauses the day's run only when the schedule is armed with `--enforce-budget` (`enforce_budget: true` in `schedule.json`). |
|
|
96
96
|
| `--dry-run` | off | Plan + squad delegation preview only; STOP. |
|
|
97
97
|
| `--continue` / `--stop` / `--status` | — | Resume / halt / report from `.planning/orchestration/` + focus-auto state. |
|
|
98
98
|
|
|
@@ -163,10 +163,10 @@ PAN is not a daemon — it cannot wake itself while the session is closed. `--sc
|
|
|
163
163
|
- **Arm:** `/pan:army "<goal>" --schedule daily --daily-budget 200` writes `.planning/orchestration/schedule.json` (cadence, daily budget, next-due) instead of running once.
|
|
164
164
|
- **The trigger (you wire one):** a host scheduler (Claude Code routines / cron / scheduled-tasks) or a `/loop` runs `pan-tools campaign due` and, when it reports due, invokes `/pan:army --continue`. On next session open, a due campaign is surfaced as a nudge.
|
|
165
165
|
- **Resume (`--continue`):** read the schedule + `.planning/orchestration/` + focus-auto state. If `campaign due` is true and the day's `--daily-budget` isn't spent, run the next mission(s), then `campaign record-run` (advances next-due, accrues the day's spend). If not due or budget-spent, report next-due and STOP.
|
|
166
|
-
- **Bounded spend:** point budgets (`--total-budget`, `--daily-budget`) are **advisory indicators by default** — they're tracked and surfaced, not hard stops
|
|
166
|
+
- **Bounded spend:** point budgets (`--total-budget`, `--daily-budget`) are **advisory indicators by default** — they're tracked and surfaced, not hard stops. `--enforce-budget` (or config `budget.enforce`) makes `--total-budget` a hard stop; a scheduled campaign's `--daily-budget` pauses the day's run only when the schedule was armed with `--enforce-budget` (`campaign schedule --enforce-budget`; `--advisory-budget` turns it back off). The real bounds are `--max-cycles`, the conductor caps, the abort file, and the human merge gate at every integrate. A scheduled campaign runs the backlog down to staged, reviewed, green PRs over days.
|
|
167
167
|
- **Verify reserve:** a fraction of `--total-budget` (`--verify-reserve`, default 0.15) is held back so the closing Quality re-review isn't starved. Surfaced always via `campaign status` / `focus auto --status` (`into_verify_reserve`, `new_work_budget_remaining`); under `--enforce-budget` the run stops taking on new missions early (`budget_reserve_reached`) and spends the reserve on the final `--clean-seal` verification before the last INTEGRATE.
|
|
168
168
|
|
|
169
|
-
Manage it: `pan-tools campaign status` (active/paused, spent today, next-due), `campaign schedule --pause` / `--resume` / `--disable
|
|
169
|
+
Manage it: `pan-tools campaign status` (active/paused, spent today, next-due), `campaign schedule --pause` / `--resume` / `--disable`, and `--enforce-budget` / `--advisory-budget` for the daily budget.
|
|
170
170
|
|
|
171
171
|
---
|
|
172
172
|
|