pan-wizard 3.8.0 → 3.12.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 (72) hide show
  1. package/README.md +80 -9
  2. package/agents/pan-conductor.md +15 -3
  3. package/agents/pan-counterfactual.md +1 -2
  4. package/agents/pan-debugger.md +1 -2
  5. package/agents/pan-distiller.md +1 -2
  6. package/agents/pan-document_code.md +1 -0
  7. package/agents/pan-executor.md +1 -0
  8. package/agents/pan-experiment-runner.md +1 -2
  9. package/agents/pan-hardener.md +1 -2
  10. package/agents/pan-integration-checker.md +1 -2
  11. package/agents/pan-knowledge.md +1 -2
  12. package/agents/pan-meta-reviewer.md +1 -2
  13. package/agents/pan-optimizer.md +1 -0
  14. package/agents/pan-phase-researcher.md +1 -0
  15. package/agents/pan-plan-checker.md +1 -2
  16. package/agents/pan-planner.md +1 -0
  17. package/agents/pan-previewer.md +1 -2
  18. package/agents/pan-project-researcher.md +6 -0
  19. package/agents/pan-release.md +58 -0
  20. package/agents/pan-research-synthesizer.md +7 -0
  21. package/agents/pan-reviewer.md +2 -3
  22. package/agents/pan-roadmapper.md +1 -0
  23. package/agents/pan-verifier.md +1 -2
  24. package/assets/pan-avatar.png +0 -0
  25. package/assets/pan-developer.png +0 -0
  26. package/assets/pan-docs-header.png +0 -0
  27. package/assets/pan-hero.png +0 -0
  28. package/assets/pan-logo-2000-transparent.svg +11 -30
  29. package/assets/pan-logo-2000.svg +12 -43
  30. package/assets/pan-logo-lockup.svg +11 -0
  31. package/assets/pan-mark.svg +7 -0
  32. package/assets/pan-orchestration.png +0 -0
  33. package/assets/pan-readme-hero.png +0 -0
  34. package/assets/terminal.svg +39 -119
  35. package/bin/install-lib.cjs +661 -46
  36. package/bin/install.js +722 -116
  37. package/commands/pan/army.md +169 -0
  38. package/commands/pan/dashboard.md +25 -0
  39. package/commands/pan/experiment.md +2 -0
  40. package/commands/pan/focus-auto.md +32 -4
  41. package/commands/pan/hud.md +91 -0
  42. package/commands/pan/profile.md +2 -0
  43. package/hooks/dist/pan-cost-logger.js +22 -7
  44. package/package.json +5 -4
  45. package/pan-wizard-core/bin/lib/campaign.cjs +198 -0
  46. package/pan-wizard-core/bin/lib/commands-learnings.cjs +544 -0
  47. package/pan-wizard-core/bin/lib/commands.cjs +12 -523
  48. package/pan-wizard-core/bin/lib/constants.cjs +8 -0
  49. package/pan-wizard-core/bin/lib/core.cjs +80 -0
  50. package/pan-wizard-core/bin/lib/cost.cjs +62 -8
  51. package/pan-wizard-core/bin/lib/focus.cjs +13 -1
  52. package/pan-wizard-core/bin/lib/git.cjs +6 -1
  53. package/pan-wizard-core/bin/lib/hud.cjs +887 -0
  54. package/pan-wizard-core/bin/lib/lock.cjs +108 -0
  55. package/pan-wizard-core/bin/lib/milestone.cjs +3 -2
  56. package/pan-wizard-core/bin/lib/phase-remove.cjs +392 -0
  57. package/pan-wizard-core/bin/lib/phase.cjs +4 -369
  58. package/pan-wizard-core/bin/lib/runner.cjs +5 -0
  59. package/pan-wizard-core/bin/lib/squads.cjs +152 -0
  60. package/pan-wizard-core/bin/lib/state.cjs +10 -1
  61. package/pan-wizard-core/bin/lib/verify-deploy.cjs +181 -0
  62. package/pan-wizard-core/bin/lib/verify-drift.cjs +255 -0
  63. package/pan-wizard-core/bin/lib/verify-preflight.cjs +261 -0
  64. package/pan-wizard-core/bin/lib/verify-retro.cjs +177 -0
  65. package/pan-wizard-core/bin/lib/verify.cjs +10 -797
  66. package/pan-wizard-core/bin/lib/worktree.cjs +123 -0
  67. package/pan-wizard-core/bin/pan-tools.cjs +78 -0
  68. package/pan-wizard-core/learnings/universal/autonomous-loop.md +56 -0
  69. package/pan-wizard-core/workflows/plan-phase.md +11 -0
  70. package/scripts/build-plugin.js +105 -0
  71. package/scripts/install-git-hooks.js +64 -0
  72. package/scripts/release-check.js +13 -2
package/bin/install.js CHANGED
@@ -18,9 +18,13 @@ const {
18
18
  convertClaudeToOpencodeFrontmatter, convertClaudeToGeminiToml, convertClaudeToGeminiAgent,
19
19
  rewriteAskUserQuestionForCopilot, stripSubTags,
20
20
  getCodexSkillAdapterHeader, convertClaudeCommandToCodexSkill,
21
+ convertClaudeCommandToUnifiedSkill,
21
22
  getCopilotSkillAdapterHeader, convertClaudeCommandToCopilotSkill, convertClaudeToCopilotAgent,
22
23
  processAttribution, parseJsonc,
23
24
  detectModelCapabilities, buildClaudeSkillShim, stripThinkingFrontmatter,
25
+ geminiTransitionNotice,
26
+ convertClaudeAgentToCodexToml, codexTrustNotice,
27
+ buildCopilotHooksConfig,
24
28
  } = lib;
25
29
 
26
30
  // Colors
@@ -104,6 +108,18 @@ function getOpencodeGlobalDir() {
104
108
  * @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
105
109
  * @param {string|null} explicitDir - Explicit directory from --config-dir flag
106
110
  */
111
+ /**
112
+ * Codex skills root. Codex reads skills from the shared `.agents/skills/`
113
+ * tree — repo scope `$REPO_ROOT/.agents/skills`, user scope `~/.agents/skills`.
114
+ * `$CODEX_HOME/skills` is NOT a documented read location (verified against
115
+ * developers.openai.com/codex/skills, 2026-06).
116
+ */
117
+ function getCodexSkillsRoot(isGlobal) {
118
+ return isGlobal
119
+ ? path.join(os.homedir(), '.agents', 'skills')
120
+ : path.join(process.cwd(), '.agents', 'skills');
121
+ }
122
+
107
123
  function getGlobalDir(runtime, explicitDir = null) {
108
124
  if (runtime === 'opencode') {
109
125
  // For OpenCode, --config-dir overrides env vars
@@ -195,12 +211,15 @@ function parseConfigDirArg() {
195
211
  const explicitConfigDir = parseConfigDirArg();
196
212
  const hasHelp = args.includes('--help') || args.includes('-h');
197
213
  const forceStatusline = args.includes('--force-statusline');
214
+ // ADR-0028 Phase 1 (opt-in): compile commands once into the shared
215
+ // .agents/skills/ tree for every runtime instead of per-runtime command trees.
216
+ const unifiedSkills = args.includes('--unified-skills');
198
217
 
199
218
  console.log(banner);
200
219
 
201
220
  // Show help if requested
202
221
  if (hasHelp) {
203
- 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}--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\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`);
222
+ 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}--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`);
204
223
  process.exit(0);
205
224
  }
206
225
 
@@ -266,8 +285,13 @@ function getCommitAttribution(runtime) {
266
285
  result = settings.attribution.commit;
267
286
  }
268
287
  } else if (runtime === 'copilot') {
269
- // Copilot CLI: check config.json for attribution setting
270
- const config = readSettings(path.join(getGlobalDir('copilot', explicitConfigDir), 'config.json'));
288
+ // Copilot CLI: user-editable settings live in settings.json; config.json is
289
+ // legacy (auto-migrated by the CLI, now internal state) — fall back for old installs
290
+ const copilotDir = getGlobalDir('copilot', explicitConfigDir);
291
+ let config = readSettings(path.join(copilotDir, 'settings.json'));
292
+ if (!config.attribution) {
293
+ config = readSettings(path.join(copilotDir, 'config.json'));
294
+ }
271
295
  if (!config.attribution || config.attribution.commit === undefined) {
272
296
  result = undefined;
273
297
  } else if (config.attribution.commit === '') {
@@ -290,11 +314,11 @@ function getCommitAttribution(runtime) {
290
314
 
291
315
  /**
292
316
  * Copy commands to a flat structure for OpenCode
293
- * OpenCode expects: command/pan-help.md (invoked as /pan-help)
317
+ * OpenCode expects: commands/pan-help.md (invoked as /pan-help)
294
318
  * Source structure: commands/pan/help.md
295
- *
319
+ *
296
320
  * @param {string} srcDir - Source directory (e.g., commands/pan/)
297
- * @param {string} destDir - Destination directory (e.g., command/)
321
+ * @param {string} destDir - Destination directory (e.g., commands/)
298
322
  * @param {string} prefix - Prefix for filenames (e.g., 'pan')
299
323
  * @param {string} pathPrefix - Path prefix for file references
300
324
  * @param {string} runtime - Target runtime ('claude' or 'opencode')
@@ -308,11 +332,11 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
308
332
  if (fs.existsSync(destDir)) {
309
333
  for (const file of fs.readdirSync(destDir)) {
310
334
  if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
311
- try { fs.unlinkSync(path.join(destDir, file)); } catch {}
335
+ try { fs.unlinkSync(path.join(destDir, file)); } catch (err) { pushInstallWarning('staleCleanup', file, err); }
312
336
  }
313
337
  }
314
338
  } else {
315
- try { fs.mkdirSync(destDir, { recursive: true }); } catch {}
339
+ try { fs.mkdirSync(destDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', destDir, err); }
316
340
  }
317
341
 
318
342
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
@@ -360,13 +384,13 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
360
384
  return;
361
385
  }
362
386
 
363
- try { fs.mkdirSync(skillsDir, { recursive: true }); } catch {}
387
+ try { fs.mkdirSync(skillsDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', skillsDir, err); }
364
388
 
365
389
  // Remove previous PAN Codex skills to avoid stale command skills.
366
390
  const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
367
391
  for (const entry of existing) {
368
392
  if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
369
- try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
393
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch (err) { pushInstallWarning('staleCleanup', entry.name, err); }
370
394
  }
371
395
  }
372
396
 
@@ -409,6 +433,187 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
409
433
  recurse(srcDir, prefix);
410
434
  }
411
435
 
436
+ /**
437
+ * Copy PAN commands as runtime-neutral unified skills (ADR-0028 Phase 1).
438
+ * Same tree layout as the Codex path (.agents/skills/pan-{name}/SKILL.md) but
439
+ * the content carries the runtime-neutral adapter so every runtime can consume it.
440
+ * Core references resolve against the shared .agents/pan-wizard-core/ copy
441
+ * (Phase 2), so the compiled skills are identical regardless of which runtime
442
+ * installed them — only the rare agent-file references stay runtime-local.
443
+ */
444
+ function copyCommandsAsUnifiedSkills(srcDir, skillsDir, prefix, pathPrefix, corePrefix, runtime) {
445
+ if (!fs.existsSync(srcDir)) {
446
+ return;
447
+ }
448
+
449
+ try { fs.mkdirSync(skillsDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', skillsDir, err); }
450
+
451
+ // Remove previous PAN skills to avoid stale command skills.
452
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
453
+ for (const entry of existing) {
454
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
455
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch (err) { pushInstallWarning('staleCleanup', entry.name, err); }
456
+ }
457
+ }
458
+
459
+ function recurse(currentSrcDir, currentPrefix) {
460
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
461
+
462
+ for (const entry of entries) {
463
+ const srcPath = path.join(currentSrcDir, entry.name);
464
+ if (entry.isDirectory()) {
465
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
466
+ continue;
467
+ }
468
+
469
+ if (!entry.name.endsWith('.md')) {
470
+ continue;
471
+ }
472
+
473
+ const baseName = entry.name.replace('.md', '');
474
+ const skillName = `${currentPrefix}-${baseName}`;
475
+ const skillDir = path.join(skillsDir, skillName);
476
+ fs.mkdirSync(skillDir, { recursive: true });
477
+
478
+ let content = fs.readFileSync(srcPath, 'utf8');
479
+ // Core + agent-definition references → shared .agents/ copies (specific,
480
+ // before the generic rewrites); everything else .claude-scoped → the
481
+ // installing runtime. Agent refs point at the canonical reference copies
482
+ // shipped with the shared core — the runtime's own agents dir may carry
483
+ // a different format (Codex TOML, Copilot .agent.md).
484
+ content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
485
+ content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
486
+ content = content.replace(/~\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
487
+ content = content.replace(/\.\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
488
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
489
+ content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
490
+ // Not every runtime puts a `pan-tools` bin on PATH — invoke via node.
491
+ const panToolsPath = `${corePrefix}pan-wizard-core/bin/pan-tools.cjs`;
492
+ content = content.replace(/\bpan-tools\b(?=\s+[a-z])/g, `node ${panToolsPath}`);
493
+ content = processAttribution(content, getCommitAttribution(runtime));
494
+ content = convertClaudeCommandToUnifiedSkill(content, skillName);
495
+
496
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
497
+ }
498
+ }
499
+
500
+ recurse(srcDir, prefix);
501
+ }
502
+
503
+ /**
504
+ * Remove a runtime's proprietary command surface after a --unified-skills
505
+ * install so commands don't resolve twice (ADR-0028 Phase 1 sweep).
506
+ */
507
+ function sweepProprietaryCommandSurfaces(targetDir, runtime) {
508
+ if (runtime === 'opencode') {
509
+ for (const dirName of ['commands', 'command']) {
510
+ const commandDir = path.join(targetDir, dirName);
511
+ try {
512
+ for (const file of fs.readdirSync(commandDir)) {
513
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
514
+ try { fs.unlinkSync(path.join(commandDir, file)); } catch {}
515
+ }
516
+ }
517
+ } catch { /* dir absent — nothing to sweep */ }
518
+ }
519
+ } else if (runtime === 'copilot') {
520
+ const skillsDir = path.join(targetDir, 'skills');
521
+ try {
522
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
523
+ if (entry.isDirectory() && entry.name.startsWith('pan-')) {
524
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
525
+ }
526
+ }
527
+ } catch { /* dir absent — nothing to sweep */ }
528
+ } else if (runtime === 'codex') {
529
+ // Legacy .codex/skills location (dead read path; swept on upgrade anyway)
530
+ const legacySkillsDir = path.join(targetDir, 'skills');
531
+ try {
532
+ for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
533
+ if (entry.isDirectory() && entry.name.startsWith('pan-')) {
534
+ try { fs.rmSync(path.join(legacySkillsDir, entry.name), { recursive: true }); } catch {}
535
+ }
536
+ }
537
+ } catch { /* dir absent — nothing to sweep */ }
538
+ } else {
539
+ // Claude Code & Gemini: nested commands/pan tree (+ Claude skill shims)
540
+ try { fs.rmSync(path.join(targetDir, 'commands', 'pan'), { recursive: true }); } catch {}
541
+ if (runtime === 'claude') {
542
+ const skillsDir = path.join(targetDir, 'skills');
543
+ try {
544
+ for (const file of fs.readdirSync(skillsDir)) {
545
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
546
+ try { fs.unlinkSync(path.join(skillsDir, file)); } catch {}
547
+ }
548
+ }
549
+ } catch { /* dir absent — nothing to sweep */ }
550
+ }
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Copy pan-wizard-core into the shared .agents/ root for unified installs
556
+ * (ADR-0028 Phase 2). Core-internal references resolve against the shared
557
+ * prefix, so the copy is runtime-neutral and rewrites across runtimes are
558
+ * idempotent; the few non-core references (agent files, cache paths) resolve
559
+ * against the installing runtime. Skill mentions are normalized to the
560
+ * neutral /pan-{name} form to match the unified SKILL.md content.
561
+ */
562
+ function copySharedCore(srcDir, destDir, corePrefix, runtimePathPrefix, runtime) {
563
+ try {
564
+ if (fs.existsSync(destDir)) {
565
+ fs.rmSync(destDir, { recursive: true });
566
+ }
567
+ fs.mkdirSync(destDir, { recursive: true });
568
+ } catch (err) {
569
+ throw new Error(`copySharedCore: cannot prepare ${destDir}: ${err.message}`);
570
+ }
571
+
572
+ const dirName = getDirName(runtime);
573
+
574
+ (function recurse(currentSrc, currentDest) {
575
+ for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
576
+ const srcPath = path.join(currentSrc, entry.name);
577
+ const destPath = path.join(currentDest, entry.name);
578
+
579
+ if (entry.isDirectory()) {
580
+ try { fs.mkdirSync(destPath, { recursive: true }); } catch (err) { pushInstallWarning('copySharedCore(mkdir)', destPath, err); }
581
+ recurse(srcPath, destPath);
582
+ } else if (entry.name.endsWith('.md')) {
583
+ try {
584
+ let content = fs.readFileSync(srcPath, 'utf8');
585
+ content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
586
+ content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
587
+ // Agent-definition refs → the canonical reference copies in the
588
+ // shared core (runtime agents dirs carry runtime-specific formats).
589
+ content = content.replace(/~\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
590
+ content = content.replace(/\.\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
591
+ content = content.replace(/~\/\.claude\//g, runtimePathPrefix);
592
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
593
+ content = processAttribution(content, getCommitAttribution(runtime));
594
+ content = convertSlashCommandsToCopilotSkillMentions(content);
595
+ fs.writeFileSync(destPath, content);
596
+ } catch (err) {
597
+ pushInstallWarning('copySharedCore(md)', destPath, err);
598
+ }
599
+ } else {
600
+ try {
601
+ fs.copyFileSync(srcPath, destPath);
602
+ } catch (err) {
603
+ pushInstallWarning('copySharedCore(copy)', destPath, err);
604
+ }
605
+ }
606
+ }
607
+ })(srcDir, destDir);
608
+
609
+ // learnings/internal is source-only — strip it like the per-runtime copy does
610
+ try {
611
+ fs.rmSync(path.join(destDir, 'learnings', 'internal'), { recursive: true, force: true });
612
+ } catch (err) {
613
+ if (err.code !== 'ENOENT') pushInstallWarning('stripInternalLearnings', 'learnings/internal', err);
614
+ }
615
+ }
616
+
412
617
  /**
413
618
  * Copy PAN commands as Copilot CLI skills.
414
619
  * Creates skills/pan-{name}/SKILL.md directory structure.
@@ -419,13 +624,13 @@ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, pathPrefix, runt
419
624
  return;
420
625
  }
421
626
 
422
- try { fs.mkdirSync(skillsDir, { recursive: true }); } catch {}
627
+ try { fs.mkdirSync(skillsDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', skillsDir, err); }
423
628
 
424
629
  // Remove previous PAN Copilot skills to avoid stale command skills
425
630
  const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
426
631
  for (const entry of existing) {
427
632
  if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
428
- try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
633
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch (err) { pushInstallWarning('staleCleanup', entry.name, err); }
429
634
  }
430
635
  }
431
636
 
@@ -670,25 +875,54 @@ function uninstall(isGlobal, runtime = 'claude') {
670
875
 
671
876
  let removedCount = 0;
672
877
 
878
+ // ADR-0028 Phase 2 ref-counting: another runtime still tracking the shared
879
+ // .agents tree (manifest keys reaching outside its config dir) keeps the
880
+ // tree alive when this runtime uninstalls.
881
+ function otherRuntimeTrackingShared(currentRuntime) {
882
+ const others = ['claude', 'codex', 'gemini', 'opencode', 'copilot']
883
+ .filter(r => r !== currentRuntime);
884
+ for (const rt of others) {
885
+ const dir = isGlobal
886
+ ? getGlobalDir(rt, null)
887
+ : path.join(process.cwd(), getDirName(rt));
888
+ try {
889
+ const m = JSON.parse(fs.readFileSync(path.join(dir, MANIFEST_NAME), 'utf8'));
890
+ if (Object.keys(m.files || {}).some(k => k.startsWith('../'))) return rt;
891
+ } catch { /* no manifest for that runtime */ }
892
+ }
893
+ return null;
894
+ }
895
+
673
896
  // 1. Remove PAN commands/skills
674
897
  if (isOpencode) {
675
- // OpenCode: remove command/pan-*.md files
676
- const commandDir = path.join(targetDir, 'command');
677
- if (fs.existsSync(commandDir)) {
678
- const files = fs.readdirSync(commandDir);
679
- for (const file of files) {
680
- if (file.startsWith('pan-') && file.endsWith('.md')) {
681
- try { fs.unlinkSync(path.join(commandDir, file)); } catch {}
682
- removedCount++;
898
+ // OpenCode: remove commands/pan-*.md files (plus legacy singular command/
899
+ // left by pre-plural installs).
900
+ for (const dirName of ['commands', 'command']) {
901
+ const commandDir = path.join(targetDir, dirName);
902
+ if (fs.existsSync(commandDir)) {
903
+ const files = fs.readdirSync(commandDir);
904
+ let removedHere = 0;
905
+ for (const file of files) {
906
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
907
+ try { fs.unlinkSync(path.join(commandDir, file)); } catch {}
908
+ removedCount++;
909
+ removedHere++;
910
+ }
911
+ }
912
+ if (removedHere > 0) {
913
+ console.log(` ${green}✓${reset} Removed PAN commands from ${dirName}/`);
683
914
  }
684
915
  }
685
- console.log(` ${green}✓${reset} Removed PAN commands from command/`);
686
916
  }
687
917
  } else if (isCodex || isCopilot) {
688
- // Codex & Copilot CLI: remove skills/pan-*/SKILL.md skill directories
689
- const skillsDir = path.join(targetDir, 'skills');
690
- if (fs.existsSync(skillsDir)) {
691
- let skillCount = 0;
918
+ // Codex & Copilot CLI: remove skills/pan-*/SKILL.md skill directories from
919
+ // the runtime-local locations. The shared .agents/skills tree (Codex's
920
+ // primary surface, plus any --unified-skills install) is handled by the
921
+ // ref-counted sweep below.
922
+ const skillDirs = [path.join(targetDir, 'skills')];
923
+ let skillCount = 0;
924
+ for (const skillsDir of skillDirs) {
925
+ if (!fs.existsSync(skillsDir)) continue;
692
926
  const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
693
927
  for (const entry of entries) {
694
928
  if (entry.isDirectory() && entry.name.startsWith('pan-')) {
@@ -696,10 +930,10 @@ function uninstall(isGlobal, runtime = 'claude') {
696
930
  skillCount++;
697
931
  }
698
932
  }
699
- if (skillCount > 0) {
700
- removedCount++;
701
- console.log(` ${green}✓${reset} Removed ${skillCount} ${isCopilot ? 'Copilot CLI' : 'Codex'} skills`);
702
- }
933
+ }
934
+ if (skillCount > 0) {
935
+ removedCount++;
936
+ console.log(` ${green}✓${reset} Removed ${skillCount} ${isCopilot ? 'Copilot CLI' : 'Codex'} skills`);
703
937
  }
704
938
  } else {
705
939
  // Claude Code & Gemini: remove commands/pan/ directory
@@ -733,6 +967,57 @@ function uninstall(isGlobal, runtime = 'claude') {
733
967
  }
734
968
  }
735
969
 
970
+ // ADR-0028 unified tree: sweep the shared .agents tree when this runtime
971
+ // tracks it (manifest ../ keys; Codex always uses it) AND no other runtime
972
+ // still does (Phase 2 ref-counting). The last tracker also removes the
973
+ // shared core and prunes empty .agents directories.
974
+ {
975
+ let manifestHasShared = false;
976
+ try {
977
+ const m = JSON.parse(fs.readFileSync(path.join(targetDir, MANIFEST_NAME), 'utf8'));
978
+ manifestHasShared = Object.keys(m.files || {}).some(k => k.startsWith('../'));
979
+ } catch { /* no manifest — nothing tracked out-of-tree */ }
980
+
981
+ if (isCodex || manifestHasShared) {
982
+ const stillTracking = otherRuntimeTrackingShared(runtime);
983
+ if (stillTracking) {
984
+ console.log(` ${dim}ℹ Shared .agents/skills left in place — still tracked by ${stillTracking}${reset}`);
985
+ } else {
986
+ const sharedDir = getCodexSkillsRoot(isGlobal);
987
+ let sharedCount = 0;
988
+ try {
989
+ for (const entry of fs.readdirSync(sharedDir, { withFileTypes: true })) {
990
+ if (entry.isDirectory() && entry.name.startsWith('pan-')) {
991
+ try { fs.rmSync(path.join(sharedDir, entry.name), { recursive: true }); } catch {}
992
+ sharedCount++;
993
+ }
994
+ }
995
+ } catch { /* shared tree absent */ }
996
+ if (sharedCount > 0) {
997
+ removedCount++;
998
+ console.log(` ${green}✓${reset} Removed ${sharedCount} unified skills from .agents/skills/`);
999
+ }
1000
+ // Shared core ships only with --unified-skills installs; remove it
1001
+ // alongside the last tracked skills sweep.
1002
+ const agentsRoot = path.dirname(sharedDir);
1003
+ const sharedCore = path.join(agentsRoot, 'pan-wizard-core');
1004
+ if (fs.existsSync(sharedCore)) {
1005
+ try {
1006
+ fs.rmSync(sharedCore, { recursive: true });
1007
+ removedCount++;
1008
+ console.log(` ${green}✓${reset} Removed shared pan-wizard-core from .agents/`);
1009
+ } catch { /* best-effort */ }
1010
+ }
1011
+ // Prune now-empty shared directories (never directories with foreign content)
1012
+ for (const dir of [sharedDir, agentsRoot]) {
1013
+ try {
1014
+ if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
1015
+ } catch { /* non-empty or absent — keep */ }
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+
736
1021
  // 2. Remove pan-wizard-core directory
737
1022
  const panDir = path.join(targetDir, 'pan-wizard-core');
738
1023
  if (fs.existsSync(panDir)) {
@@ -741,13 +1026,13 @@ function uninstall(isGlobal, runtime = 'claude') {
741
1026
  console.log(` ${green}✓${reset} Removed pan-wizard-core/`);
742
1027
  }
743
1028
 
744
- // 3. Remove PAN agents (pan-*.md files only)
1029
+ // 3. Remove PAN agents (pan-*.md / pan-*.agent.md / pan-*.toml files only)
745
1030
  const agentsDir = path.join(targetDir, 'agents');
746
1031
  if (fs.existsSync(agentsDir)) {
747
1032
  const files = fs.readdirSync(agentsDir);
748
1033
  let agentCount = 0;
749
1034
  for (const file of files) {
750
- if (file.startsWith('pan-') && file.endsWith('.md')) {
1035
+ if (file.startsWith('pan-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
751
1036
  try { fs.unlinkSync(path.join(agentsDir, file)); } catch {}
752
1037
  agentCount++;
753
1038
  }
@@ -758,10 +1043,10 @@ function uninstall(isGlobal, runtime = 'claude') {
758
1043
  }
759
1044
  }
760
1045
 
761
- // 4. Remove PAN hooks
1046
+ // 4. Remove PAN hooks (scripts + Copilot CLI hooks config file)
762
1047
  const hooksDir = path.join(targetDir, 'hooks');
763
1048
  if (fs.existsSync(hooksDir)) {
764
- 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'];
1049
+ 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.json'];
765
1050
  let hookCount = 0;
766
1051
  for (const hook of panHooks) {
767
1052
  const hookPath = path.join(hooksDir, hook);
@@ -776,6 +1061,44 @@ function uninstall(isGlobal, runtime = 'claude') {
776
1061
  }
777
1062
  }
778
1063
 
1064
+ // 4a. Remove native workflow scripts (Claude-only surface)
1065
+ const workflowsDir = path.join(targetDir, 'workflows');
1066
+ if (fs.existsSync(workflowsDir)) {
1067
+ let wfCount = 0;
1068
+ for (const file of fs.readdirSync(workflowsDir)) {
1069
+ if (file.startsWith('pan-') && file.endsWith('.js')) {
1070
+ try { fs.unlinkSync(path.join(workflowsDir, file)); } catch {}
1071
+ wfCount++;
1072
+ }
1073
+ }
1074
+ if (wfCount > 0) {
1075
+ removedCount++;
1076
+ console.log(` ${green}✓${reset} Removed ${wfCount} native workflows`);
1077
+ }
1078
+ }
1079
+
1080
+ // 4b. Codex: strip PAN entries from the shared .codex/hooks.json (foreign
1081
+ // hook registrations are preserved; the file is deleted only when nothing
1082
+ // but PAN content remained).
1083
+ if (isCodex) {
1084
+ const hooksJsonPath = path.join(targetDir, 'hooks.json');
1085
+ try {
1086
+ const rawText = fs.readFileSync(hooksJsonPath, 'utf8');
1087
+ const before = JSON.stringify(JSON.parse(rawText));
1088
+ // removeCodexPanHooks mutates its argument — parse a fresh copy for it.
1089
+ const stripped = lib.removeCodexPanHooks(JSON.parse(rawText));
1090
+ if (stripped === null) {
1091
+ fs.unlinkSync(hooksJsonPath);
1092
+ removedCount++;
1093
+ console.log(` ${green}✓${reset} Removed hooks.json (only PAN hooks remained)`);
1094
+ } else if (JSON.stringify(stripped) !== before) {
1095
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(stripped, null, 2) + '\n');
1096
+ removedCount++;
1097
+ console.log(` ${green}✓${reset} Removed PAN hooks from hooks.json`);
1098
+ }
1099
+ } catch { /* absent or unparseable — nothing to strip */ }
1100
+ }
1101
+
779
1102
  // 5. Remove PAN package.json (CommonJS mode marker)
780
1103
  const pkgJsonPath = path.join(targetDir, 'package.json');
781
1104
  if (fs.existsSync(pkgJsonPath)) {
@@ -856,6 +1179,35 @@ function uninstall(isGlobal, runtime = 'claude') {
856
1179
  // Ignore JSON parse errors
857
1180
  }
858
1181
  }
1182
+
1183
+ // Clean PAN statusline from the documented settings read paths:
1184
+ // settings.json (global ~/.copilot/) and copilot/settings.json (.github/).
1185
+ for (const { settingsPath, removableDir } of [
1186
+ { settingsPath: path.join(targetDir, 'settings.json'), removableDir: false },
1187
+ { settingsPath: path.join(targetDir, 'copilot', 'settings.json'), removableDir: true },
1188
+ ]) {
1189
+ try {
1190
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1191
+ if (settings.statusLine && settings.statusLine.command &&
1192
+ settings.statusLine.command.includes('pan-statusline')) {
1193
+ delete settings.statusLine;
1194
+ if (Object.keys(settings).length === 0) {
1195
+ fs.unlinkSync(settingsPath);
1196
+ if (removableDir) {
1197
+ // Only the nested .github/copilot/ dir PAN may have created —
1198
+ // never the user's ~/.copilot home.
1199
+ try { fs.rmdirSync(path.dirname(settingsPath)); } catch { /* non-empty — keep */ }
1200
+ }
1201
+ } else {
1202
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
1203
+ }
1204
+ console.log(` ${green}✓${reset} Removed PAN statusline from settings`);
1205
+ removedCount++;
1206
+ }
1207
+ } catch {
1208
+ // Missing file or parse error — nothing to clean
1209
+ }
1210
+ }
859
1211
  }
860
1212
 
861
1213
  // 6b. Clean up settings.json (remove PAN hooks and statusline)
@@ -916,6 +1268,27 @@ function uninstall(isGlobal, runtime = 'claude') {
916
1268
  }
917
1269
  }
918
1270
 
1271
+ // Remove PAN hooks from SubagentStop (cost logger v3.4+, trace logger v3.5+)
1272
+ if (settings.hooks && settings.hooks.SubagentStop) {
1273
+ const before = settings.hooks.SubagentStop.length;
1274
+ settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(entry => {
1275
+ if (entry.hooks && Array.isArray(entry.hooks)) {
1276
+ const hasPanHook = entry.hooks.some(h =>
1277
+ h.command && (h.command.includes('pan-cost-logger') || h.command.includes('pan-trace-logger'))
1278
+ );
1279
+ return !hasPanHook;
1280
+ }
1281
+ return true;
1282
+ });
1283
+ if (settings.hooks.SubagentStop.length < before) {
1284
+ settingsModified = true;
1285
+ console.log(` ${green}✓${reset} Removed cost/trace logger hooks from settings`);
1286
+ }
1287
+ if (settings.hooks.SubagentStop.length === 0) {
1288
+ delete settings.hooks.SubagentStop;
1289
+ }
1290
+ }
1291
+
919
1292
  // Clean up empty hooks object
920
1293
  if (settings.hooks && Object.keys(settings.hooks).length === 0) {
921
1294
  delete settings.hooks;
@@ -997,13 +1370,42 @@ function uninstall(isGlobal, runtime = 'claude') {
997
1370
  console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
998
1371
  }
999
1372
 
1373
+ // 7b. AGENTS.md / CLAUDE.md rules layer (ADR-0028 Phase 3): strip the PAN
1374
+ // marker block when this was the last PAN runtime in the project (the
1375
+ // manifest scan runs AFTER this runtime's manifest was removed above, so
1376
+ // any hit means another runtime still needs the section). Local only.
1377
+ if (!isGlobal) {
1378
+ const anotherRuntimeInstalled = ['.claude', '.codex', '.gemini', '.opencode', '.github']
1379
+ .some(d => fs.existsSync(path.join(process.cwd(), d, MANIFEST_NAME)));
1380
+ if (!anotherRuntimeInstalled) {
1381
+ for (const fileName of ['AGENTS.md', 'CLAUDE.md']) {
1382
+ const filePath = path.join(process.cwd(), fileName);
1383
+ try {
1384
+ const existing = fs.readFileSync(filePath, 'utf8');
1385
+ const stripped = lib.removeAgentsMdSection(existing);
1386
+ if (stripped === existing) continue; // no PAN block — leave untouched
1387
+ if (stripped === null) {
1388
+ fs.unlinkSync(filePath);
1389
+ console.log(` ${green}✓${reset} Removed ${fileName} (only the PAN section remained)`);
1390
+ } else {
1391
+ fs.writeFileSync(filePath, stripped);
1392
+ console.log(` ${green}✓${reset} Removed PAN section from ${fileName}`);
1393
+ }
1394
+ removedCount++;
1395
+ } catch { /* file absent — nothing to strip */ }
1396
+ }
1397
+ }
1398
+ }
1399
+
1000
1400
  // 8. Clean up empty PAN directories
1001
1401
  const dirsToClean = [
1002
1402
  path.join(targetDir, 'agents'),
1003
1403
  path.join(targetDir, 'hooks'),
1004
1404
  path.join(targetDir, 'skills'),
1405
+ path.join(targetDir, 'workflows'),
1005
1406
  path.join(targetDir, 'commands', 'pan'),
1006
1407
  path.join(targetDir, 'commands'),
1408
+ path.join(targetDir, 'command'), // legacy OpenCode singular dir
1007
1409
  ];
1008
1410
  for (const dir of dirsToClean) {
1009
1411
  try {
@@ -1183,14 +1585,23 @@ function generateManifest(dir, baseDir) {
1183
1585
  /**
1184
1586
  * Write file manifest after installation for future modification detection
1185
1587
  */
1186
- function writeManifest(configDir, runtime = 'claude') {
1588
+ function writeManifest(configDir, runtime = 'claude', isGlobal = false) {
1187
1589
  const isOpencode = runtime === 'opencode';
1188
1590
  const isCodex = runtime === 'codex';
1189
1591
  const isCopilot = runtime === 'copilot';
1190
1592
  const panDir = path.join(configDir, 'pan-wizard-core');
1191
1593
  const commandsDir = path.join(configDir, 'commands', 'pan');
1192
- const opencodeCommandDir = path.join(configDir, 'command');
1193
- const codexSkillsDir = path.join(configDir, 'skills');
1594
+ const opencodeCommandDir = path.join(configDir, 'commands');
1595
+ // Codex skills (and every runtime under --unified-skills, ADR-0028) live in
1596
+ // the shared .agents/skills tree (outside configDir); Copilot otherwise uses
1597
+ // configDir/skills. Out-of-tree manifest keys are stored relative to
1598
+ // configDir (e.g. "../.agents/skills/...") so existing
1599
+ // path.join(configDir, key) consumers resolve them.
1600
+ const skillsShared = isCodex || unifiedSkills;
1601
+ const codexSkillsDir = skillsShared ? getCodexSkillsRoot(isGlobal) : path.join(configDir, 'skills');
1602
+ const codexSkillsPrefix = skillsShared
1603
+ ? path.relative(configDir, codexSkillsDir).replace(/\\/g, '/')
1604
+ : 'skills';
1194
1605
  const agentsDir = path.join(configDir, 'agents');
1195
1606
  const manifest = { version: pkg.version, timestamp: new Date().toISOString(), files: {} };
1196
1607
 
@@ -1198,31 +1609,31 @@ function writeManifest(configDir, runtime = 'claude') {
1198
1609
  for (const [rel, hash] of Object.entries(panHashes)) {
1199
1610
  manifest.files['pan-wizard-core/' + rel] = hash;
1200
1611
  }
1201
- if (!isOpencode && !isCodex && fs.existsSync(commandsDir)) {
1612
+ if (!isOpencode && !isCodex && !unifiedSkills && fs.existsSync(commandsDir)) {
1202
1613
  const cmdHashes = generateManifest(commandsDir);
1203
1614
  for (const [rel, hash] of Object.entries(cmdHashes)) {
1204
1615
  manifest.files['commands/pan/' + rel] = hash;
1205
1616
  }
1206
1617
  }
1207
- if (isOpencode && fs.existsSync(opencodeCommandDir)) {
1618
+ if (isOpencode && !unifiedSkills && fs.existsSync(opencodeCommandDir)) {
1208
1619
  for (const file of fs.readdirSync(opencodeCommandDir)) {
1209
1620
  if (file.startsWith('pan-') && file.endsWith('.md')) {
1210
- manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
1621
+ manifest.files['commands/' + file] = fileHash(path.join(opencodeCommandDir, file));
1211
1622
  }
1212
1623
  }
1213
1624
  }
1214
- if ((isCodex || isCopilot) && fs.existsSync(codexSkillsDir)) {
1625
+ if ((isCodex || isCopilot || unifiedSkills) && fs.existsSync(codexSkillsDir)) {
1215
1626
  for (const skillName of listCodexSkillNames(codexSkillsDir)) {
1216
1627
  const skillRoot = path.join(codexSkillsDir, skillName);
1217
1628
  const skillHashes = generateManifest(skillRoot);
1218
1629
  for (const [rel, hash] of Object.entries(skillHashes)) {
1219
- manifest.files[`skills/${skillName}/${rel}`] = hash;
1630
+ manifest.files[`${codexSkillsPrefix}/${skillName}/${rel}`] = hash;
1220
1631
  }
1221
1632
  }
1222
1633
  }
1223
1634
  if (fs.existsSync(agentsDir)) {
1224
1635
  for (const file of fs.readdirSync(agentsDir)) {
1225
- if (file.startsWith('pan-') && file.endsWith('.md')) {
1636
+ if (file.startsWith('pan-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
1226
1637
  manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
1227
1638
  }
1228
1639
  }
@@ -1236,6 +1647,15 @@ function writeManifest(configDir, runtime = 'claude') {
1236
1647
  }
1237
1648
  }
1238
1649
  }
1650
+ // Track native workflow scripts (Claude-only surface)
1651
+ const workflowsDir = path.join(configDir, 'workflows');
1652
+ if (fs.existsSync(workflowsDir)) {
1653
+ for (const file of fs.readdirSync(workflowsDir)) {
1654
+ if (file.startsWith('pan-') && file.endsWith('.js')) {
1655
+ manifest.files['workflows/' + file] = fileHash(path.join(workflowsDir, file));
1656
+ }
1657
+ }
1658
+ }
1239
1659
 
1240
1660
  try {
1241
1661
  fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
@@ -1260,6 +1680,11 @@ function saveLocalPatches(configDir) {
1260
1680
  const modified = [];
1261
1681
 
1262
1682
  for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
1683
+ // Keys reaching outside configDir (Codex skills in ../.agents/skills/)
1684
+ // can't be backed up under patchesDir — path.join would collapse the
1685
+ // `..` and write outside the patches tree. Skip them; they're still
1686
+ // overwritten cleanly on reinstall.
1687
+ if (relPath.split('/').includes('..')) continue;
1263
1688
  const fullPath = path.join(configDir, relPath);
1264
1689
  if (!fs.existsSync(fullPath)) continue;
1265
1690
  const currentHash = fileHash(fullPath);
@@ -1378,31 +1803,108 @@ function install(isGlobal, runtime = 'claude') {
1378
1803
  // Clean up orphaned files from previous versions
1379
1804
  cleanupOrphanedFiles(targetDir);
1380
1805
 
1381
- // OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/pan/
1806
+ // OpenCode uses commands/ (flat), Codex uses skills/, Claude/Gemini use commands/pan/
1382
1807
  try {
1383
- if (isOpencode) {
1384
- // OpenCode: flat structure in command/ directory
1385
- const commandDir = path.join(targetDir, 'command');
1808
+ if (unifiedSkills) {
1809
+ // ADR-0028 Phase 1: every runtime consumes one runtime-neutral
1810
+ // .agents/skills/ tree; the proprietary command surface is swept so
1811
+ // commands don't resolve twice.
1812
+ const skillsDir = getCodexSkillsRoot(isGlobal);
1813
+ const agentsRoot = path.dirname(skillsDir);
1814
+ const corePrefix = isGlobal
1815
+ ? `${agentsRoot.replace(/\\/g, '/')}/`
1816
+ : './.agents/';
1817
+
1818
+ // ADR-0028 Phase 2: shared runtime-neutral core. Skills resolve
1819
+ // pan-tools against this copy regardless of which runtime installed
1820
+ // last; the per-runtime core remains for agents/hooks.
1821
+ const sharedCoreDest = path.join(agentsRoot, 'pan-wizard-core');
1822
+ copySharedCore(path.join(src, 'pan-wizard-core'), sharedCoreDest, corePrefix, pathPrefix, runtime);
1823
+ try { fs.writeFileSync(path.join(sharedCoreDest, 'VERSION'), pkg.version); } catch { /* non-fatal */ }
1824
+
1825
+ // Canonical agent-definition reference copies (ADR-0028 agent-ref
1826
+ // canonicalization): shared content references these instead of the
1827
+ // runtime's agents dir, whose files carry runtime-specific formats.
1828
+ // These are reading material for agents, not runtime registrations —
1829
+ // the per-runtime installed agents still drive subagent spawning.
1830
+ try {
1831
+ const agentsRefDir = path.join(sharedCoreDest, 'agents');
1832
+ fs.mkdirSync(agentsRefDir, { recursive: true });
1833
+ const agentsSrc = path.join(src, 'agents');
1834
+ for (const f of fs.readdirSync(agentsSrc).filter(n => n.endsWith('.md'))) {
1835
+ let content = fs.readFileSync(path.join(agentsSrc, f), 'utf8');
1836
+ content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
1837
+ content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
1838
+ content = convertSlashCommandsToCopilotSkillMentions(content);
1839
+ fs.writeFileSync(path.join(agentsRefDir, f), content);
1840
+ }
1841
+ } catch (e) {
1842
+ pushInstallWarning('unifiedAgentRefs', 'pan-wizard-core/agents', e);
1843
+ }
1844
+ console.log(` ${green}✓${reset} Installed shared pan-wizard-core to ${isGlobal ? '~/.agents/' : '.agents/'}`);
1845
+
1846
+ const panSrc = path.join(src, 'commands', 'pan');
1847
+ copyCommandsAsUnifiedSkills(panSrc, skillsDir, 'pan', pathPrefix, corePrefix, runtime);
1848
+ sweepProprietaryCommandSurfaces(targetDir, runtime);
1849
+
1850
+ const installedSkillNames = listCodexSkillNames(skillsDir);
1851
+ if (installedSkillNames.length > 0) {
1852
+ const label = isGlobal ? '~/.agents/skills/' : '.agents/skills/';
1853
+ console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} unified skills to ${label} (ADR-0028)`);
1854
+ } else {
1855
+ failures.push('.agents/skills/pan-* (unified)');
1856
+ }
1857
+ } else if (isOpencode) {
1858
+ // OpenCode: flat structure in commands/ directory. Plural since
1859
+ // OpenCode 2026 releases — singular command/ is back-compat only.
1860
+ const commandDir = path.join(targetDir, 'commands');
1386
1861
  fs.mkdirSync(commandDir, { recursive: true });
1387
1862
 
1388
- // Copy commands/pan/*.md as command/pan-*.md (flatten structure)
1863
+ // Copy commands/pan/*.md as commands/pan-*.md (flatten structure)
1389
1864
  const panSrc = path.join(src, 'commands', 'pan');
1390
1865
  copyFlattenedCommands(panSrc, commandDir, 'pan', pathPrefix, runtime);
1391
- if (verifyInstalled(commandDir, 'command/pan-*')) {
1866
+
1867
+ // Upgrade path: remove PAN files left in the legacy singular command/
1868
+ // directory by older installs so commands don't resolve twice.
1869
+ const legacyCommandDir = path.join(targetDir, 'command');
1870
+ if (fs.existsSync(legacyCommandDir)) {
1871
+ for (const file of fs.readdirSync(legacyCommandDir)) {
1872
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
1873
+ try { fs.unlinkSync(path.join(legacyCommandDir, file)); } catch {}
1874
+ }
1875
+ }
1876
+ }
1877
+
1878
+ if (verifyInstalled(commandDir, 'commands/pan-*')) {
1392
1879
  const count = fs.readdirSync(commandDir).filter(f => f.startsWith('pan-')).length;
1393
- console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
1880
+ console.log(` ${green}✓${reset} Installed ${count} commands to commands/`);
1394
1881
  } else {
1395
- failures.push('command/pan-*');
1882
+ failures.push('commands/pan-*');
1396
1883
  }
1397
1884
  } else if (isCodex) {
1398
- const skillsDir = path.join(targetDir, 'skills');
1885
+ // Codex reads skills from the shared .agents/skills tree (repo or user
1886
+ // scope) — $CODEX_HOME/skills is no longer a read location (2026-06).
1887
+ const skillsDir = getCodexSkillsRoot(isGlobal);
1399
1888
  const panSrc = path.join(src, 'commands', 'pan');
1400
1889
  copyCommandsAsCodexSkills(panSrc, skillsDir, 'pan', pathPrefix, runtime);
1890
+
1891
+ // Upgrade path: remove pan-* skills left in the legacy .codex/skills/
1892
+ // location by older installs (dead directory for current Codex).
1893
+ const legacySkillsDir = path.join(targetDir, 'skills');
1894
+ if (fs.existsSync(legacySkillsDir)) {
1895
+ for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
1896
+ if (entry.isDirectory() && entry.name.startsWith('pan-')) {
1897
+ try { fs.rmSync(path.join(legacySkillsDir, entry.name), { recursive: true }); } catch {}
1898
+ }
1899
+ }
1900
+ }
1901
+
1401
1902
  const installedSkillNames = listCodexSkillNames(skillsDir);
1402
1903
  if (installedSkillNames.length > 0) {
1403
- console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
1904
+ const label = isGlobal ? '~/.agents/skills/' : '.agents/skills/';
1905
+ console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to ${label}`);
1404
1906
  } else {
1405
- failures.push('skills/pan-*');
1907
+ failures.push('.agents/skills/pan-*');
1406
1908
  }
1407
1909
  } else if (isCopilot) {
1408
1910
  const skillsDir = path.join(targetDir, 'skills');
@@ -1474,8 +1976,9 @@ function install(isGlobal, runtime = 'claude') {
1474
1976
  if (fs.existsSync(internalLearningsDir)) {
1475
1977
  try {
1476
1978
  fs.rmSync(internalLearningsDir, { recursive: true, force: true });
1477
- } catch {
1478
- // Best-effort: install proceeds even if cleanup fails
1979
+ } catch (err) {
1980
+ // Surface it: a failed strip would ship source-only internal learnings.
1981
+ if (err.code !== 'ENOENT') pushInstallWarning('stripInternalLearnings', 'learnings/internal', err);
1479
1982
  }
1480
1983
  }
1481
1984
 
@@ -1496,10 +1999,10 @@ function install(isGlobal, runtime = 'claude') {
1496
1999
  const agentsDest = path.join(targetDir, 'agents');
1497
2000
  fs.mkdirSync(agentsDest, { recursive: true });
1498
2001
 
1499
- // Remove old PAN agents (pan-*.md and pan-*.agent.md) before copying new ones
2002
+ // Remove old PAN agents (pan-*.md, pan-*.agent.md, pan-*.toml) before copying new ones
1500
2003
  if (fs.existsSync(agentsDest)) {
1501
2004
  for (const file of fs.readdirSync(agentsDest)) {
1502
- if (file.startsWith('pan-') && file.endsWith('.md')) {
2005
+ if (file.startsWith('pan-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
1503
2006
  fs.unlinkSync(path.join(agentsDest, file));
1504
2007
  }
1505
2008
  }
@@ -1514,6 +2017,18 @@ function install(isGlobal, runtime = 'claude') {
1514
2017
  const dirRegex = /~\/\.claude\//g;
1515
2018
  content = content.replace(dirRegex, pathPrefix);
1516
2019
  content = processAttribution(content, getCommitAttribution(runtime));
2020
+ // Codex custom agents are standalone TOML files (2026-06 format);
2021
+ // markdown in .codex/agents/ is not recognized. Handle before
2022
+ // stripThinkingFrontmatter so `effort:` survives to be mapped to
2023
+ // Codex's native model_reasoning_effort field.
2024
+ if (isCodex) {
2025
+ const toml = convertClaudeAgentToCodexToml(stripSubTags(content));
2026
+ if (toml) {
2027
+ const tomlName = entry.name.replace(/\.md$/, '.toml');
2028
+ fs.writeFileSync(path.join(agentsDest, tomlName), toml);
2029
+ }
2030
+ continue;
2031
+ }
1517
2032
  // E-3 (per-runtime): strip unsupported thinking frontmatter and inject
1518
2033
  // prose preamble for runtimes without native extended thinking.
1519
2034
  content = stripThinkingFrontmatter(content, runtime);
@@ -1522,8 +2037,6 @@ function install(isGlobal, runtime = 'claude') {
1522
2037
  content = convertClaudeToOpencodeFrontmatter(content);
1523
2038
  } else if (isGemini) {
1524
2039
  content = convertClaudeToGeminiAgent(content);
1525
- } else if (isCodex) {
1526
- content = convertClaudeToCodexMarkdown(content);
1527
2040
  } else if (isCopilot) {
1528
2041
  content = convertClaudeToCopilotAgent(content);
1529
2042
  }
@@ -1599,9 +2112,10 @@ function install(isGlobal, runtime = 'claude') {
1599
2112
  }
1600
2113
  }
1601
2114
 
1602
- if (!isCodex && !isOpencode) {
2115
+ if (!isOpencode) {
1603
2116
  // Copy hooks from dist/ (bundled with dependencies)
1604
- // Hooks are only supported by Claude Code, Gemini, and Copilot CLI
2117
+ // Hooks are supported by Claude Code, Gemini, Copilot CLI, and (since
2118
+ // 2026-06) Codex via .codex/hooks.json. OpenCode has no hook support.
1605
2119
  // Template paths for the target runtime (replaces '.claude' with correct config dir)
1606
2120
  try {
1607
2121
  const hooksSrc = path.join(src, 'hooks', 'dist');
@@ -1636,15 +2150,60 @@ function install(isGlobal, runtime = 'claude') {
1636
2150
  }
1637
2151
  }
1638
2152
 
2153
+ // Native Claude Code workflows (2026-06): deterministic orchestration
2154
+ // scripts for PAN's fan-out-shaped protocols. Claude-only — no other
2155
+ // runtime has an equivalent discovery surface.
2156
+ if (runtime === 'claude') {
2157
+ try {
2158
+ const workflowsDir = path.join(targetDir, 'workflows');
2159
+ fs.mkdirSync(workflowsDir, { recursive: true });
2160
+ const scripts = lib.buildNativeWorkflowScripts();
2161
+ for (const { name, content } of scripts) {
2162
+ fs.writeFileSync(path.join(workflowsDir, name), content);
2163
+ }
2164
+ console.log(` ${green}✓${reset} Installed ${scripts.length} native workflows to workflows/`);
2165
+ } catch (e) {
2166
+ pushInstallWarning('nativeWorkflows', 'workflows/pan-*', e);
2167
+ }
2168
+ }
2169
+
1639
2170
  if (failures.length > 0) {
1640
2171
  console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
1641
2172
  process.exit(1);
1642
2173
  }
1643
2174
 
1644
2175
  // Write file manifest for future modification detection
1645
- const manifest = writeManifest(targetDir, runtime);
2176
+ const manifest = writeManifest(targetDir, runtime, isGlobal);
1646
2177
  console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
1647
2178
 
2179
+ // AGENTS.md universal rules layer (ADR-0028 Phase 3): contribute one
2180
+ // marker-fenced PAN section to the project's AGENTS.md (read natively by
2181
+ // every PAN runtime), and bridge CLAUDE.md to it via @AGENTS.md for the
2182
+ // Claude runtime. Project-scoped — local installs only; user content
2183
+ // outside the markers is never touched.
2184
+ if (!isGlobal) {
2185
+ try {
2186
+ const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
2187
+ let agentsExisting = null;
2188
+ try { agentsExisting = fs.readFileSync(agentsMdPath, 'utf8'); } catch { /* absent */ }
2189
+ fs.writeFileSync(agentsMdPath, lib.upsertAgentsMdSection(agentsExisting, lib.buildAgentsMdSection()));
2190
+ console.log(` ${green}✓${reset} ${agentsExisting === null ? 'Created' : 'Updated'} AGENTS.md (PAN section)`);
2191
+
2192
+ if (runtime === 'claude') {
2193
+ const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
2194
+ let claudeExisting = null;
2195
+ try { claudeExisting = fs.readFileSync(claudeMdPath, 'utf8'); } catch { /* absent */ }
2196
+ const bridged = lib.ensureClaudeMdImport(claudeExisting);
2197
+ if (bridged !== claudeExisting) {
2198
+ fs.writeFileSync(claudeMdPath, bridged);
2199
+ console.log(` ${green}✓${reset} Bridged CLAUDE.md to AGENTS.md (@AGENTS.md import)`);
2200
+ }
2201
+ }
2202
+ } catch (e) {
2203
+ pushInstallWarning('agentsMd', 'AGENTS.md', e);
2204
+ }
2205
+ }
2206
+
1648
2207
  // Report any backed-up local patches
1649
2208
  reportLocalPatches(targetDir, runtime);
1650
2209
 
@@ -1657,12 +2216,17 @@ function install(isGlobal, runtime = 'claude') {
1657
2216
  for (const w of verifyResult.warnings) console.error(` - ${w}`);
1658
2217
  }
1659
2218
  if (!verifyResult.ok) {
1660
- console.error(`\n ${red}✖ Install verification FAILED — ${verifyResult.missing.length} file(s) missing:${reset}`);
1661
- // Limit output to first 20 missing to avoid screen-flooding
1662
- const sample = verifyResult.missing.slice(0, 20);
2219
+ const emptyFiles = verifyResult.empty || [];
2220
+ const problems = [
2221
+ ...verifyResult.missing.map(m => `${m} (missing)`),
2222
+ ...emptyFiles.map(e => `${e} (empty — copy failed)`),
2223
+ ];
2224
+ console.error(`\n ${red}✖ Install verification FAILED — ${verifyResult.missing.length} missing, ${emptyFiles.length} empty:${reset}`);
2225
+ // Limit output to first 20 to avoid screen-flooding
2226
+ const sample = problems.slice(0, 20);
1663
2227
  for (const m of sample) console.error(` - ${m}`);
1664
- if (verifyResult.missing.length > sample.length) {
1665
- console.error(` ... and ${verifyResult.missing.length - sample.length} more`);
2228
+ if (problems.length > sample.length) {
2229
+ console.error(` ... and ${problems.length - sample.length} more`);
1666
2230
  }
1667
2231
  console.error(`\n Run ${cyan}pan-tools validate deployment${reset} for full diagnostics, then re-run the installer.\n`);
1668
2232
  process.exit(1);
@@ -1682,10 +2246,6 @@ function install(isGlobal, runtime = 'claude') {
1682
2246
  }
1683
2247
  }
1684
2248
 
1685
- if (isCodex) {
1686
- return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
1687
- }
1688
-
1689
2249
  const updateCheckCommand = isGlobal
1690
2250
  ? buildHookCommand(targetDir, 'pan-check-update.js')
1691
2251
  : 'node ' + dirName + '/hooks/pan-check-update.js';
@@ -1702,45 +2262,76 @@ function install(isGlobal, runtime = 'claude') {
1702
2262
  ? buildHookCommand(targetDir, 'pan-trace-logger.js')
1703
2263
  : 'node ' + dirName + '/hooks/pan-trace-logger.js';
1704
2264
 
1705
- // Copilot CLI uses config.json with its own hook format
2265
+ if (isCodex) {
2266
+ // Codex hooks (2026-06): Claude-compatible PascalCase events in the shared
2267
+ // .codex/hooks.json — merge PAN entries non-destructively (foreign hooks
2268
+ // preserved, reinstall idempotent). Project-scoped hooks load once the
2269
+ // project is trusted; codexTrustNotice() already covers that.
2270
+ try {
2271
+ const hooksJsonPath = path.join(targetDir, 'hooks.json');
2272
+ let existing = null;
2273
+ try { existing = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch { /* absent or invalid — start fresh */ }
2274
+ const merged = lib.mergeCodexHooksConfig(existing, {
2275
+ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand,
2276
+ });
2277
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(merged, null, 2) + '\n');
2278
+ console.log(` ${green}✓${reset} Configured hooks (.codex/hooks.json: update check, context monitor, cost + trace loggers)`);
2279
+ } catch (e) {
2280
+ pushInstallWarning('codexHooks', 'hooks.json', e);
2281
+ }
2282
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
2283
+ }
2284
+
2285
+ // Copilot CLI reads hook config from .github/hooks/*.json (version:1 schema),
2286
+ // NOT from config.json. Write a dedicated PAN hooks file and migrate away
2287
+ // from any legacy config.json registration.
1706
2288
  if (isCopilot) {
2289
+ const hooksConfigPath = path.join(targetDir, 'hooks', 'pan.json');
2290
+ const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand });
2291
+ try {
2292
+ fs.mkdirSync(path.dirname(hooksConfigPath), { recursive: true });
2293
+ fs.writeFileSync(hooksConfigPath, JSON.stringify(hooksConfig, null, 2) + '\n');
2294
+ console.log(` ${green}✓${reset} Configured hooks (.github/hooks/pan.json: update check, context monitor, cost + trace loggers)`);
2295
+ } catch (e) {
2296
+ console.error(` ${yellow}✗${reset} Failed to write Copilot hooks config: ${e.message}`);
2297
+ }
2298
+
2299
+ // Upgrade cleanup: strip PAN hook + statusline registrations left in
2300
+ // config.json by pre-2026-06 installs. Copilot CLI never read them there —
2301
+ // user-editable settings live in settings.json (~/.copilot/ global,
2302
+ // .github/copilot/ repo-level); config.json is internal CLI state.
1707
2303
  const configPath = path.join(targetDir, 'config.json');
1708
2304
  const config = readSettings(configPath);
1709
-
1710
- if (!config.hooks) {
1711
- config.hooks = {};
2305
+ let legacyModified = false;
2306
+ if (config.hooks) {
2307
+ for (const evt of ['sessionStart', 'postToolUse']) {
2308
+ if (Array.isArray(config.hooks[evt])) {
2309
+ config.hooks[evt] = config.hooks[evt].filter(h =>
2310
+ !(h.command && (h.command.includes('pan-check-update') || h.command.includes('pan-context-monitor')))
2311
+ );
2312
+ if (config.hooks[evt].length === 0) delete config.hooks[evt];
2313
+ }
2314
+ }
2315
+ if (Object.keys(config.hooks).length === 0) delete config.hooks;
2316
+ legacyModified = true;
1712
2317
  }
1713
-
1714
- // Register sessionStart hook for update checking
1715
- if (!config.hooks.sessionStart) {
1716
- config.hooks.sessionStart = [];
2318
+ if (config.statusLine && config.statusLine.command &&
2319
+ config.statusLine.command.includes('pan-statusline')) {
2320
+ delete config.statusLine;
2321
+ legacyModified = true;
1717
2322
  }
1718
- const hasPanUpdateHook = config.hooks.sessionStart.some(h =>
1719
- h.command && h.command.includes('pan-check-update')
1720
- );
1721
- if (!hasPanUpdateHook) {
1722
- config.hooks.sessionStart.push({
1723
- command: updateCheckCommand
1724
- });
1725
- console.log(` ${green}✓${reset} Configured update check hook`);
2323
+ if (legacyModified) {
2324
+ writeSettings(configPath, config);
1726
2325
  }
1727
2326
 
1728
- // Register postToolUse hook for context monitoring
1729
- if (!config.hooks.postToolUse) {
1730
- config.hooks.postToolUse = [];
1731
- }
1732
- const hasContextHook = config.hooks.postToolUse.some(h =>
1733
- h.command && h.command.includes('pan-context-monitor')
1734
- );
1735
- if (!hasContextHook) {
1736
- config.hooks.postToolUse.push({
1737
- command: contextMonitorCommand
1738
- });
1739
- console.log(` ${green}✓${reset} Configured context window monitor hook`);
1740
- }
2327
+ // Documented Copilot CLI settings read paths: ~/.copilot/settings.json
2328
+ // (global) or .github/copilot/settings.json (repo-level, committed).
2329
+ const copilotSettingsPath = isGlobal
2330
+ ? path.join(targetDir, 'settings.json')
2331
+ : path.join(targetDir, 'copilot', 'settings.json');
2332
+ const copilotSettings = readSettings(copilotSettingsPath);
1741
2333
 
1742
- writeSettings(configPath, config);
1743
- return { settingsPath: configPath, settings: config, statuslineCommand, runtime };
2334
+ return { settingsPath: copilotSettingsPath, settings: copilotSettings, statuslineCommand, runtime };
1744
2335
  }
1745
2336
 
1746
2337
  // Configure statusline and hooks in settings.json
@@ -1856,22 +2447,25 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
1856
2447
  const isCopilot = runtime === 'copilot';
1857
2448
 
1858
2449
  if (shouldInstallStatusline && !isOpencode && !isCodex) {
2450
+ // Same schema everywhere — Copilot CLI also uses {type: "command", command}
2451
+ // in settings.json (statusline is experimental there as of 2026-05).
2452
+ settings.statusLine = {
2453
+ type: 'command',
2454
+ command: statuslineCommand
2455
+ };
2456
+ console.log(` ${green}✓${reset} Configured statusline`);
1859
2457
  if (isCopilot) {
1860
- // Copilot CLI: statusline stored in config.json
1861
- settings.statusLine = {
1862
- command: statuslineCommand
1863
- };
1864
- } else {
1865
- settings.statusLine = {
1866
- type: 'command',
1867
- command: statuslineCommand
1868
- };
2458
+ console.log(` ${dim}ℹ Copilot CLI statusline is experimental — if it doesn't render, start with 'copilot --experimental'${reset}`);
1869
2459
  }
1870
- console.log(` ${green}✓${reset} Configured statusline`);
1871
2460
  }
1872
2461
 
1873
- // Write settings/config when runtime supports it
1874
- if (!isCodex) {
2462
+ // Write settings/config when runtime supports it. For Copilot, skip the
2463
+ // write when there is nothing to persist (avoids creating an empty
2464
+ // .github/copilot/settings.json).
2465
+ if (!isCodex && !(isCopilot && Object.keys(settings).length === 0)) {
2466
+ if (isCopilot) {
2467
+ try { fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); } catch { /* surfaced by writeSettings */ }
2468
+ }
1875
2469
  writeSettings(settingsPath, settings);
1876
2470
  }
1877
2471
 
@@ -1896,8 +2490,8 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
1896
2490
  !caps.has_thinking ? 'extended thinking (E-3, E-10, E-11)' : null,
1897
2491
  ].filter(Boolean).join(', ');
1898
2492
  console.log(`
1899
- ${yellow}ℹ${reset} PAN 2.10+ is tuned for Opus 4.7. Default model "${modelField}" lacks: ${missing}.
1900
- Features degrade gracefully, but upgrade to claude-opus-4-7 for best results.`);
2493
+ ${yellow}ℹ${reset} PAN 2.10+ is tuned for Opus 4.7+ class models. Default model "${modelField}" lacks: ${missing}.
2494
+ Features degrade gracefully, but upgrade to claude-opus-4-8 (or claude-fable-5) for best results.`);
1901
2495
  }
1902
2496
  }
1903
2497
  } catch {
@@ -1905,6 +2499,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
1905
2499
  }
1906
2500
  }
1907
2501
 
2502
+ // Gemini CLI → Antigravity transition (2026-06): informational, non-blocking.
2503
+ if (runtime === 'gemini' && !args.includes('--skip-warnings')) {
2504
+ console.log(`\n ${yellow}ℹ${reset} ${geminiTransitionNotice().split('\n').join('\n ')}`);
2505
+ }
2506
+
2507
+ // Codex project-trust gate (2026-06): project-scoped .codex/ config only
2508
+ // loads once the project is trusted. Local installs only — personal
2509
+ // ~/.codex/ config is not gated.
2510
+ if (runtime === 'codex' && !isGlobal && !args.includes('--skip-warnings')) {
2511
+ console.log(`\n ${yellow}ℹ${reset} ${codexTrustNotice().split('\n').join('\n ')}`);
2512
+ }
2513
+
1908
2514
  let program = 'Claude Code';
1909
2515
  if (runtime === 'opencode') program = 'OpenCode';
1910
2516
  if (runtime === 'gemini') program = 'Gemini';