pan-wizard 2.8.1 → 2.9.1

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 (35) hide show
  1. package/README.md +4 -2
  2. package/bin/install.js +23 -0
  3. package/commands/pan/assumptions.md +38 -3
  4. package/commands/pan/audit-deployment.md +6 -0
  5. package/commands/pan/debug.md +71 -2
  6. package/commands/pan/exec-phase.md +90 -0
  7. package/commands/pan/focus-auto.md +181 -18
  8. package/commands/pan/focus-design.md +302 -14
  9. package/commands/pan/focus-doc-audit.md +530 -0
  10. package/commands/pan/focus-drift-walking.md +525 -0
  11. package/commands/pan/focus-exec.md +168 -46
  12. package/commands/pan/focus-plan.md +204 -12
  13. package/commands/pan/focus-scan.md +17 -5
  14. package/commands/pan/map-codebase.md +32 -6
  15. package/commands/pan/milestone-audit.md +23 -0
  16. package/commands/pan/new-project.md +64 -0
  17. package/commands/pan/pause.md +42 -1
  18. package/commands/pan/plan-phase.md +84 -0
  19. package/commands/pan/profile.md +2 -1
  20. package/commands/pan/quick.md +15 -0
  21. package/commands/pan/resume.md +62 -2
  22. package/commands/pan/verify-phase.md +42 -0
  23. package/package.json +1 -1
  24. package/pan-wizard-core/bin/lib/commands.cjs +29 -7
  25. package/pan-wizard-core/bin/lib/config.cjs +10 -0
  26. package/pan-wizard-core/bin/lib/constants.cjs +3 -1
  27. package/pan-wizard-core/bin/lib/core.cjs +168 -21
  28. package/pan-wizard-core/bin/lib/focus.cjs +5 -0
  29. package/pan-wizard-core/bin/lib/verify.cjs +283 -4
  30. package/pan-wizard-core/bin/pan-tools.cjs +11 -2
  31. package/pan-wizard-core/references/model-profiles.md +191 -62
  32. package/pan-wizard-core/workflows/help.md +11 -1
  33. package/pan-wizard-core/workflows/profile.md +8 -1
  34. package/pan-wizard-core/workflows/settings.md +14 -0
  35. package/scripts/generate-skills-docs.py +560 -0
@@ -25,21 +25,41 @@ const {
25
25
  MILESTONE_VERSION_RE,
26
26
  } = require('./constants.cjs');
27
27
 
28
+ // ─── Multi-Model Routing ─────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Provider-specific model name mapping for each tier alias.
32
+ * Each provider maps reasoning/mid/fast to its native model identifiers.
33
+ * "inherit" means the host runtime uses its own top-tier model selection.
34
+ */
35
+ const PROVIDER_MODELS = {
36
+ anthropic: { reasoning: 'inherit', mid: 'sonnet', fast: 'haiku' },
37
+ openai: { reasoning: 'inherit', mid: 'mid', fast: 'fast' },
38
+ google: { reasoning: 'inherit', mid: 'mid', fast: 'fast' },
39
+ default: { reasoning: 'inherit', mid: 'sonnet', fast: 'haiku' },
40
+ };
41
+
42
+ /** Maps legacy Anthropic model names to provider-agnostic tier aliases. */
43
+ const LEGACY_ALIASES = { opus: 'reasoning', sonnet: 'mid', haiku: 'fast' };
44
+
45
+ /** Relative cost multipliers per tier (fast = 1× baseline). */
46
+ const COST_MULTIPLIERS = { reasoning: 15, mid: 3, fast: 1 };
47
+
28
48
  // ─── Model Profile Table ─────────────────────────────────────────────────────
29
49
 
30
50
  const MODEL_PROFILES = {
31
- 'pan-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
32
- 'pan-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
33
- 'pan-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
34
- 'pan-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
35
- 'pan-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
36
- 'pan-research-synthesizer': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
37
- 'pan-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
38
- 'pan-document_code': { quality: 'opus', balanced: 'haiku', budget: 'haiku' },
39
- 'pan-verifier': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
40
- 'pan-plan-checker': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
41
- 'pan-integration-checker': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
42
- 'pan-reviewer': { quality: 'opus', balanced: 'haiku', budget: 'haiku' },
51
+ 'pan-planner': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
52
+ 'pan-roadmapper': { quality: 'reasoning', balanced: 'mid', budget: 'mid' },
53
+ 'pan-executor': { quality: 'reasoning', balanced: 'mid', budget: 'mid' },
54
+ 'pan-phase-researcher': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
55
+ 'pan-project-researcher': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
56
+ 'pan-research-synthesizer': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
57
+ 'pan-debugger': { quality: 'reasoning', balanced: 'mid', budget: 'mid' },
58
+ 'pan-document_code': { quality: 'reasoning', balanced: 'fast', budget: 'fast' },
59
+ 'pan-verifier': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
60
+ 'pan-plan-checker': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
61
+ 'pan-integration-checker': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
62
+ 'pan-reviewer': { quality: 'reasoning', balanced: 'fast', budget: 'fast' },
43
63
  };
44
64
 
45
65
  // ─── Output helpers ───────────────────────────────────────────────────────────
@@ -179,6 +199,8 @@ function loadConfig(cwd) {
179
199
  commit: parsed.commit || { safety_checks: true, conventional_types: true, sensitive_patterns: ['\\.env$', '\\.pem$', '\\.key$', 'credentials', 'secret', 'password', 'token'] },
180
200
  execution: parsed.execution || { default_mode: 'wave_order', rollback_snapshots: true, error_pattern_learning: true },
181
201
  focus: parsed.focus || { auto_commit: true },
202
+ model_overrides: parsed.model_overrides || {},
203
+ routing: parsed.routing || { strategy: 'static', provider: 'auto' },
182
204
  };
183
205
  } catch { // Config missing or malformed — use defaults
184
206
  return {
@@ -187,6 +209,8 @@ function loadConfig(cwd) {
187
209
  commit: { safety_checks: true, conventional_types: true, sensitive_patterns: ['\\.env$', '\\.pem$', '\\.key$', 'credentials', 'secret', 'password', 'token'] },
188
210
  execution: { default_mode: 'wave_order', rollback_snapshots: true, error_pattern_learning: true },
189
211
  focus: { auto_commit: true },
212
+ model_overrides: {},
213
+ routing: { strategy: 'static', provider: 'auto' },
190
214
  };
191
215
  }
192
216
  }
@@ -485,27 +509,142 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
485
509
  }
486
510
 
487
511
  /**
488
- * Resolve the model for a given agent type based on profile and overrides.
489
- * Returns "inherit" for opus-tier to let Claude Code use its configured opus version.
512
+ * Extract a model tier override from a roadmap phase section.
513
+ * Looks for `<!-- model_tier: <tier> -->` in the phase section text.
514
+ * @param {string} cwd - Project root directory
515
+ * @param {string|number} phaseNum - Phase number to look up
516
+ * @returns {string|null} Tier alias if found, null otherwise
517
+ */
518
+ function getPhaseModelTier(cwd, phaseNum) {
519
+ const phaseData = getRoadmapPhaseInternal(cwd, phaseNum);
520
+ if (!phaseData?.section) return null;
521
+ const match = phaseData.section.match(/<!--\s*model_tier:\s*(\S+)\s*-->/i);
522
+ return match ? match[1] : null;
523
+ }
524
+
525
+ /**
526
+ * Resolve the model for a given agent type based on profile, provider, and routing strategy.
527
+ * Returns "inherit" for reasoning-tier to let the host runtime use its top-tier model.
490
528
  * @param {string} cwd - Project root directory
491
529
  * @param {string} agentType - Agent name (e.g., "pan-planner", "pan-executor")
492
- * @returns {string} Model identifier: "inherit" (opus), "sonnet", or "haiku"
530
+ * @param {Object} [taskMetadata] - Optional metadata for complexity routing
531
+ * @returns {string} Model identifier: "inherit", "sonnet", "haiku", "mid", "fast", etc.
493
532
  */
494
- function resolveModelInternal(cwd, agentType) {
533
+ function resolveModelInternal(cwd, agentType, taskMetadata) {
495
534
  const config = loadConfig(cwd);
535
+ const provider = detectProvider(cwd, config);
496
536
 
497
- // Check per-agent override first
537
+ // Check per-agent override first (highest priority)
498
538
  const override = config.model_overrides?.[agentType];
499
539
  if (override) {
500
- return override === 'opus' ? 'inherit' : override;
540
+ return resolveTierToModel(override, provider);
541
+ }
542
+
543
+ // Check per-phase override from roadmap (second priority)
544
+ if (taskMetadata?.phaseNum) {
545
+ const phaseTier = getPhaseModelTier(cwd, taskMetadata.phaseNum);
546
+ if (phaseTier) {
547
+ return resolveTierToModel(phaseTier, provider);
548
+ }
501
549
  }
502
550
 
503
551
  // Fall back to profile lookup
504
552
  const profile = config.model_profile || 'balanced';
505
553
  const agentModels = MODEL_PROFILES[agentType];
506
- if (!agentModels) return 'sonnet';
507
- const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
508
- return resolved === 'opus' ? 'inherit' : resolved;
554
+ if (!agentModels) return resolveTierToModel('mid', provider);
555
+
556
+ let tier = agentModels[profile] || agentModels['balanced'] || 'mid';
557
+
558
+ // Apply routing strategy
559
+ const strategy = config.routing?.strategy || 'static';
560
+ if (strategy === 'complexity' && taskMetadata) {
561
+ const thresholds = config.routing?.complexity_thresholds;
562
+ tier = resolveComplexityTier(tier, { ...taskMetadata, thresholds });
563
+ }
564
+
565
+ return resolveTierToModel(tier, provider);
566
+ }
567
+
568
+ /**
569
+ * Detect the LLM provider from config, environment, or runtime directory presence.
570
+ * @param {string} cwd - Project root directory
571
+ * @param {Object} config - Loaded config object
572
+ * @returns {string} Provider name: "anthropic", "openai", "google", or "default"
573
+ */
574
+ function detectProvider(cwd, config) {
575
+ // 1. Explicit config
576
+ if (config.routing?.provider && config.routing.provider !== 'auto') {
577
+ const p = config.routing.provider;
578
+ return PROVIDER_MODELS[p] ? p : 'default';
579
+ }
580
+ // 2. Environment variable
581
+ const envProvider = process.env.PAN_PROVIDER;
582
+ if (envProvider) {
583
+ return PROVIDER_MODELS[envProvider] ? envProvider : 'default';
584
+ }
585
+ // 3. Runtime directory detection
586
+ const checks = [
587
+ ['.claude', 'anthropic'], ['.codex', 'openai'],
588
+ ['.gemini', 'google'], ['.opencode', 'openai'], ['.github', 'default'],
589
+ ];
590
+ for (const [dir, provider] of checks) {
591
+ try { if (fs.statSync(path.join(cwd, dir)).isDirectory()) return provider; }
592
+ catch { /* continue */ }
593
+ }
594
+ return 'default';
595
+ }
596
+
597
+ /**
598
+ * Resolve a tier alias (or legacy model name) to a provider-specific model name.
599
+ * @param {string} tier - Tier alias ("reasoning", "mid", "fast") or legacy name ("opus", "sonnet", "haiku")
600
+ * @param {string} provider - Provider key from detectProvider()
601
+ * @returns {string} Provider-specific model name
602
+ */
603
+ function resolveTierToModel(tier, provider) {
604
+ const normalizedTier = LEGACY_ALIASES[tier] || tier;
605
+ const providerMap = PROVIDER_MODELS[provider] || PROVIDER_MODELS['default'];
606
+ return providerMap[normalizedTier] || providerMap['mid'];
607
+ }
608
+
609
+ /**
610
+ * Adjust model tier based on task complexity metadata.
611
+ * @param {string} baseTier - Starting tier ("reasoning", "mid", "fast")
612
+ * @param {Object} [taskMetadata] - Complexity indicators
613
+ * @returns {string} Adjusted tier
614
+ */
615
+ function resolveComplexityTier(baseTier, taskMetadata) {
616
+ if (!taskMetadata) return baseTier;
617
+ const { fileCount = 0, waveCount = 0, requirementCount = 0, isArchitectural = false } = taskMetadata;
618
+
619
+ const score =
620
+ (fileCount > 15 ? 2 : fileCount > 5 ? 1 : 0) +
621
+ (waveCount > 3 ? 2 : waveCount > 1 ? 1 : 0) +
622
+ (requirementCount > 5 ? 2 : requirementCount > 2 ? 1 : 0) +
623
+ (isArchitectural ? 3 : 0);
624
+
625
+ const thresholds = taskMetadata.thresholds || { downgrade_max: 2, upgrade_min: 6 };
626
+ const tiers = ['fast', 'mid', 'reasoning'];
627
+ const idx = tiers.indexOf(baseTier);
628
+ if (idx === -1) return baseTier;
629
+
630
+ if (score <= thresholds.downgrade_max && idx > 0) return tiers[idx - 1];
631
+ if (score >= thresholds.upgrade_min && idx < 2) return tiers[idx + 1];
632
+ return baseTier;
633
+ }
634
+
635
+ /**
636
+ * Estimate relative cost multiplier for a given profile.
637
+ * @param {string} profile - "quality", "balanced", or "budget"
638
+ * @returns {Object} Cost estimation with total, average, agentCount
639
+ */
640
+ function estimateCostMultiplier(profile) {
641
+ let total = 0;
642
+ const agents = Object.keys(MODEL_PROFILES);
643
+ for (const agent of agents) {
644
+ const tier = MODEL_PROFILES[agent][profile] || 'mid';
645
+ total += COST_MULTIPLIERS[tier] || 3;
646
+ }
647
+ return { profile, total, average: +(total / agents.length).toFixed(1), agentCount: agents.length };
509
648
  }
510
649
 
511
650
  // ─── Misc utilities ───────────────────────────────────────────────────────────
@@ -625,6 +764,9 @@ function scanSourceTodos(cwd) {
625
764
 
626
765
  module.exports = {
627
766
  MODEL_PROFILES,
767
+ PROVIDER_MODELS,
768
+ LEGACY_ALIASES,
769
+ COST_MULTIPLIERS,
628
770
  output,
629
771
  error,
630
772
  verbose,
@@ -641,6 +783,11 @@ module.exports = {
641
783
  getArchivedPhaseDirs,
642
784
  getRoadmapPhaseInternal,
643
785
  resolveModelInternal,
786
+ detectProvider,
787
+ resolveTierToModel,
788
+ resolveComplexityTier,
789
+ estimateCostMultiplier,
790
+ getPhaseModelTier,
644
791
  pathExistsInternal,
645
792
  generateSlugInternal,
646
793
  getMilestoneInfo,
@@ -783,6 +783,11 @@ function determineStopReason(cycle, run) {
783
783
  if (run.totals.cycles_completed >= run.max_cycles) return 'max_cycles';
784
784
  if (cycle.items_completed === 0) return 'zero_completed';
785
785
 
786
+ // Prompts category: stop when all prompts are completed
787
+ if (run.category === 'prompts' && cycle.prompts_remaining === 0) {
788
+ return 'prompts_complete';
789
+ }
790
+
786
791
  // Optimize category: stop when efficiency drops below threshold of previous cycle
787
792
  if (run.category === 'optimize' && run.cycles.length >= 2) {
788
793
  const prev = run.cycles[run.cycles.length - 2];
@@ -826,6 +826,16 @@ function repairIssues(cwd, repairs) {
826
826
  repairActions.push({ action: repair, success: true, path: CONFIG_FILE });
827
827
  break;
828
828
  }
829
+ case 'syncRequirements': {
830
+ const syncResult = syncRequirementCheckboxes(cwd);
831
+ repairActions.push({ action: repair, success: !syncResult.error, fixed: syncResult.fixed, error: syncResult.error || undefined });
832
+ break;
833
+ }
834
+ case 'syncRoadmap': {
835
+ const syncResult = syncRoadmapPlanCheckboxes(cwd);
836
+ repairActions.push({ action: repair, success: !syncResult.error, fixed: syncResult.fixed, error: syncResult.error || undefined });
837
+ break;
838
+ }
829
839
  case 'regenerateState': {
830
840
  // Create timestamped backup before overwriting to prevent data loss
831
841
  try {
@@ -950,13 +960,112 @@ function checkVerificationGate(cwd, addIssue) {
950
960
  }
951
961
  }
952
962
 
963
+ /**
964
+ * Sync REQUIREMENTS.md checkboxes — mark requirements as complete if their
965
+ * linked phases are completed in roadmap.md. Returns count of boxes fixed.
966
+ * @param {string} cwd
967
+ * @returns {{ fixed: number, error?: string }}
968
+ */
969
+ function syncRequirementCheckboxes(cwd) {
970
+ const planDir = planningPath(cwd);
971
+ const reqPath = path.join(planDir, REQUIREMENTS_FILE);
972
+ const roadmapPath = path.join(planDir, ROADMAP_FILE);
973
+
974
+ let reqContent, roadmapContent;
975
+ try { reqContent = fs.readFileSync(reqPath, 'utf-8'); } catch { return { fixed: 0, error: 'requirements.md not found' }; }
976
+ try { roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); } catch { return { fixed: 0, error: 'roadmap.md not found' }; }
977
+
978
+ // Find completed phases (checkbox marked [x] in roadmap)
979
+ const completedPhases = [];
980
+ const phaseRe = /- \[x\]\s*.*Phase\s+(\d+(?:\.\d+)?)/gi;
981
+ let m;
982
+ while ((m = phaseRe.exec(roadmapContent)) !== null) {
983
+ completedPhases.push(m[1]);
984
+ }
985
+
986
+ if (completedPhases.length === 0) return { fixed: 0 };
987
+
988
+ // For each completed phase, find linked requirement IDs and check their boxes
989
+ let fixed = 0;
990
+ for (const phaseNum of completedPhases) {
991
+ const reqMatch = roadmapContent.match(
992
+ new RegExp(`Phase\\s+${phaseNum.replace(/\./g, '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
993
+ );
994
+ if (!reqMatch) continue;
995
+ const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(id => id.trim()).filter(Boolean);
996
+ for (const reqId of reqIds) {
997
+ const escaped = reqId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
998
+ const re = new RegExp(`(- \\[) (\\]\\s*\\*\\*${escaped}\\*\\*)`, 'gi');
999
+ const before = reqContent;
1000
+ reqContent = reqContent.replace(re, '$1x$2');
1001
+ if (reqContent !== before) fixed++;
1002
+ }
1003
+ }
1004
+
1005
+ if (fixed > 0) {
1006
+ try { fs.writeFileSync(reqPath, reqContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message }; }
1007
+ }
1008
+ return { fixed };
1009
+ }
1010
+
1011
+ /**
1012
+ * Sync ROADMAP.md plan checkboxes — mark plans as checked if corresponding
1013
+ * summary files exist on disk (indicating execution completed).
1014
+ * @param {string} cwd
1015
+ * @returns {{ fixed: number, error?: string }}
1016
+ */
1017
+ function syncRoadmapPlanCheckboxes(cwd) {
1018
+ const planDir = planningPath(cwd);
1019
+ const roadmapPath = path.join(planDir, ROADMAP_FILE);
1020
+
1021
+ let roadmapContent;
1022
+ try { roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); } catch { return { fixed: 0, error: 'roadmap.md not found' }; }
1023
+
1024
+ // Collect all summary files on disk (indicates plan was executed)
1025
+ const phasesDir = path.join(planDir, PHASES_DIR);
1026
+ const summaryFiles = new Set();
1027
+ try {
1028
+ const dirs = fs.readdirSync(phasesDir, { withFileTypes: true }).filter(d => d.isDirectory());
1029
+ for (const dir of dirs) {
1030
+ try {
1031
+ const files = fs.readdirSync(path.join(phasesDir, dir.name));
1032
+ for (const f of files) {
1033
+ if (isSummaryFile(f)) {
1034
+ // Extract plan stem: "01-02-summary.md" → "01-02"
1035
+ const stem = f.replace(/-summary\.md$/i, '');
1036
+ summaryFiles.add(stem);
1037
+ }
1038
+ }
1039
+ } catch { /* unreadable phase dir */ }
1040
+ }
1041
+ } catch { return { fixed: 0 }; }
1042
+
1043
+ if (summaryFiles.size === 0) return { fixed: 0 };
1044
+
1045
+ // Mark plan checkboxes: "- [ ] 01-02-plan.md" → "- [x] 01-02-plan.md"
1046
+ let fixed = 0;
1047
+ for (const stem of summaryFiles) {
1048
+ const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1049
+ const re = new RegExp(`(- \\[) (\\]\\s*${escaped}-plan)`, 'gi');
1050
+ const before = roadmapContent;
1051
+ roadmapContent = roadmapContent.replace(re, '$1x$2');
1052
+ if (roadmapContent !== before) fixed++;
1053
+ }
1054
+
1055
+ if (fixed > 0) {
1056
+ try { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message }; }
1057
+ }
1058
+ return { fixed };
1059
+ }
1060
+
953
1061
  /**
954
1062
  * Cross-check STATE.md progress counts against REQUIREMENTS.md checkboxes and ROADMAP.md plan checkboxes.
955
1063
  * Reports mismatches as warnings so users can run --repair or investigate.
956
1064
  * @param {string} cwd - Working directory path
957
1065
  * @param {Function} addIssue - Issue recording callback
1066
+ * @param {string[]} [repairs] - Repair action list (pushed to when repairable)
958
1067
  */
959
- function checkStateConsistency(cwd, addIssue) {
1068
+ function checkStateConsistency(cwd, addIssue, repairs) {
960
1069
  const planDir = planningPath(cwd);
961
1070
  const statePath = path.join(planDir, STATE_FILE);
962
1071
  const reqPath = path.join(planDir, REQUIREMENTS_FILE);
@@ -983,7 +1092,8 @@ function checkStateConsistency(cwd, addIssue) {
983
1092
  if (completedPlans > 0 && completedPlans >= totalPlans && uncheckedBoxes > 0) {
984
1093
  addIssue('warning', 'STATE_REQ_DRIFT',
985
1094
  `STATE.md shows all plans complete but REQUIREMENTS.md has ${uncheckedBoxes}/${totalBoxes} unchecked checkboxes`,
986
- 'Run syncRequirementCheckboxes or manually check completed requirement boxes');
1095
+ 'Run /pan:health --repair to auto-check completed requirement boxes', true);
1096
+ if (repairs) repairs.push('syncRequirements');
987
1097
  }
988
1098
  }
989
1099
  } catch { /* no REQUIREMENTS.md — not required */ }
@@ -1001,7 +1111,8 @@ function checkStateConsistency(cwd, addIssue) {
1001
1111
  if (completedPlans > 0 && completedPlans >= totalPlans && uncheckedPlans > 0) {
1002
1112
  addIssue('warning', 'STATE_ROADMAP_DRIFT',
1003
1113
  `STATE.md shows all plans complete but ROADMAP.md has ${uncheckedPlans} unchecked plan checkboxes`,
1004
- 'Run cmdRoadmapUpdatePlanProgress for each phase or manually check completed plan boxes');
1114
+ 'Run /pan:health --repair to auto-check completed plan boxes', true);
1115
+ if (repairs) repairs.push('syncRoadmap');
1005
1116
  }
1006
1117
  }
1007
1118
  } catch { /* no ROADMAP.md — other checks handle this */ }
@@ -1050,7 +1161,7 @@ function cmdValidateHealth(cwd, options, raw) {
1050
1161
  checkPhaseContents(cwd, addIssue);
1051
1162
 
1052
1163
  // Check 8b: cross-document state consistency
1053
- checkStateConsistency(cwd, addIssue);
1164
+ checkStateConsistency(cwd, addIssue, repairs);
1054
1165
 
1055
1166
  // Check 8c: verification gate (phases with verifier enabled need verification.md)
1056
1167
  checkVerificationGate(cwd, addIssue);
@@ -1783,6 +1894,169 @@ function cmdRetro(cwd, raw) {
1783
1894
  output(result, raw, rawLines.join('\n'));
1784
1895
  }
1785
1896
 
1897
+ // ─── Deployment Validation ──────────────────────────────────────────────────
1898
+
1899
+ /**
1900
+ * Detect which PAN runtimes are installed in cwd.
1901
+ * @param {string} cwd
1902
+ * @returns {Array<{runtime: string, configDir: string}>}
1903
+ */
1904
+ function detectInstalledRuntimes(cwd) {
1905
+ const RUNTIME_DIRS = [
1906
+ { runtime: 'claude', configDir: '.claude' },
1907
+ { runtime: 'opencode', configDir: '.opencode' },
1908
+ { runtime: 'gemini', configDir: '.gemini' },
1909
+ { runtime: 'codex', configDir: '.codex' },
1910
+ { runtime: 'copilot', configDir: '.github' },
1911
+ ];
1912
+ const found = [];
1913
+ for (const rt of RUNTIME_DIRS) {
1914
+ const manifestPath = path.join(cwd, rt.configDir, 'pan-file-manifest.json');
1915
+ try {
1916
+ fs.accessSync(manifestPath);
1917
+ found.push(rt);
1918
+ } catch (_) { /* not installed */ }
1919
+ }
1920
+ return found;
1921
+ }
1922
+
1923
+ /**
1924
+ * Validate a single PAN runtime installation.
1925
+ * Checks: manifest files exist, hashes match, settings integrity.
1926
+ * @param {string} cwd
1927
+ * @param {string} configDir - e.g. '.claude'
1928
+ * @param {string} runtime - e.g. 'claude'
1929
+ * @returns {{ status: string, version: string, total_files: number, missing: string[], modified: string[], orphaned: string[], settings_ok: boolean, settings_issues: string[] }}
1930
+ */
1931
+ function validateRuntimeInstall(cwd, configDir, runtime) {
1932
+ const crypto = require('crypto');
1933
+ const baseDir = path.join(cwd, configDir);
1934
+ const manifestPath = path.join(baseDir, 'pan-file-manifest.json');
1935
+
1936
+ let manifest;
1937
+ try {
1938
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1939
+ } catch (e) {
1940
+ return { status: 'broken', version: null, error: `Cannot read manifest: ${e.message}`, total_files: 0, missing: [], modified: [], orphaned: [], settings_ok: false, settings_issues: ['manifest unreadable'] };
1941
+ }
1942
+
1943
+ const missing = [];
1944
+ const modified = [];
1945
+ const files = manifest.files || {};
1946
+ const totalFiles = Object.keys(files).length;
1947
+
1948
+ for (const [relPath, expectedHash] of Object.entries(files)) {
1949
+ const absPath = path.join(baseDir, relPath);
1950
+ try {
1951
+ const content = fs.readFileSync(absPath);
1952
+ const actualHash = crypto.createHash('sha256').update(content).digest('hex');
1953
+ if (actualHash !== expectedHash) {
1954
+ modified.push(relPath);
1955
+ }
1956
+ } catch (_) {
1957
+ missing.push(relPath);
1958
+ }
1959
+ }
1960
+
1961
+ // Check settings integrity (hook paths resolve to real files)
1962
+ const settingsIssues = [];
1963
+ const settingsFile = runtime === 'copilot' ? 'config.json' : 'settings.json';
1964
+ const settingsPath = path.join(baseDir, settingsFile);
1965
+ let settingsOk = true;
1966
+ try {
1967
+ const settingsContent = fs.readFileSync(settingsPath, 'utf8');
1968
+ const settings = JSON.parse(settingsContent);
1969
+ // Check hook paths in settings
1970
+ // Collect all hook command strings from settings
1971
+ const hookCommands = [];
1972
+ const hooks = settings.hooks;
1973
+ if (hooks && typeof hooks === 'object') {
1974
+ for (const hookArr of Object.values(hooks)) {
1975
+ if (!Array.isArray(hookArr)) continue;
1976
+ for (const hook of hookArr) {
1977
+ if (hook.command) hookCommands.push(hook.command);
1978
+ }
1979
+ }
1980
+ }
1981
+ // Copilot/Gemini statusLine
1982
+ if (settings.statusLine && settings.statusLine.command) {
1983
+ hookCommands.push(settings.statusLine.command);
1984
+ }
1985
+ // Claude statusline
1986
+ if (settings.statusline && settings.statusline.command) {
1987
+ hookCommands.push(settings.statusline.command);
1988
+ }
1989
+ for (const cmd of hookCommands) {
1990
+ const parts = cmd.split(/\s+/);
1991
+ const hookFile = parts.find(p => p.endsWith('.js'));
1992
+ if (hookFile) {
1993
+ // Hook paths are relative to cwd, not to config dir
1994
+ const resolvedPath = path.isAbsolute(hookFile) ? hookFile : path.join(cwd, hookFile);
1995
+ try { fs.accessSync(resolvedPath); } catch (_) {
1996
+ settingsIssues.push(`Hook path not found: ${hookFile}`);
1997
+ settingsOk = false;
1998
+ }
1999
+ }
2000
+ }
2001
+ } catch (_) {
2002
+ // No settings file is OK for some runtimes (codex, opencode)
2003
+ if (runtime !== 'codex' && runtime !== 'opencode') {
2004
+ settingsIssues.push(`${settingsFile} missing or unreadable`);
2005
+ settingsOk = false;
2006
+ }
2007
+ }
2008
+
2009
+ const status = missing.length > 0 ? 'broken' : modified.length > 0 ? 'modified' : 'clean';
2010
+
2011
+ return {
2012
+ status,
2013
+ version: manifest.version || null,
2014
+ total_files: totalFiles,
2015
+ missing,
2016
+ modified,
2017
+ orphaned: [],
2018
+ settings_ok: settingsOk,
2019
+ settings_issues: settingsIssues,
2020
+ };
2021
+ }
2022
+
2023
+ /**
2024
+ * CLI command: validate deployment
2025
+ * Validates PAN installations in the current directory.
2026
+ * @param {string} cwd
2027
+ * @param {boolean} raw
2028
+ */
2029
+ function cmdValidateDeployment(cwd, raw) {
2030
+ const runtimes = detectInstalledRuntimes(cwd);
2031
+ if (runtimes.length === 0) {
2032
+ output({ error: 'No PAN installations found in this directory' }, raw);
2033
+ return;
2034
+ }
2035
+
2036
+ const results = {};
2037
+ let overallStatus = 'clean';
2038
+
2039
+ for (const { runtime, configDir } of runtimes) {
2040
+ const result = validateRuntimeInstall(cwd, configDir, runtime);
2041
+ results[runtime] = result;
2042
+ if (result.status === 'broken') overallStatus = 'broken';
2043
+ else if (result.status === 'modified' && overallStatus !== 'broken') overallStatus = 'modified';
2044
+ }
2045
+
2046
+ const summary = {
2047
+ status: overallStatus,
2048
+ runtimes_found: runtimes.length,
2049
+ runtimes: results,
2050
+ };
2051
+
2052
+ const rawLines = [`Deployment status: ${overallStatus} (${runtimes.length} runtimes)`];
2053
+ for (const [rt, r] of Object.entries(results)) {
2054
+ rawLines.push(` ${rt}: ${r.status} (${r.total_files} files, ${r.missing.length} missing, ${r.modified.length} modified)`);
2055
+ }
2056
+
2057
+ output(summary, raw, rawLines.join('\n'));
2058
+ }
2059
+
1786
2060
  module.exports = {
1787
2061
  cmdVerifySummary,
1788
2062
  cmdVerifyPlanStructure,
@@ -1805,4 +2079,9 @@ module.exports = {
1805
2079
  countRoadmapPhases,
1806
2080
  groupGapPatterns,
1807
2081
  checkVerificationGate,
2082
+ cmdValidateDeployment,
2083
+ validateRuntimeInstall,
2084
+ detectInstalledRuntimes,
2085
+ syncRequirementCheckboxes,
2086
+ syncRoadmapPlanCheckboxes,
1808
2087
  };
@@ -309,7 +309,14 @@ async function main() {
309
309
  }
310
310
 
311
311
  case 'resolve-model': {
312
- commands.cmdResolveModel(cwd, args[1], raw);
312
+ const metadataIdx = args.indexOf('--metadata');
313
+ const metadataJson = metadataIdx !== -1 ? args[metadataIdx + 1] : undefined;
314
+ commands.cmdResolveModel(cwd, args[1], raw, metadataJson);
315
+ break;
316
+ }
317
+
318
+ case 'estimate-cost': {
319
+ commands.cmdEstimateCost(cwd, raw);
313
320
  break;
314
321
  }
315
322
 
@@ -534,8 +541,10 @@ async function main() {
534
541
  const fullFlag = args.includes('--full');
535
542
  const driftFlag = args.includes('--drift');
536
543
  verify.cmdValidateHealth(cwd, { repair: repairFlag, standards: standardsFlag, full: fullFlag, drift: driftFlag }, raw);
544
+ } else if (subcommand === 'deployment') {
545
+ verify.cmdValidateDeployment(cwd, raw);
537
546
  } else {
538
- error('Unknown validate subcommand. Available: consistency, health');
547
+ error('Unknown validate subcommand. Available: consistency, health, deployment');
539
548
  }
540
549
  break;
541
550
  }