pan-wizard 3.28.0 → 3.30.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 (40) hide show
  1. package/README.md +39 -39
  2. package/bin/install-lib.cjs +65 -58
  3. package/bin/install.js +199 -188
  4. package/commands/pan/army.md +2 -2
  5. package/commands/pan/audit-deployment.md +2 -2
  6. package/commands/pan/cost.md +19 -7
  7. package/commands/pan/exec-phase.md +2 -0
  8. package/commands/pan/focus-auto.md +5 -5
  9. package/hooks/dist/pan-check-update.js +4 -0
  10. package/hooks/dist/pan-cost-logger.js +322 -43
  11. package/hooks/dist/pan-stop-guard.js +81 -2
  12. package/hooks/dist/pan-trace-logger.js +275 -32
  13. package/package.json +4 -1
  14. package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
  15. package/pan-wizard-core/bin/lib/commands.cjs +3 -1
  16. package/pan-wizard-core/bin/lib/constants.cjs +17 -0
  17. package/pan-wizard-core/bin/lib/context-budget.cjs +10 -0
  18. package/pan-wizard-core/bin/lib/core.cjs +17 -4
  19. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +511 -0
  20. package/pan-wizard-core/bin/lib/cost.cjs +165 -55
  21. package/pan-wizard-core/bin/lib/git.cjs +5 -1
  22. package/pan-wizard-core/bin/lib/hud.cjs +5 -3
  23. package/pan-wizard-core/bin/lib/hygiene.cjs +22 -25
  24. package/pan-wizard-core/bin/lib/memory-rebuild.cjs +3 -3
  25. package/pan-wizard-core/bin/lib/memory.cjs +14 -8
  26. package/pan-wizard-core/bin/lib/optimize.cjs +78 -2
  27. package/pan-wizard-core/bin/lib/utils.cjs +22 -0
  28. package/pan-wizard-core/bin/lib/verify-deploy.cjs +1 -1
  29. package/pan-wizard-core/bin/lib/verify.cjs +24 -10
  30. package/pan-wizard-core/bin/pan-tools.cjs +8 -1
  31. package/pan-wizard-core/references/model-profiles.md +4 -4
  32. package/pan-wizard-core/references/planning-config.md +19 -23
  33. package/pan-wizard-core/workflows/health.md +1 -0
  34. package/pan-wizard-core/workflows/settings.md +2 -4
  35. package/scripts/coverage-gate.cjs +257 -0
  36. package/scripts/install-git-hooks.js +5 -0
  37. package/scripts/mutation-probe.cjs +272 -0
  38. package/scripts/release-check.js +33 -12
  39. package/scripts/test-quality-lint.cjs +240 -0
  40. package/scripts/test-surface.cjs +336 -0
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
  }
@@ -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
  }
@@ -2187,7 +2237,7 @@ function install(isGlobal, runtime = 'claude') {
2187
2237
  // OpenCode uses commands/ (flat), Codex uses skills/, Claude/Gemini use commands/pan/
2188
2238
  try {
2189
2239
  if (unifiedSkills) {
2190
- // ADR-0028 Phase 1: every runtime consumes one runtime-neutral
2240
+ // ADR-0028 Phase 1: every runtime consumes one runtime-neutral (Claude via the skills/ mirror below)
2191
2241
  // .agents/skills/ tree; the proprietary command surface is swept so
2192
2242
  // commands don't resolve twice.
2193
2243
  const skillsDir = getCodexSkillsRoot(isGlobal);
@@ -2233,6 +2283,17 @@ function install(isGlobal, runtime = 'claude') {
2233
2283
  } else {
2234
2284
  failures.push('.agents/skills/pan-* (unified)');
2235
2285
  }
2286
+
2287
+ // Claude Code is the one target runtime that does not read the shared tree
2288
+ // (R32): without this copy the sweep above leaves it with no PAN commands.
2289
+ if (runtime === 'claude') {
2290
+ const mirrored = mirrorUnifiedSkillsForClaude(skillsDir, targetDir, 'pan');
2291
+ if (mirrored > 0 && mirrored === installedSkillNames.length) {
2292
+ console.log(` ${green}✓${reset} Mirrored ${mirrored} unified skills to skills/ (Claude Code does not read .agents/skills/)`);
2293
+ } else {
2294
+ failures.push('skills/pan-* (Claude copy of the unified tree)');
2295
+ }
2296
+ }
2236
2297
  } else if (isOpencode) {
2237
2298
  // OpenCode: flat structure in commands/ directory. Plural since
2238
2299
  // OpenCode 2026 releases — singular command/ is back-compat only.
@@ -2310,45 +2371,20 @@ function install(isGlobal, runtime = 'claude') {
2310
2371
  failures.push('commands/pan');
2311
2372
  }
2312
2373
 
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.
2374
+ // Claude Code discovers a skill only as a skills/<name>/SKILL.md directory:
2375
+ // its skills documentation lists no flat-file form, and the loader it ships
2376
+ // (2.1.280, read 2026-09-23) reads <skills>/<entry>/SKILL.md for every entry
2377
+ // and drops anything else. So the flat skills/pan-*.md shims v2.10.0 (E-5)
2378
+ // wrote here were never loaded. Nothing replaces them: the commands above
2379
+ // already reach the model the way skills do (a command file and a skill
2380
+ // "both create /deploy and work the same way", code.claude.com/docs/en/skills).
2381
+ // The sweep removes the shims older installs left, and the skill
2382
+ // directories a previous --unified-skills install mirrored, so no command
2383
+ // resolves twice (reality check 2026-09-22, R33).
2316
2384
  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}`);
2385
+ const swept = sweepClaudePanSkills(targetDir);
2386
+ if (swept > 0) {
2387
+ console.log(` ${green}✓${reset} Removed ${swept} legacy PAN entries from skills/`);
2352
2388
  }
2353
2389
  }
2354
2390
  }
@@ -2588,6 +2624,14 @@ function install(isGlobal, runtime = 'claude') {
2588
2624
  try {
2589
2625
  const workflowsDir = path.join(targetDir, 'workflows');
2590
2626
  fs.mkdirSync(workflowsDir, { recursive: true });
2627
+ // Upgrade path: a script renamed or removed since the last install would
2628
+ // otherwise survive the rewrite below. (This sweep used to sit inside the
2629
+ // retired skill-shim step, so --unified-skills installs never ran it.)
2630
+ for (const file of fs.readdirSync(workflowsDir)) {
2631
+ if (file.startsWith('pan-') && file.endsWith('.js')) {
2632
+ try { fs.unlinkSync(path.join(workflowsDir, file)); } catch (err) { pushInstallWarning('staleCleanup', file, err); }
2633
+ }
2634
+ }
2591
2635
  const scripts = lib.buildNativeWorkflowScripts();
2592
2636
  for (const { name, content } of scripts) {
2593
2637
  fs.writeFileSync(path.join(workflowsDir, name), content);
@@ -2609,7 +2653,7 @@ function install(isGlobal, runtime = 'claude') {
2609
2653
 
2610
2654
  // AGENTS.md universal rules layer (ADR-0028 Phase 3): contribute one
2611
2655
  // 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
2656
+ // Codex, OpenCode and Copilot), and bridge CLAUDE.md to it via @AGENTS.md for the
2613
2657
  // Claude runtime. Project-scoped — local installs only; user content
2614
2658
  // outside the markers is never touched.
2615
2659
  if (!isGlobal) {
@@ -2780,7 +2824,7 @@ function install(isGlobal, runtime = 'claude') {
2780
2824
  }
2781
2825
 
2782
2826
  // Configure statusline and hooks in settings.json
2783
- // Claude Code, Gemini, OpenCode use settings.json
2827
+ // Claude Code and Gemini use settings.json (OpenCode's is unused; its config is opencode.json)
2784
2828
  const settingsPath = path.join(targetDir, 'settings.json');
2785
2829
  const rawSettings = readSettings(settingsPath);
2786
2830
  if (settingsUnusable(rawSettings, settingsPath, 'statusline and hook configuration')) {
@@ -2801,112 +2845,58 @@ function install(isGlobal, runtime = 'claude') {
2801
2845
  }
2802
2846
  }
2803
2847
 
2804
- // Configure SessionStart hook for update checking (skip for opencode)
2848
+ // Hooks in settings.json (Claude Code, Gemini CLI; OpenCode has no hook system).
2849
+ // Every event name comes from HOOK_EVENT_MAP, which carries each runtime's own
2850
+ // vocabulary. Until 2026-09-23 these blocks wrote Claude's names for both
2851
+ // runtimes, and Gemini CLI skips every key outside its own set with an "Invalid
2852
+ // hook event name" warning — so on Gemini only SessionStart ever ran (R29).
2805
2853
  if (!isOpencode) {
2806
2854
  if (!settings.hooks) {
2807
2855
  settings.hooks = {};
2808
2856
  }
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`);
2857
+ const events = lib.HOOK_EVENT_MAP[runtime] || {};
2858
+ // One row per PAN hook: the table slot it serves, its command, the log label.
2859
+ // The SubagentStop pair stays two separate entries so a host can run them
2860
+ // independently; P-1809's stop guard blocks a stop ONCE when workflow
2861
+ // autonomy is armed, state.md records no failure, and the roadmap has
2862
+ // unbuilt phases — fail-open and inert outside PAN projects.
2863
+ const registrations = [
2864
+ { slot: 'sessionStart', hook: 'pan-check-update', command: updateCheckCommand, label: 'update check hook' },
2865
+ { slot: 'postToolUse', hook: 'pan-context-monitor', command: contextMonitorCommand, label: 'context window monitor hook' },
2866
+ { slot: 'subagentStop', hook: 'pan-cost-logger', command: costLoggerCommand, label: 'cost logger hook' },
2867
+ { slot: 'subagentStop', hook: 'pan-trace-logger', command: traceLoggerCommand, label: 'trace logger hook' },
2868
+ { slot: 'stop', hook: 'pan-stop-guard', command: stopGuardCommand, label: 'auto-advance stop guard hook' },
2869
+ ];
2870
+ const unsupported = [];
2871
+ for (const r of registrations) {
2872
+ const event = events[r.slot] || null;
2873
+ // Upgrade path: an entry for this hook under any other event is dead (a key
2874
+ // the runtime does not have) or a stale duplicate. Keyed on the script, so a
2875
+ // hook that moved event is cleaned wherever an older install put it.
2876
+ const removedFrom = lib.stripPanHookEntries(settings.hooks, [r.hook], event);
2877
+ if (removedFrom.length > 0) {
2878
+ console.log(` ${green}✓${reset} Removed the ${r.label} from ${removedFrom.join(', ')}${event ? '' : ` (${runtime} has no event for it)`}`);
2879
+ }
2880
+ if (!event) {
2881
+ unsupported.push(r.hook);
2882
+ continue;
2883
+ }
2884
+ if (!Array.isArray(settings.hooks[event])) {
2885
+ settings.hooks[event] = [];
2886
+ }
2887
+ const registered = settings.hooks[event].some(entry =>
2888
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes(r.hook))
2889
+ );
2890
+ if (!registered) {
2891
+ settings.hooks[event].push({ hooks: [{ type: 'command', command: r.command }] });
2892
+ console.log(` ${green}✓${reset} Configured ${r.label}`);
2893
+ }
2886
2894
  }
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 = [];
2895
+ if (Object.keys(settings.hooks).length === 0) {
2896
+ delete settings.hooks;
2896
2897
  }
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`);
2898
+ if (isGemini && unsupported.length > 0) {
2899
+ console.log(` ${dim}ℹ Gemini CLI has no context-window metric for hooks and no subagent-completion event, so ${unsupported.join(', ')} ${unsupported.length === 1 ? 'is' : 'are'} not registered there${reset}`);
2910
2900
  }
2911
2901
  }
2912
2902
 
@@ -2927,7 +2917,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
2927
2917
  const isCodex = runtime === 'codex';
2928
2918
  const isCopilot = runtime === 'copilot';
2929
2919
 
2930
- if (shouldInstallStatusline && !isOpencode && !isCodex) {
2920
+ // Gemini CLI has no statusline command: its footer shows built-in items only
2921
+ // (gemini-cli packages/cli/src/config/settingsSchema.ts, read 2026-09-23), so the
2922
+ // `statusLine` block PAN wrote into .gemini/settings.json was never read — and
2923
+ // without it the context monitor has no metrics there either (R29). Remove
2924
+ // PAN's own copy on upgrade; a user's custom block is not PAN's to touch.
2925
+ if (runtime === 'gemini' && settings.statusLine && typeof settings.statusLine.command === 'string'
2926
+ && /pan-statusline/.test(settings.statusLine.command)) {
2927
+ delete settings.statusLine;
2928
+ console.log(` ${green}✓${reset} Removed the statusline block from Gemini settings (Gemini CLI has no statusline command)`);
2929
+ }
2930
+
2931
+ if (shouldInstallStatusline && !isOpencode && !isCodex && runtime !== 'gemini') {
2931
2932
  // Preserve a user's EXISTING custom statusline in THIS runtime's settings.
2932
2933
  // finishInstall runs per runtime, so this is the per-runtime check the old
2933
2934
  // primary-only guard skipped — it clobbered custom Gemini/Copilot
@@ -3022,8 +3023,17 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
3022
3023
  if (runtime === 'opencode') command = '/pan-new-project';
3023
3024
  if (runtime === 'codex') command = '$pan-new-project';
3024
3025
  if (runtime === 'copilot') command = '/pan-new-project';
3026
+ // Under --unified-skills the nested /pan:<name> tree is swept: Claude Code gets
3027
+ // the hyphenated skill names from its skills/ copy (R32), and Gemini CLI has no
3028
+ // per-skill slash command at all — the model activates a skill when a request
3029
+ // matches its description (geminicli.com/docs/cli/skills, read 2026-09-23).
3030
+ if (unifiedSkills && runtime === 'claude') command = '/pan-new-project';
3031
+ let nextStep = `run ${cyan}${command}${reset}`;
3032
+ if (unifiedSkills && runtime === 'gemini') {
3033
+ nextStep = `ask it to start a new PAN project (skills activate on request; ${cyan}/skills list${reset} shows them)`;
3034
+ }
3025
3035
  console.log(`
3026
- ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
3036
+ ${green}Done!${reset} Open a blank directory in ${program} and ${nextStep}.
3027
3037
 
3028
3038
  ${cyan}Join the community:${reset} https://discord.gg/pan-wizard
3029
3039
  `);
@@ -3145,7 +3155,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
3145
3155
  results.push(result);
3146
3156
  }
3147
3157
 
3148
- const statuslineRuntimes = ['claude', 'gemini', 'copilot'];
3158
+ // Gemini CLI has no statusline command (see finishInstall), so it is not offered one.
3159
+ const statuslineRuntimes = ['claude', 'copilot'];
3149
3160
  const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
3150
3161
 
3151
3162
  const finalize = (shouldInstallStatusline) => {
@@ -85,7 +85,7 @@ 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`); `--daily-budget` is enforced only by `enforce_budget: true` in `schedule.json`. |
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). |
@@ -163,7 +163,7 @@ 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 `enforce_budget: true` is set by hand in `schedule.json`. 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
169
  Manage it: `pan-tools campaign status` (active/paused, spent today, next-due), `campaign schedule --pause` / `--resume` / `--disable`.
@@ -77,7 +77,7 @@ For the detected runtime config directory (CONFIG_DIR), audit ALL of the followi
77
77
  - [ ] `CONFIG_DIR/pan-wizard-core/references/` — non-empty, every manifest-listed reference present
78
78
 
79
79
  **1.4 Commands**
80
- - For Claude/Gemini: `CONFIG_DIR/commands/pan/` — every manifest-listed command file present
80
+ - For Claude/Gemini: `CONFIG_DIR/commands/pan/` — every manifest-listed command file present (a `--unified-skills` install has none: check `./.agents/skills/pan-*/SKILL.md`, and for Claude also `CONFIG_DIR/skills/pan-*/SKILL.md`)
81
81
  - For OpenCode: `CONFIG_DIR/commands/` — every manifest-listed `pan-*.md` present (flat, one file per command)
82
82
  - For Copilot: `CONFIG_DIR/skills/pan-*/SKILL.md` — every manifest-listed skill directory present
83
83
  - For Codex: the SHARED skills tree, not `CONFIG_DIR` — `./.agents/skills/pan-*/SKILL.md` for a local
@@ -95,7 +95,7 @@ For the detected runtime config directory (CONFIG_DIR), audit ALL of the followi
95
95
 
96
96
  **1.7 Settings/Config**
97
97
  - [ ] Settings file exists (settings.json / opencode.json / config.json)
98
- - [ ] Hooks are registered in settings (SessionStart, PostToolUse, statusLine)
98
+ - [ ] Hooks are registered — Claude `settings.json`: SessionStart, PostToolUse, SubagentStop, Stop, statusLine; Gemini `settings.json`: SessionStart and AfterAgent only (no statusLine); Codex: `.codex/hooks.json`; Copilot: `.github/hooks/pan.json`
99
99
  - [ ] Hook commands point to existing files
100
100
 
101
101
  **1.8 Manifest Integrity**
@@ -2,7 +2,7 @@
2
2
  name: pan:cost
3
3
  group: Observability
4
4
  description: Show token usage and estimated cost across PAN commands and agents
5
- argument-hint: "[report|append|clear] [--format json|table|chart] [--since YYYY-MM-DD] [--until YYYY-MM-DD]"
5
+ argument-hint: "[report|append|clear|rebuild] [--format json|table|chart] [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--apply] [--no-main-thread]"
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Bash
@@ -45,7 +45,10 @@ pan-tools cost report [--format json|table|chart] [--since YYYY-MM-DD] [--until
45
45
  "cache_read_tokens": 50000,
46
46
  "cache_write_tokens": 5000,
47
47
  "cost_usd": 2.1234,
48
- "cost_unknown": 0
48
+ "cost_unknown": 0,
49
+ "suspect_excluded": 0,
50
+ "empty_excluded": 0,
51
+ "malformed_skipped": 0
49
52
  },
50
53
  "cache_hit_rate_pct": 40.5,
51
54
  "by_agent": { "pan-planner": { "calls": 8, "input": 50000, ... } },
@@ -78,6 +81,15 @@ Delete the cost log. Useful at the start of a billing cycle.
78
81
  pan-tools cost clear
79
82
  ```
80
83
 
84
+ ### `rebuild`
85
+
86
+ Rebuild the ledger from Claude Code's own transcripts (session file plus the per-agent files under `<session>/subagents/`, Workflow-tool subagents one level down). Rows written by hooks before v3.29 booked a slice of the parent session to whichever subagent stopped and counted turns once per content block; the rebuild replaces them with one exact row per agent transcript, typed from the main thread's `Agent` calls, plus one row per session for the main thread's own usage (`--no-main-thread` omits it). Rows whose session transcript is gone, and caller-appended rows, are kept. Dry-run by default — show the user the per-session before → after (and any `warnings`), then apply only on their say-so; the previous ledger is kept beside the new one as `tokens.jsonl.rebuilt-<date>` (a later copy never overwrites an earlier one). Run it **before** `/pan:hygiene --apply` on a poisoned ledger: quarantine moves the whole file aside, and a rebuild afterwards has no rows left to keep. The main-thread row is dated to the session's last record, so `--since`/`--until` windows and the per-day view see a session's own usage on its final day.
87
+
88
+ ```
89
+ pan-tools cost rebuild # dry run
90
+ pan-tools cost rebuild --apply # write the rebuilt ledger
91
+ ```
92
+
81
93
  </subcommands>
82
94
 
83
95
  <rate_table>
@@ -111,7 +123,7 @@ When a record has neither a known model nor a known tier, its cost is `null` and
111
123
 
112
124
  <instrumentation_note>
113
125
 
114
- Token records are written by any caller that knows its usage — typically the host runtime or a wrapper. PAN ships the log format + aggregator (this command); the capture hook itself is opt-in (Wave 5 of Spec B v2). Until then, records can be appended manually via `pan-tools cost append` or by external scripts reading the provider API.
126
+ Token records are written by any caller that knows its usage — typically the host runtime or a wrapper. PAN ships the log format, this aggregator, and the `pan-cost-logger` capture hook, which the installer registers on Claude Code, Codex and Copilot CLI. Elsewhere, records can be appended manually via `pan-tools cost append` or by external scripts reading the provider API.
115
127
 
116
128
  If `.planning/metrics/tokens.jsonl` is empty, `/pan:cost` returns zero totals — the feature is inert, not broken.
117
129
 
@@ -122,10 +134,10 @@ If `.planning/metrics/tokens.jsonl` is empty, `/pan:cost` returns zero totals
122
134
  | Runtime | Support |
123
135
  |---------|---------|
124
136
  | Claude Code | Full — data format + aggregation + all output formats |
125
- | OpenCode | Full aggregator; token capture depends on OpenCode's own hooks |
126
- | Gemini | Full aggregator; token capture depends on Gemini CLI instrumentation |
127
- | Codex | Full aggregator; token capture via external script |
128
- | Copilot CLI | Full aggregator; Copilot doesn't currently expose per-call usage |
137
+ | OpenCode | Full aggregator; PAN registers no hooks on OpenCode, so records come from `pan-tools cost append` or an external script |
138
+ | Gemini | Full aggregator; PAN registers no cost hook on Gemini CLI (no subagent-completion event), so records come from `pan-tools cost append` or an external script |
139
+ | Codex | Full — `pan-cost-logger` registered on `SubagentStop` in `.codex/hooks.json` |
140
+ | Copilot CLI | Full aggregator; `pan-cost-logger` registered on `subagentStop` in `.github/hooks/pan.json` (token counts depend on what the payload carries) |
129
141
 
130
142
  The aggregator is runtime-agnostic. What varies across runtimes is how records *get into* `tokens.jsonl` in the first place.
131
143
 
@@ -117,6 +117,8 @@ Next: Wave {N+1} — {task count} tasks [{task IDs}]
117
117
  ```
118
118
  This prevents drift in multi-wave phases where the agent loses track of which waves remain and what the test baseline was.
119
119
 
120
+ **Task tracking:** where your runtime offers a todo or task-tracking tool, you may keep the wave list in it. Do not depend on one: some runtimes offer none on their newest models (Claude Code gates its task tools behind `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` there). The attention anchor above, the wave summaries and state.md are the record either way.
121
+
120
122
  **State Intent Before Implementing (M+ tasks):**
121
123
  For each STANDARD or FULL task, state before coding: "I will modify [files], adding [what], to achieve [goal]. Risk: [what could break]."
122
124