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.
- package/README.md +4 -2
- package/bin/install.js +23 -0
- package/commands/pan/assumptions.md +38 -3
- package/commands/pan/audit-deployment.md +6 -0
- package/commands/pan/debug.md +71 -2
- package/commands/pan/exec-phase.md +90 -0
- package/commands/pan/focus-auto.md +181 -18
- package/commands/pan/focus-design.md +302 -14
- package/commands/pan/focus-doc-audit.md +530 -0
- package/commands/pan/focus-drift-walking.md +525 -0
- package/commands/pan/focus-exec.md +168 -46
- package/commands/pan/focus-plan.md +204 -12
- package/commands/pan/focus-scan.md +17 -5
- package/commands/pan/map-codebase.md +32 -6
- package/commands/pan/milestone-audit.md +23 -0
- package/commands/pan/new-project.md +64 -0
- package/commands/pan/pause.md +42 -1
- package/commands/pan/plan-phase.md +84 -0
- package/commands/pan/profile.md +2 -1
- package/commands/pan/quick.md +15 -0
- package/commands/pan/resume.md +62 -2
- package/commands/pan/verify-phase.md +42 -0
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/commands.cjs +29 -7
- package/pan-wizard-core/bin/lib/config.cjs +10 -0
- package/pan-wizard-core/bin/lib/constants.cjs +3 -1
- package/pan-wizard-core/bin/lib/core.cjs +168 -21
- package/pan-wizard-core/bin/lib/focus.cjs +5 -0
- package/pan-wizard-core/bin/lib/verify.cjs +283 -4
- package/pan-wizard-core/bin/pan-tools.cjs +11 -2
- package/pan-wizard-core/references/model-profiles.md +191 -62
- package/pan-wizard-core/workflows/help.md +11 -1
- package/pan-wizard-core/workflows/profile.md +8 -1
- package/pan-wizard-core/workflows/settings.md +14 -0
- 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: '
|
|
32
|
-
'pan-roadmapper': { quality: '
|
|
33
|
-
'pan-executor': { quality: '
|
|
34
|
-
'pan-phase-researcher': { quality: '
|
|
35
|
-
'pan-project-researcher': { quality: '
|
|
36
|
-
'pan-research-synthesizer': { quality: '
|
|
37
|
-
'pan-debugger': { quality: '
|
|
38
|
-
'pan-document_code': { quality: '
|
|
39
|
-
'pan-verifier': { quality: '
|
|
40
|
-
'pan-plan-checker': { quality: '
|
|
41
|
-
'pan-integration-checker': { quality: '
|
|
42
|
-
'pan-reviewer': { quality: '
|
|
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
|
-
*
|
|
489
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
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 '
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|