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.
Files changed (50) hide show
  1. package/README.md +40 -40
  2. package/bin/install-lib.cjs +186 -83
  3. package/bin/install.js +213 -195
  4. package/commands/pan/army.md +4 -4
  5. package/commands/pan/audit-deployment.md +2 -2
  6. package/commands/pan/cost.md +5 -5
  7. package/commands/pan/exec-phase.md +2 -0
  8. package/commands/pan/focus-auto.md +5 -5
  9. package/commands/pan/verify-phase.md +8 -9
  10. package/hooks/dist/pan-check-update.js +53 -2
  11. package/hooks/dist/pan-context-monitor.js +41 -0
  12. package/hooks/dist/pan-cost-logger.js +71 -2
  13. package/hooks/dist/pan-state-reinject.js +158 -0
  14. package/hooks/dist/pan-stop-guard.js +127 -2
  15. package/hooks/dist/pan-trace-logger.js +42 -1
  16. package/package.json +1 -1
  17. package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
  18. package/pan-wizard-core/bin/lib/commands.cjs +20 -4
  19. package/pan-wizard-core/bin/lib/config.cjs +2 -0
  20. package/pan-wizard-core/bin/lib/constants.cjs +3 -1
  21. package/pan-wizard-core/bin/lib/context-budget.cjs +27 -2
  22. package/pan-wizard-core/bin/lib/core.cjs +249 -27
  23. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
  24. package/pan-wizard-core/bin/lib/cost.cjs +234 -88
  25. package/pan-wizard-core/bin/lib/frontmatter.cjs +5 -2
  26. package/pan-wizard-core/bin/lib/memory-optimize.cjs +12 -6
  27. package/pan-wizard-core/bin/lib/memory-rebuild.cjs +12 -5
  28. package/pan-wizard-core/bin/lib/phase-report.cjs +4 -1
  29. package/pan-wizard-core/bin/lib/roadmap.cjs +87 -18
  30. package/pan-wizard-core/bin/lib/state.cjs +86 -22
  31. package/pan-wizard-core/bin/lib/suggest.cjs +21 -9
  32. package/pan-wizard-core/bin/lib/template.cjs +6 -3
  33. package/pan-wizard-core/bin/lib/verify-deploy.cjs +20 -3
  34. package/pan-wizard-core/bin/pan-tools.cjs +10 -1
  35. package/pan-wizard-core/mcp/tool-registry.cjs +14 -4
  36. package/pan-wizard-core/references/model-profiles.md +4 -4
  37. package/pan-wizard-core/references/planning-config.md +19 -23
  38. package/pan-wizard-core/workflows/exec-phase.md +8 -0
  39. package/pan-wizard-core/workflows/help.md +2 -2
  40. package/pan-wizard-core/workflows/phase-tests.md +1 -1
  41. package/pan-wizard-core/workflows/progress.md +1 -1
  42. package/pan-wizard-core/workflows/settings.md +2 -4
  43. package/pan-wizard-core/workflows/todo-add.md +1 -1
  44. package/pan-wizard-core/workflows/todo-check.md +7 -6
  45. package/scripts/build-agent-plugin.js +4 -0
  46. package/scripts/build-hooks.js +2 -1
  47. package/scripts/build-plugin.js +10 -0
  48. package/scripts/npm-dist-tag.js +47 -0
  49. package/scripts/plugin-path.js +6 -1
  50. 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, buildClaudeSkillShim, stripThinkingFrontmatter,
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
- const skillsDir = path.join(targetDir, 'skills');
626
- try {
627
- for (const file of fs.readdirSync(skillsDir)) {
628
- if (file.startsWith('pan-') && file.endsWith('.md')) {
629
- try { fs.unlinkSync(path.join(skillsDir, file)); } catch {}
630
- }
631
- }
632
- } catch { /* dir absent — nothing to sweep */ }
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/pan-*.md shim files (registered at install time)
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 skillsDir = path.join(targetDir, 'skills');
1081
- if (fs.existsSync(skillsDir)) {
1082
- let skillCount = 0;
1083
- for (const file of fs.readdirSync(skillsDir)) {
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.23+)
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 native skill shims: flat skills/pan-*.md files (E-5). Only the Claude
1881
- // runtime's nested-commands install writes these; under --unified-skills the
1882
- // shared .agents/skills tree above already covers skills. Tracking them lets
1883
- // verifyInstall catch silent shim-write failures and saveLocalPatches back up
1884
- // user edits — the codex/copilot SKILL.md branch above matches directories, not
1885
- // these flat files.
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
- if (fs.existsSync(claudeSkillsDir)) {
1889
- for (const file of fs.readdirSync(claudeSkillsDir)) {
1890
- if (file.startsWith('pan-') && file.endsWith('.md')) {
1891
- manifest.files['skills/' + file] = fileHash(path.join(claudeSkillsDir, file));
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
- const currentHash = fileHash(fullPath);
1968
- if (currentHash !== originalHash) {
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
- // E-5: Claude native skill shims — register each PAN command as a skill
2314
- // so Claude Code's native skill discovery surfaces them. Gemini doesn't
2315
- // use the skills/ directory, so only generate for Claude.
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
- try {
2318
- const skillsDir = path.join(targetDir, 'skills');
2319
- fs.mkdirSync(skillsDir, { recursive: true });
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
- // every PAN runtime), and bridge CLAUDE.md to it via @AGENTS.md for the
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, Gemini, OpenCode use settings.json
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
- // Configure SessionStart hook for update checking (skip for opencode)
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
- if (!settings.hooks.SessionStart) {
2810
- settings.hooks.SessionStart = [];
2811
- }
2812
-
2813
- const hasPanUpdateHook = settings.hooks.SessionStart.some(entry =>
2814
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-check-update'))
2815
- );
2816
-
2817
- if (!hasPanUpdateHook) {
2818
- settings.hooks.SessionStart.push({
2819
- hooks: [
2820
- {
2821
- type: 'command',
2822
- command: updateCheckCommand
2823
- }
2824
- ]
2825
- });
2826
- console.log(` ${green}✓${reset} Configured update check hook`);
2827
- }
2828
-
2829
- // Configure PostToolUse hook for context window monitoring
2830
- if (!settings.hooks.PostToolUse) {
2831
- settings.hooks.PostToolUse = [];
2832
- }
2833
-
2834
- const hasContextMonitorHook = settings.hooks.PostToolUse.some(entry =>
2835
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-context-monitor'))
2836
- );
2837
-
2838
- if (!hasContextMonitorHook) {
2839
- settings.hooks.PostToolUse.push({
2840
- hooks: [
2841
- {
2842
- type: 'command',
2843
- command: contextMonitorCommand
2844
- }
2845
- ]
2846
- });
2847
- console.log(` ${green}✓${reset} Configured context window monitor hook`);
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
- // v3.23+ (P-1809): Stop hook guarding the auto-advance phase boundary.
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
- const hasStopGuardHook = settings.hooks.Stop.some(entry =>
2898
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-stop-guard'))
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
- if (shouldInstallStatusline && !isOpencode && !isCodex) {
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 run ${cyan}${command}${reset}.
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
- const statuslineRuntimes = ['claude', 'gemini', 'copilot'];
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) => {
@@ -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 the point budgets hard stops again (also settable via config `budget.enforce: true`). |
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 only pauses the day's run when `enforce_budget: true` is set by hand in the schedule descriptor (`schedule.json`); no flag or config key sets 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, unless `budget.enforce` / `--enforce-budget` is set. 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.
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