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.
- package/README.md +4 -2
- package/bin/install.js +23 -0
- package/commands/pan/focus-design.md +235 -12
- package/commands/pan/focus-doc-audit.md +530 -0
- package/commands/pan/focus-drift-walking.md +525 -0
- package/commands/pan/focus-plan.md +204 -12
- package/commands/pan/profile.md +2 -1
- 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/core.cjs +168 -21
- 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
|
@@ -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
|
}
|
|
@@ -1,52 +1,142 @@
|
|
|
1
|
-
# Model Profiles
|
|
1
|
+
# Model Profiles & Routing
|
|
2
2
|
|
|
3
|
-
Model profiles control which
|
|
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 |
|
|
10
|
-
| pan-roadmapper |
|
|
11
|
-
| pan-executor |
|
|
12
|
-
| pan-phase-researcher |
|
|
13
|
-
| pan-project-researcher |
|
|
14
|
-
| pan-research-synthesizer |
|
|
15
|
-
| pan-debugger |
|
|
16
|
-
| pan-document_code |
|
|
17
|
-
| pan-verifier |
|
|
18
|
-
| pan-plan-checker |
|
|
19
|
-
| pan-integration-checker |
|
|
20
|
-
| pan-reviewer |
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
**quality**
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 | 1× |
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
156
|
+
---
|
|
67
157
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
Runtime: `/pan:profile <profile>`
|
|
211
|
+
|
|
212
|
+
### Downgrade Confirmation
|
|
80
213
|
|
|
81
214
|
| Direction | Example | Behavior |
|
|
82
215
|
|-----------|---------|----------|
|
|
83
|
-
| Downgrade | quality → balanced |
|
|
84
|
-
| Downgrade | balanced → budget |
|
|
85
|
-
| Upgrade | budget → balanced |
|
|
86
|
-
|
|
|
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
|
-
|
|
223
|
+
---
|
|
92
224
|
|
|
93
225
|
## Design Rationale
|
|
94
226
|
|
|
95
|
-
**Why
|
|
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
|
|
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
|
|
102
|
-
Verification requires goal-backward reasoning
|
|
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
|
|
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
|
|
108
|
-
Code review is pattern-matching against known conventions and security rules.
|
|
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.
|