pan-wizard 2.8.1 → 2.9.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.
@@ -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
  }
@@ -1,52 +1,142 @@
1
- # Model Profiles
1
+ # Model Profiles & Routing
2
2
 
3
- Model profiles control which Claude model each PAN agent uses. This allows balancing quality vs token spend.
3
+ Model profiles control which model tier each PAN agent uses. The routing system maps abstract tiers to provider-specific models, allowing PAN to work across Anthropic, OpenAI, and Google providers.
4
+
5
+ ---
6
+
7
+ ## Tier System
8
+
9
+ PAN uses three abstract tiers instead of hardcoded model names:
10
+
11
+ | Tier | Purpose | Anthropic | OpenAI | Google |
12
+ |------|---------|-----------|--------|--------|
13
+ | `reasoning` | Architecture, planning, complex decisions | inherit (Opus) | inherit | inherit |
14
+ | `mid` | Execution, research, verification | Sonnet | mid | mid |
15
+ | `fast` | Read-only extraction, budget tasks | Haiku | fast | fast |
16
+
17
+ **Why `inherit` for reasoning?** Host runtimes map "opus" to a specific model version. PAN returns `inherit` for reasoning-tier agents, so they use whatever top-tier model the user has configured. This avoids version conflicts and silent fallbacks.
18
+
19
+ ### Legacy Aliases
20
+
21
+ For backward compatibility, legacy Anthropic model names still work:
22
+
23
+ | Legacy Name | Maps To | Tier |
24
+ |-------------|---------|------|
25
+ | `opus` | `reasoning` | Top-tier |
26
+ | `sonnet` | `mid` | Mid-tier |
27
+ | `haiku` | `fast` | Budget |
28
+
29
+ ---
4
30
 
5
31
  ## Profile Definitions
6
32
 
7
33
  | Agent | `quality` | `balanced` | `budget` |
8
34
  |-------|-----------|------------|----------|
9
- | pan-planner | opus | opus | sonnet |
10
- | pan-roadmapper | opus | sonnet | sonnet |
11
- | pan-executor | opus | sonnet | sonnet |
12
- | pan-phase-researcher | opus | sonnet | haiku |
13
- | pan-project-researcher | opus | sonnet | haiku |
14
- | pan-research-synthesizer | opus | sonnet | haiku |
15
- | pan-debugger | opus | sonnet | sonnet |
16
- | pan-document_code | opus | haiku | haiku |
17
- | pan-verifier | opus | sonnet | haiku |
18
- | pan-plan-checker | opus | sonnet | haiku |
19
- | pan-integration-checker | opus | sonnet | haiku |
20
- | pan-reviewer | opus | haiku | haiku |
21
-
22
- ## Profile Philosophy
23
-
24
- **quality** - Maximum reasoning power (Opus 4.6 for everything)
25
- - Opus for ALL agents no exceptions
26
- - Use when: quota available, critical architecture work, maximum quality desired
27
-
28
- **balanced** (default) - Smart allocation
29
- - Opus only for planning (where architecture decisions happen)
30
- - Sonnet for execution and research (follows explicit instructions)
31
- - Sonnet for verification (needs reasoning, not just pattern matching)
32
- - Use when: normal development, good balance of quality and cost
33
-
34
- **budget** - Minimal Opus usage
35
- - Sonnet for anything that writes code
36
- - Haiku for research and verification
37
- - Use when: conserving quota, high-volume work, less critical phases
38
-
39
- ## Resolution Logic
40
-
41
- Orchestrators resolve model before spawning:
35
+ | pan-planner | reasoning | reasoning | mid |
36
+ | pan-roadmapper | reasoning | mid | mid |
37
+ | pan-executor | reasoning | mid | mid |
38
+ | pan-phase-researcher | reasoning | mid | fast |
39
+ | pan-project-researcher | reasoning | mid | fast |
40
+ | pan-research-synthesizer | reasoning | mid | fast |
41
+ | pan-debugger | reasoning | mid | mid |
42
+ | pan-document_code | reasoning | fast | fast |
43
+ | pan-verifier | reasoning | mid | fast |
44
+ | pan-plan-checker | reasoning | mid | fast |
45
+ | pan-integration-checker | reasoning | mid | fast |
46
+ | pan-reviewer | reasoning | fast | fast |
47
+
48
+ ### Profile Philosophy
49
+
50
+ **quality** Maximum reasoning power
51
+ - Reasoning tier for ALL agents. Use when quota is available, critical architecture work, or maximum quality is desired.
52
+
53
+ **balanced** (default) — Smart allocation
54
+ - Reasoning only for planning (where architecture decisions happen). Mid for execution. Fast for read-only tasks. Good balance of quality and cost.
55
+
56
+ **budget** Minimal token spend
57
+ - Mid for anything that writes code. Fast for research and verification. Use for high-volume work or less critical phases.
58
+
59
+ ### Cost Multipliers
60
+
61
+ Relative cost per tier (fast = 1× baseline):
62
+
63
+ | Tier | Multiplier |
64
+ |------|------------|
65
+ | reasoning | 15× |
66
+ | mid | 3× |
67
+ | fast | |
68
+
69
+ Use `/pan:profile <profile>` to see estimated cost differences before switching.
42
70
 
71
+ ---
72
+
73
+ ## Routing Pipeline
74
+
75
+ Model resolution follows this priority chain:
76
+
77
+ ```
78
+ 1. Per-agent override (model_overrides in config.json) ← highest priority
79
+ 2. Per-phase override (<!-- model_tier: X --> in roadmap)
80
+ 3. Complexity routing (if strategy = "complexity")
81
+ 4. Profile lookup (MODEL_PROFILES[agent][profile]) ← lowest priority
82
+ ```
83
+
84
+ ### Provider Detection
85
+
86
+ PAN auto-detects the LLM provider to map tiers to the right model names:
87
+
88
+ 1. **Explicit config** — `routing.provider` in config.json (if not `"auto"`)
89
+ 2. **Environment variable** — `PAN_PROVIDER` env var
90
+ 3. **Runtime directory** — `.claude/` → Anthropic, `.codex/` → OpenAI, `.gemini/` → Google
91
+ 4. **Fallback** — Default provider map (Anthropic-style names)
92
+
93
+ ---
94
+
95
+ ## Routing Strategies
96
+
97
+ Set in `.planning/config.json` under the `routing` section:
98
+
99
+ ### Static (default)
100
+
101
+ ```json
102
+ {
103
+ "routing": {
104
+ "strategy": "static"
105
+ }
106
+ }
43
107
  ```
44
- 1. Read .planning/config.json
45
- 2. Check model_overrides for agent-specific override
46
- 3. If no override, look up agent in profile table
47
- 4. Pass model parameter to Task call
108
+
109
+ Every agent always gets the tier assigned by its profile. Predictable and simple.
110
+
111
+ ### Complexity
112
+
113
+ ```json
114
+ {
115
+ "routing": {
116
+ "strategy": "complexity",
117
+ "complexity_thresholds": {
118
+ "downgrade_max": 2,
119
+ "upgrade_min": 6
120
+ }
121
+ }
122
+ }
48
123
  ```
49
124
 
125
+ Adjusts tiers up or down based on task metadata:
126
+
127
+ | Factor | Score 0 | Score 1 | Score 2 | Score 3 |
128
+ |--------|---------|---------|---------|---------|
129
+ | fileCount | ≤5 | 6–15 | >15 | — |
130
+ | waveCount | ≤1 | 2–3 | >3 | — |
131
+ | requirementCount | ≤2 | 3–5 | >5 | — |
132
+ | isArchitectural | false | — | — | true |
133
+
134
+ - Score ≤ `downgrade_max` (default 2): tier steps down one level (e.g., mid → fast)
135
+ - Score ≥ `upgrade_min` (default 6): tier steps up one level (e.g., mid → reasoning)
136
+ - Otherwise: tier stays as assigned by profile
137
+
138
+ ---
139
+
50
140
  ## Per-Agent Overrides
51
141
 
52
142
  Override specific agents without changing the entire profile:
@@ -61,51 +151,90 @@ Override specific agents without changing the entire profile:
61
151
  }
62
152
  ```
63
153
 
64
- Overrides take precedence over the profile. Valid values: `opus`, `sonnet`, `haiku`.
154
+ Overrides accept tier names (`reasoning`, `mid`, `fast`) or legacy names (`opus`, `sonnet`, `haiku`). They take highest priority — above per-phase overrides, complexity routing, and profile lookup.
65
155
 
66
- ## Switching Profiles
156
+ ---
67
157
 
68
- Runtime: `/pan:profile <profile>`
158
+ ## Per-Phase Overrides
159
+
160
+ Override the model tier for all agents within a specific roadmap phase by adding an HTML comment to the phase section:
161
+
162
+ ```markdown
163
+ ## Phase 3: Quick UI polish
164
+ **Goal:** Style cleanup
165
+ <!-- model_tier: fast -->
166
+ ```
167
+
168
+ When an orchestrator passes `phaseNum` in task metadata, the routing pipeline checks the roadmap phase for a `model_tier` comment. This lets you use a cheaper tier for simple phases without changing the global profile.
169
+
170
+ Valid values: `reasoning`, `mid`, `fast`, `opus`, `sonnet`, `haiku`.
171
+
172
+ ---
173
+
174
+ ## Configuration Reference
175
+
176
+ Full routing config in `.planning/config.json`:
69
177
 
70
- Per-project default: Set in `.planning/config.json`:
71
178
  ```json
72
179
  {
73
- "model_profile": "balanced"
180
+ "model_profile": "balanced",
181
+ "model_overrides": {
182
+ "pan-executor": "opus"
183
+ },
184
+ "routing": {
185
+ "strategy": "static",
186
+ "provider": "auto",
187
+ "cascade_quality_gate": true,
188
+ "complexity_thresholds": {
189
+ "downgrade_max": 2,
190
+ "upgrade_min": 6
191
+ }
192
+ }
74
193
  }
75
194
  ```
76
195
 
77
- ### Downgrade Confirmation
196
+ | Field | Values | Default | Description |
197
+ |-------|--------|---------|-------------|
198
+ | `model_profile` | `quality`, `balanced`, `budget` | `balanced` | Base tier assignment for all agents |
199
+ | `model_overrides` | `{ agent: tier }` | `{}` | Per-agent tier override |
200
+ | `routing.strategy` | `static`, `complexity` | `static` | How tiers are adjusted at runtime |
201
+ | `routing.provider` | `auto`, `anthropic`, `openai`, `google` | `auto` | LLM provider for tier→model mapping |
202
+ | `routing.cascade_quality_gate` | `true`, `false` | `true` | Reserved for future cascade routing |
203
+ | `routing.complexity_thresholds.downgrade_max` | number | `2` | Max complexity score to downgrade tier |
204
+ | `routing.complexity_thresholds.upgrade_min` | number | `6` | Min complexity score to upgrade tier |
205
+
206
+ ---
207
+
208
+ ## Switching Profiles
78
209
 
79
- When switching to a **lower-tier** profile, PAN asks for confirmation:
210
+ Runtime: `/pan:profile <profile>`
211
+
212
+ ### Downgrade Confirmation
80
213
 
81
214
  | Direction | Example | Behavior |
82
215
  |-----------|---------|----------|
83
- | Downgrade | quality → balanced | ⚠️ Confirmation required |
84
- | Downgrade | balanced → budget | ⚠️ Confirmation required |
85
- | Upgrade | budget → balanced | Proceeds silently |
86
- | Upgrade | balanced → quality | Proceeds silently |
87
- | Same | balanced → balanced | ✓ Proceeds silently |
216
+ | Downgrade | quality → balanced | Confirmation required |
217
+ | Downgrade | balanced → budget | Confirmation required |
218
+ | Upgrade | budget → balanced | Proceeds silently |
219
+ | Same | balanced → balanced | Proceeds silently |
88
220
 
89
221
  **Tier Order:** `quality` (3) > `balanced` (2) > `budget` (1)
90
222
 
91
- This prevents accidental quality reductions. Upgrades proceed without prompting since higher quality is always safe.
223
+ ---
92
224
 
93
225
  ## Design Rationale
94
226
 
95
- **Why Opus for pan-planner?**
227
+ **Why reasoning for pan-planner?**
96
228
  Planning involves architecture decisions, goal decomposition, and task design. This is where model quality has the highest impact.
97
229
 
98
- **Why Sonnet for pan-executor?**
230
+ **Why mid for pan-executor?**
99
231
  Executors follow explicit PLAN.md instructions. The plan already contains the reasoning; execution is implementation.
100
232
 
101
- **Why Sonnet (not Haiku) for verifiers in balanced?**
102
- Verification requires goal-backward reasoning - checking if code *delivers* what the phase promised, not just pattern matching. Sonnet handles this well; Haiku may miss subtle gaps.
233
+ **Why mid (not fast) for verifiers in balanced?**
234
+ Verification requires goal-backward reasoning checking if code *delivers* what the phase promised, not just pattern matching.
103
235
 
104
- **Why Haiku for pan-document_code?**
236
+ **Why fast for pan-document_code?**
105
237
  Read-only exploration and pattern extraction. No reasoning required, just structured output from file contents.
106
238
 
107
- **Why Haiku for pan-reviewer in balanced?**
108
- Code review is pattern-matching against known conventions and security rules. Haiku handles checklist-style verification efficiently. Quality profile uses Sonnet for nuanced review when accuracy matters most.
109
-
110
- **Why `inherit` instead of passing `opus` directly?**
111
- Claude Code's `"opus"` alias maps to a specific model version. Organizations may block older opus versions while allowing newer ones. PAN returns `"inherit"` for opus-tier agents, causing them to use whatever opus version the user has configured in their session. This avoids version conflicts and silent fallbacks to Sonnet.
239
+ **Why fast for pan-reviewer in balanced?**
240
+ Code review is pattern-matching against known conventions and security rules. Fast handles checklist-style verification efficiently.