pan-wizard 3.8.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +80 -9
  2. package/agents/pan-conductor.md +15 -3
  3. package/agents/pan-counterfactual.md +1 -2
  4. package/agents/pan-debugger.md +1 -2
  5. package/agents/pan-distiller.md +1 -2
  6. package/agents/pan-document_code.md +1 -0
  7. package/agents/pan-executor.md +1 -0
  8. package/agents/pan-experiment-runner.md +1 -2
  9. package/agents/pan-hardener.md +1 -2
  10. package/agents/pan-integration-checker.md +1 -2
  11. package/agents/pan-knowledge.md +1 -2
  12. package/agents/pan-meta-reviewer.md +1 -2
  13. package/agents/pan-optimizer.md +1 -0
  14. package/agents/pan-phase-researcher.md +1 -0
  15. package/agents/pan-plan-checker.md +1 -2
  16. package/agents/pan-planner.md +1 -0
  17. package/agents/pan-previewer.md +1 -2
  18. package/agents/pan-project-researcher.md +6 -0
  19. package/agents/pan-release.md +58 -0
  20. package/agents/pan-research-synthesizer.md +7 -0
  21. package/agents/pan-reviewer.md +2 -3
  22. package/agents/pan-roadmapper.md +1 -0
  23. package/agents/pan-verifier.md +1 -2
  24. package/assets/pan-avatar.png +0 -0
  25. package/assets/pan-developer.png +0 -0
  26. package/assets/pan-docs-header.png +0 -0
  27. package/assets/pan-hero.png +0 -0
  28. package/assets/pan-logo-2000-transparent.svg +11 -30
  29. package/assets/pan-logo-2000.svg +12 -43
  30. package/assets/pan-logo-lockup.svg +11 -0
  31. package/assets/pan-mark.svg +7 -0
  32. package/assets/pan-orchestration.png +0 -0
  33. package/assets/pan-readme-hero.png +0 -0
  34. package/assets/terminal.svg +39 -119
  35. package/bin/install-lib.cjs +661 -46
  36. package/bin/install.js +722 -116
  37. package/commands/pan/army.md +169 -0
  38. package/commands/pan/dashboard.md +25 -0
  39. package/commands/pan/experiment.md +2 -0
  40. package/commands/pan/focus-auto.md +32 -4
  41. package/commands/pan/hud.md +91 -0
  42. package/commands/pan/profile.md +2 -0
  43. package/hooks/dist/pan-cost-logger.js +22 -7
  44. package/package.json +5 -4
  45. package/pan-wizard-core/bin/lib/campaign.cjs +198 -0
  46. package/pan-wizard-core/bin/lib/commands-learnings.cjs +544 -0
  47. package/pan-wizard-core/bin/lib/commands.cjs +12 -523
  48. package/pan-wizard-core/bin/lib/constants.cjs +8 -0
  49. package/pan-wizard-core/bin/lib/core.cjs +80 -0
  50. package/pan-wizard-core/bin/lib/cost.cjs +62 -8
  51. package/pan-wizard-core/bin/lib/focus.cjs +13 -1
  52. package/pan-wizard-core/bin/lib/git.cjs +6 -1
  53. package/pan-wizard-core/bin/lib/hud.cjs +887 -0
  54. package/pan-wizard-core/bin/lib/lock.cjs +108 -0
  55. package/pan-wizard-core/bin/lib/milestone.cjs +3 -2
  56. package/pan-wizard-core/bin/lib/phase-remove.cjs +392 -0
  57. package/pan-wizard-core/bin/lib/phase.cjs +4 -369
  58. package/pan-wizard-core/bin/lib/runner.cjs +5 -0
  59. package/pan-wizard-core/bin/lib/squads.cjs +152 -0
  60. package/pan-wizard-core/bin/lib/state.cjs +10 -1
  61. package/pan-wizard-core/bin/lib/verify-deploy.cjs +181 -0
  62. package/pan-wizard-core/bin/lib/verify-drift.cjs +255 -0
  63. package/pan-wizard-core/bin/lib/verify-preflight.cjs +261 -0
  64. package/pan-wizard-core/bin/lib/verify-retro.cjs +177 -0
  65. package/pan-wizard-core/bin/lib/verify.cjs +10 -797
  66. package/pan-wizard-core/bin/lib/worktree.cjs +123 -0
  67. package/pan-wizard-core/bin/pan-tools.cjs +78 -0
  68. package/pan-wizard-core/learnings/universal/autonomous-loop.md +56 -0
  69. package/pan-wizard-core/workflows/plan-phase.md +11 -0
  70. package/scripts/build-plugin.js +105 -0
  71. package/scripts/install-git-hooks.js +64 -0
  72. package/scripts/release-check.js +13 -2
@@ -15,6 +15,12 @@ const {
15
15
  BUILTIN_DRIFT_RULES, DRIFT_VERDICTS, BINARY_EXTENSIONS, DRIFT_MAX_FILES, DRIFT_MAX_FILE_SIZE, DRIFT_SEVERITY_WEIGHTS,
16
16
  } = require('./constants.cjs');
17
17
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, fileAccessible } = require('./utils.cjs');
18
+ // Drift detection lives in verify-drift.cjs; re-exported below so consumers of
19
+ // verify.cjs are unaffected by the decomposition.
20
+ const { runDriftCheck, parseConventionRules, checkFileConventions, calculateDriftScore, getChangedFiles, cmdDriftCheck } = require('./verify-drift.cjs');
21
+ const { collectVerificationStats, countRoadmapPhases, groupGapPatterns, cmdRetro } = require('./verify-retro.cjs');
22
+ const { detectInstalledRuntimes, validateRuntimeInstall, cmdValidateDeployment } = require('./verify-deploy.cjs');
23
+ const { cmdPreflight, cmdDepsValidate } = require('./verify-preflight.cjs');
18
24
 
19
25
  /**
20
26
  * Spot-check files mentioned in summary content.
@@ -1313,805 +1319,12 @@ function runFullBuildCheck(cwd) {
1313
1319
  }
1314
1320
  }
1315
1321
 
1316
- /**
1317
- * Pre-flight validation: check execution prerequisites before starting work.
1318
- * Validates state consistency, git cleanliness, blockers, and error patterns.
1319
- * @param {string} cwd - Working directory path
1320
- * @param {string|null} target - Optional target (phase number or 'batch')
1321
- * @param {boolean} raw - If true, output raw value instead of JSON
1322
- * @returns {void}
1323
- */
1324
- function cmdPreflight(cwd, target, raw) {
1325
- const checks = [];
1326
- const blockers = [];
1327
-
1328
- // Check 1: .planning/ directory exists
1329
- const planDir = planningPath(cwd);
1330
- if (fileAccessible(planDir)) {
1331
- checks.push({ name: 'planning_dir', passed: true });
1332
- } else {
1333
- checks.push({ name: 'planning_dir', passed: false, detail: '.planning/ directory not found' });
1334
- blockers.push('.planning/ directory not found — run /pan:new-project');
1335
- }
1336
-
1337
- // Check 2: state.md is parseable
1338
- const statePath = path.join(planDir, STATE_FILE);
1339
- const stateContent = readStateSafe(statePath);
1340
- if (stateContent) {
1341
- checks.push({ name: 'state_readable', passed: true });
1342
- } else {
1343
- checks.push({ name: 'state_readable', passed: false, detail: 'state.md not found or unreadable' });
1344
- blockers.push('state.md not found — run /pan:new-project');
1345
- }
1346
-
1347
- // Check 3: no unresolved blockers in state.md
1348
- if (stateContent) {
1349
- const blockersMatch = stateContent.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
1350
- const activeBlockers = [];
1351
- if (blockersMatch) {
1352
- const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
1353
- for (const item of items) {
1354
- const text = item.replace(/^-\s+/, '').trim();
1355
- if (text && !/^none$/i.test(text)) {
1356
- activeBlockers.push(text);
1357
- }
1358
- }
1359
- }
1360
- if (activeBlockers.length === 0) {
1361
- checks.push({ name: 'no_blockers', passed: true });
1362
- } else {
1363
- checks.push({ name: 'no_blockers', passed: false, detail: activeBlockers.length + ' active blocker(s)' });
1364
- for (const b of activeBlockers) blockers.push('Blocker: ' + b);
1365
- }
1366
- }
1367
-
1368
- // Check 4: git working tree is clean
1369
- const gitResult = execGit(cwd, ['status', '--porcelain']);
1370
- if (gitResult.exitCode !== 0) {
1371
- checks.push({ name: 'git_clean', passed: true, detail: 'not a git repo or git unavailable' });
1372
- } else {
1373
- const dirty = gitResult.stdout.split('\n').filter(l => l.trim()).length;
1374
- if (dirty === 0) {
1375
- checks.push({ name: 'git_clean', passed: true });
1376
- } else {
1377
- checks.push({ name: 'git_clean', passed: false, detail: dirty + ' uncommitted change(s)' });
1378
- blockers.push(dirty + ' uncommitted changes — commit or stash before executing');
1379
- }
1380
- }
1381
-
1382
- // Check 5: no known error patterns (check patterns.md exists and has entries)
1383
- const patternsPath = path.join(planDir, PATTERNS_FILE);
1384
- let patternCount = 0;
1385
- try {
1386
- const patternsContent = fs.readFileSync(patternsPath, 'utf-8');
1387
- const patternMatches = patternsContent.match(/^### PAT-\d+:/gm);
1388
- patternCount = patternMatches ? patternMatches.length : 0;
1389
- } catch { /* no patterns file — that's fine */ }
1390
- checks.push({ name: 'error_patterns', passed: true, detail: patternCount + ' known pattern(s)' });
1391
-
1392
- // Check 6: config.json exists
1393
- const configPath = path.join(planDir, CONFIG_FILE);
1394
- if (fileAccessible(configPath)) {
1395
- checks.push({ name: 'config_exists', passed: true });
1396
- } else {
1397
- checks.push({ name: 'config_exists', passed: false, detail: 'config.json not found' });
1398
- }
1399
-
1400
- // Check 7: target-specific checks
1401
- if (target && stateContent) {
1402
- const currentPhaseMatch = stateContent.match(/\*\*Current Phase:\*\*\s*(\S+)/);
1403
- const currentPhase = currentPhaseMatch ? currentPhaseMatch[1] : null;
1404
- if (target === 'batch') {
1405
- // Check that a batch file exists
1406
- const focusDir = path.join(planDir, 'focus');
1407
- try {
1408
- const files = fs.readdirSync(focusDir).filter(f => f.startsWith('batch-') && f.endsWith('.json'));
1409
- if (files.length > 0) {
1410
- checks.push({ name: 'batch_exists', passed: true, detail: files[files.length - 1] });
1411
- } else {
1412
- checks.push({ name: 'batch_exists', passed: false, detail: 'no batch file found' });
1413
- blockers.push('No batch file — run /pan:focus-plan first');
1414
- }
1415
- } catch {
1416
- checks.push({ name: 'batch_exists', passed: false, detail: 'focus/ directory not found' });
1417
- blockers.push('No focus/ directory — run /pan:focus-scan first');
1418
- }
1419
- } else {
1420
- // target is a phase number — check the phase directory exists
1421
- const phaseResult = findPhaseInternal(cwd, target);
1422
- if (phaseResult) {
1423
- checks.push({ name: 'target_phase', passed: true, detail: 'Phase ' + target + ' found' });
1424
- } else {
1425
- checks.push({ name: 'target_phase', passed: false, detail: 'Phase ' + target + ' not found' });
1426
- blockers.push('Phase ' + target + ' directory not found');
1427
- }
1428
- }
1429
- }
1430
-
1431
- const ready = blockers.length === 0;
1432
-
1433
- output({
1434
- ready,
1435
- target: target || null,
1436
- checks,
1437
- blockers,
1438
- passed: checks.filter(c => c.passed).length,
1439
- total: checks.length,
1440
- }, raw, ready ? 'ready' : 'blocked');
1441
- }
1442
-
1443
- /**
1444
- * Dependency graph validation — cross-reference roadmap phases vs disk directories
1445
- * and requirements vs phase summaries to detect drift.
1446
- * @param {string} cwd - Working directory path
1447
- * @param {boolean} raw - If true, output raw value instead of JSON
1448
- * @returns {void}
1449
- */
1450
- function cmdDepsValidate(cwd, raw) {
1451
- const planDir = planningPath(cwd);
1452
- const issues = [];
1453
- const orphanedReqs = [];
1454
- const missingPhases = [];
1455
- const orphanedDirs = [];
1456
-
1457
- // Step 1: Parse roadmap phases
1458
- const roadmapPath = path.join(planDir, ROADMAP_FILE);
1459
- const roadmapContent = safeReadFile(roadmapPath);
1460
- const roadmapPhases = new Map(); // number -> name
1461
- if (roadmapContent) {
1462
- const headerRe = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
1463
- let match;
1464
- while ((match = headerRe.exec(roadmapContent)) !== null) {
1465
- roadmapPhases.set(match[1], match[2].trim());
1466
- }
1467
- } else {
1468
- issues.push({ type: 'warning', message: 'roadmap.md not found' });
1469
- }
1470
-
1471
- // Step 2: Scan disk phase directories
1472
- const diskPhases = new Map(); // number -> dirName
1473
- try {
1474
- const entries = fs.readdirSync(phasesPath(cwd), { withFileTypes: true });
1475
- for (const entry of entries) {
1476
- if (entry.isDirectory()) {
1477
- const dirMatch = entry.name.match(PHASE_DIR_RE);
1478
- if (dirMatch) {
1479
- diskPhases.set(dirMatch[1], entry.name);
1480
- }
1481
- }
1482
- }
1483
- } catch { /* phases dir missing */ }
1484
-
1485
- // Step 3: Cross-reference roadmap vs disk
1486
- for (const [num, name] of roadmapPhases) {
1487
- if (!diskPhases.has(num)) {
1488
- missingPhases.push({ number: num, name, source: 'roadmap' });
1489
- issues.push({ type: 'error', message: 'Phase ' + num + ' (' + name + ') in roadmap but no directory on disk' });
1490
- }
1491
- }
1492
- for (const [num, dirName] of diskPhases) {
1493
- if (!roadmapPhases.has(num)) {
1494
- orphanedDirs.push({ number: num, directory: dirName });
1495
- issues.push({ type: 'warning', message: 'Directory ' + dirName + ' exists on disk but Phase ' + num + ' not found in roadmap' });
1496
- }
1497
- }
1498
-
1499
- // Step 4: Parse requirements
1500
- const reqPath = path.join(planDir, 'requirements.md');
1501
- const reqContent = safeReadFile(reqPath);
1502
- const allReqIds = [];
1503
- const completedReqIds = new Set();
1504
- if (reqContent) {
1505
- // Find all REQ-NN patterns in checkbox lines
1506
- const reqLines = reqContent.match(/^-\s*\[[ x]\]\s*\*\*([A-Z]+-\d+)\*\*/gmi) || [];
1507
- for (const line of reqLines) {
1508
- const idMatch = line.match(/\*\*([A-Z]+-\d+)\*\*/i);
1509
- if (idMatch) {
1510
- allReqIds.push(idMatch[1]);
1511
- if (/\[x\]/i.test(line)) {
1512
- completedReqIds.add(idMatch[1]);
1513
- }
1514
- }
1515
- }
1516
- }
1517
-
1518
- // Step 5: Check requirements traceability — find REQ IDs mentioned in summaries
1519
- const tracedReqIds = new Set();
1520
- for (const [, dirName] of diskPhases) {
1521
- try {
1522
- const files = fs.readdirSync(path.join(phasesPath(cwd), dirName));
1523
- for (const file of filterSummaryFiles(files)) {
1524
- const summaryContent = safeReadFile(path.join(phasesPath(cwd), dirName, file));
1525
- if (summaryContent) {
1526
- const mentions = summaryContent.match(/[A-Z]+-\d+/g) || [];
1527
- for (const id of mentions) {
1528
- if (allReqIds.includes(id)) tracedReqIds.add(id);
1529
- }
1530
- }
1531
- }
1532
- } catch { /* unreadable dir */ }
1533
- }
1534
-
1535
- // Find requirements that are neither completed nor traced in any summary
1536
- for (const reqId of allReqIds) {
1537
- if (!completedReqIds.has(reqId) && !tracedReqIds.has(reqId)) {
1538
- orphanedReqs.push(reqId);
1539
- issues.push({ type: 'info', message: 'Requirement ' + reqId + ' not marked complete and not referenced in any summary' });
1540
- }
1541
- }
1542
-
1543
- const valid = issues.filter(i => i.type === 'error').length === 0;
1544
-
1545
- output({
1546
- valid,
1547
- issues,
1548
- roadmap_phases: roadmapPhases.size,
1549
- disk_phases: diskPhases.size,
1550
- requirements_total: allReqIds.length,
1551
- requirements_completed: completedReqIds.size,
1552
- orphaned_reqs: orphanedReqs,
1553
- missing_phases: missingPhases,
1554
- orphaned_dirs: orphanedDirs,
1555
- }, raw, valid ? 'valid' : 'issues found');
1556
- }
1557
-
1558
- // ─── Drift detection ─────────────────────────────────────────────────────────
1559
-
1560
- /**
1561
- * Parse convention rules from CONVENTIONS.md markdown content.
1562
- * Extracts anti-pattern rules from prose containing "instead of", "not", "never".
1563
- * Run drift check internally and return result object (no output).
1564
- * Used by cmdValidateHealth --drift.
1565
- */
1566
- function runDriftCheck(cwd) {
1567
- const conventionsPath = path.join(planningPath(cwd), 'codebase', 'CONVENTIONS.md');
1568
- const conventionsContent = safeReadFile(conventionsPath);
1569
- const claudeMdContent = safeReadFile(path.join(cwd, 'CLAUDE.md'));
1570
- const combined = [conventionsContent, claudeMdContent].filter(Boolean).join('\n');
1571
- const rules = parseConventionRules(combined || null);
1572
- const files = getChangedFiles(cwd);
1573
- const allViolations = [];
1574
- let filesChecked = 0;
1575
- for (const filePath of files) {
1576
- const fullPath = path.join(cwd, filePath);
1577
- try {
1578
- const stat = fs.statSync(fullPath);
1579
- if (stat.size > DRIFT_MAX_FILE_SIZE) continue;
1580
- } catch { continue; }
1581
- const content = safeReadFile(fullPath);
1582
- if (!content) continue;
1583
- filesChecked++;
1584
- allViolations.push(...checkFileConventions(filePath, content, rules));
1585
- }
1586
- const { score, verdict } = calculateDriftScore(allViolations, filesChecked, rules.length);
1587
- return { drift_score: score, verdict, violation_count: allViolations.length, files_checked: filesChecked };
1588
- }
1322
+ // ─── Preflight + deps validation — extracted to verify-preflight.cjs (re-exported below)
1589
1323
 
1590
- /**
1591
- * Always merges with BUILTIN_DRIFT_RULES.
1592
- * @param {string|null} content - Markdown content from CONVENTIONS.md
1593
- * @returns {Array} Array of rule objects {id, antiPattern, message, severity, fileGlob}
1594
- */
1595
- function parseConventionRules(content) {
1596
- const parsed = [];
1597
- if (content) {
1598
- // Match lines with inline code containing negation patterns
1599
- const lines = content.split(/\r?\n/);
1600
- for (const line of lines) {
1601
- // Pattern: "Use X instead of `Y`" or "Never use `Y`" or "Don't use `Y`"
1602
- const negMatch = line.match(/(?:instead of|never use|don'?t use|avoid|not)\s+`([^`]+)`/i);
1603
- if (negMatch) {
1604
- const raw = negMatch[1].trim();
1605
- try {
1606
- const antiPattern = new RegExp('\\b' + raw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
1607
- parsed.push({
1608
- id: 'conv-' + raw.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase().slice(0, 30),
1609
- antiPattern,
1610
- message: line.trim().slice(0, 120),
1611
- severity: 'warning',
1612
- fileGlob: null,
1613
- });
1614
- } catch { /* invalid regex — skip */ }
1615
- }
1616
- }
1617
- }
1618
- // Merge: parsed + builtins, dedup by id (parsed takes priority)
1619
- const ids = new Set(parsed.map(r => r.id));
1620
- for (const rule of BUILTIN_DRIFT_RULES) {
1621
- if (!ids.has(rule.id)) parsed.push(rule);
1622
- }
1623
- return parsed;
1624
- }
1324
+ // ─── Drift detection — extracted to verify-drift.cjs (re-exported below) ─────
1625
1325
 
1626
- /**
1627
- * Check a single file's content against convention rules.
1628
- * @param {string} filePath - Relative file path (for glob matching and output)
1629
- * @param {string} content - File content
1630
- * @param {Array} rules - Convention rules from parseConventionRules
1631
- * @returns {Array} violations [{file, line, rule, message, severity}]
1632
- */
1633
- function checkFileConventions(filePath, content, rules) {
1634
- const violations = [];
1635
- const lines = content.split(/\r?\n/);
1636
- for (const rule of rules) {
1637
- // Check fileGlob match (simple endsWith check — zero-dep)
1638
- if (rule.fileGlob && !filePath.endsWith(rule.fileGlob)) continue;
1639
- for (let i = 0; i < lines.length; i++) {
1640
- const line = lines[i];
1641
- // Skip comment-only lines
1642
- if (/^\s*(\/\/|\/?\*|\*)/.test(line)) continue;
1643
- if (rule.antiPattern.test(line)) {
1644
- violations.push({
1645
- file: toPosix(filePath),
1646
- line: i + 1,
1647
- rule: rule.id,
1648
- message: rule.message,
1649
- severity: rule.severity,
1650
- });
1651
- }
1652
- }
1653
- }
1654
- return violations;
1655
- }
1656
-
1657
- /**
1658
- * Calculate drift score from violations.
1659
- * @param {Array} violations - All violations across checked files
1660
- * @param {number} filesChecked - Number of files checked
1661
- * @param {number} rulesCount - Total rules applied
1662
- * @returns {{score: number, verdict: string}}
1663
- */
1664
- function calculateDriftScore(violations, filesChecked, rulesCount) {
1665
- if (filesChecked === 0 || rulesCount === 0) return { score: 0, verdict: 'clean' };
1666
- let weighted = 0;
1667
- for (const v of violations) {
1668
- weighted += DRIFT_SEVERITY_WEIGHTS[v.severity] || 0;
1669
- }
1670
- const ceiling = filesChecked * rulesCount * 0.3;
1671
- const score = Math.min(1.0, weighted / Math.max(ceiling, 1));
1672
- const rounded = Math.round(score * 100) / 100;
1673
- const verdict = DRIFT_VERDICTS.find(b => rounded <= b.max)?.verdict || 'high';
1674
- return { score: rounded, verdict };
1675
- }
1676
-
1677
- /**
1678
- * Get list of changed files from git diff.
1679
- * @param {string} cwd - Working directory
1680
- * @param {string} [sinceRef] - Git ref to diff against (default: HEAD)
1681
- * @returns {string[]} Array of relative file paths
1682
- */
1683
- function getChangedFiles(cwd, sinceRef) {
1684
- const ref = sinceRef || 'HEAD';
1685
- // Try staged + unstaged first, then diff against ref
1686
- const result = execGit(cwd, ['diff', '--name-only', ref]);
1687
- if (result.exitCode !== 0) return [];
1688
- const stagedResult = execGit(cwd, ['diff', '--name-only', '--cached']);
1689
- const allFiles = new Set();
1690
- for (const line of result.stdout.split(/\r?\n/)) {
1691
- if (line.trim()) allFiles.add(line.trim());
1692
- }
1693
- if (stagedResult.exitCode === 0) {
1694
- for (const line of stagedResult.stdout.split(/\r?\n/)) {
1695
- if (line.trim()) allFiles.add(line.trim());
1696
- }
1697
- }
1698
- // Filter out binary extensions and limit
1699
- const filtered = [];
1700
- for (const f of allFiles) {
1701
- const ext = path.extname(f).toLowerCase();
1702
- if (BINARY_EXTENSIONS.has(ext)) continue;
1703
- filtered.push(f);
1704
- if (filtered.length >= DRIFT_MAX_FILES) break;
1705
- }
1706
- return filtered;
1707
- }
1708
-
1709
- /**
1710
- * Run drift check on changed files against project conventions.
1711
- * @param {string} cwd - Working directory
1712
- * @param {boolean} raw - Raw output mode
1713
- * @param {string[]} args - CLI arguments
1714
- */
1715
- function cmdDriftCheck(cwd, raw, args) {
1716
- // Parse flags
1717
- let sinceRef = null;
1718
- let threshold = 0.5;
1719
- let specificFiles = null;
1720
- const verbose = process.env.PAN_VERBOSE === '1';
1721
- for (let i = 0; i < args.length; i++) {
1722
- if (args[i] === '--since' && args[i + 1]) { sinceRef = args[++i]; }
1723
- else if (args[i] === '--threshold' && args[i + 1]) {
1724
- const t = parseFloat(args[++i]);
1725
- if (isNaN(t) || t < 0 || t > 1) { error('threshold must be 0.0-1.0'); return; }
1726
- threshold = t;
1727
- }
1728
- else if (args[i] === '--files' && args[i + 1]) { specificFiles = args[++i].split(',').map(f => f.trim()); }
1729
- }
1730
-
1731
- // Load convention rules
1732
- const conventionsPath = path.join(planningPath(cwd), 'codebase', 'CONVENTIONS.md');
1733
- const conventionsContent = safeReadFile(conventionsPath);
1734
- const claudeMdContent = safeReadFile(path.join(cwd, 'CLAUDE.md'));
1735
- const combined = [conventionsContent, claudeMdContent].filter(Boolean).join('\n');
1736
- const rules = parseConventionRules(combined || null);
1737
-
1738
- // Get files to check
1739
- let files;
1740
- if (specificFiles) {
1741
- files = specificFiles;
1742
- } else {
1743
- files = getChangedFiles(cwd, sinceRef);
1744
- }
1745
-
1746
- // Check each file
1747
- const allViolations = [];
1748
- let filesChecked = 0;
1749
- for (const filePath of files) {
1750
- const fullPath = path.join(cwd, filePath);
1751
- try {
1752
- const stat = fs.statSync(fullPath);
1753
- if (stat.size > DRIFT_MAX_FILE_SIZE) continue;
1754
- } catch { continue; }
1755
- const content = safeReadFile(fullPath);
1756
- if (!content) continue;
1757
- filesChecked++;
1758
- const violations = checkFileConventions(filePath, content, rules);
1759
- allViolations.push(...violations);
1760
- }
1761
-
1762
- // Calculate score
1763
- const { score, verdict } = calculateDriftScore(allViolations, filesChecked, rules.length);
1764
- const passed = score <= threshold;
1765
- const summary = filesChecked === 0
1766
- ? 'no files changed'
1767
- : rules.length === 0
1768
- ? 'no conventions loaded'
1769
- : `drift: ${score} (${verdict}) — ${allViolations.length} violations in ${filesChecked} files`;
1770
-
1771
- const result = {
1772
- drift_score: score,
1773
- verdict,
1774
- passed,
1775
- threshold,
1776
- violations: allViolations,
1777
- violation_count: allViolations.length,
1778
- files_checked: filesChecked,
1779
- conventions_loaded: rules.length,
1780
- summary,
1781
- };
1782
- if (verbose) {
1783
- const byFile = {};
1784
- for (const v of allViolations) {
1785
- if (!byFile[v.file]) byFile[v.file] = [];
1786
- byFile[v.file].push({ line: v.line, rule: v.rule, message: v.message, severity: v.severity });
1787
- }
1788
- result.per_file = byFile;
1789
- }
1790
- output(result, raw, summary);
1791
- }
1792
-
1793
- // ─── Retrospective Analysis ─────────────────────────────────────────────────
1794
-
1795
- /**
1796
- * Scan verification files in a phases directory and collect stats.
1797
- * @param {string} phasesDir - Absolute path to phases directory
1798
- * @returns {{ total: number, passed: number, gaps_found: number, human_needed: number, gap_patterns: string[] }}
1799
- */
1800
- function collectVerificationStats(phasesDir) {
1801
- const stats = { total: 0, passed: 0, gaps_found: 0, human_needed: 0, gap_patterns: [] };
1802
- let dirs;
1803
- try { dirs = fs.readdirSync(phasesDir, { withFileTypes: true }); } catch { return stats; }
1804
- for (const d of dirs) {
1805
- if (!d.isDirectory()) continue;
1806
- const phaseDir = path.join(phasesDir, d.name);
1807
- let files;
1808
- try { files = fs.readdirSync(phaseDir); } catch { continue; }
1809
- for (const f of files) {
1810
- if (!isVerificationFile(f)) continue;
1811
- stats.total++;
1812
- const content = safeReadFile(path.join(phaseDir, f));
1813
- if (!content) continue;
1814
- const fm = extractFrontmatter(content);
1815
- const status = (fm.status || '').toLowerCase();
1816
- if (status === 'passed') stats.passed++;
1817
- else if (status === 'gaps_found') stats.gaps_found++;
1818
- else if (status === 'human_needed') stats.human_needed++;
1819
- // Extract gap descriptions from ## Gaps section
1820
- const gapsMatch = content.match(/## Gaps[\s\S]*?(?=\n## |$)/);
1821
- if (gapsMatch) {
1822
- const lines = gapsMatch[0].split('\n').filter(l => l.match(/^[-*]\s+/));
1823
- for (const line of lines) {
1824
- const desc = line.replace(/^[-*]\s+/, '').trim();
1825
- if (desc) stats.gap_patterns.push(desc);
1826
- }
1827
- }
1828
- }
1829
- }
1830
- return stats;
1831
- }
1832
-
1833
- /**
1834
- * Count phases from roadmap: total planned, completed, and decimal (gap closure) phases.
1835
- * @param {string} roadmapContent - Roadmap file content
1836
- * @returns {{ planned: number, completed: number, decimal_phases: number }}
1837
- */
1838
- function countRoadmapPhases(roadmapContent) {
1839
- const result = { planned: 0, completed: 0, decimal_phases: 0 };
1840
- const checkboxRe = /- \[([ x])\]\s*(?:\*\*)?Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
1841
- let m;
1842
- while ((m = checkboxRe.exec(roadmapContent)) !== null) {
1843
- result.planned++;
1844
- if (m[1] === 'x') result.completed++;
1845
- if (m[2].includes('.')) result.decimal_phases++;
1846
- }
1847
- return result;
1848
- }
1849
-
1850
- /**
1851
- * Group gap patterns by similarity (simple keyword grouping).
1852
- * @param {string[]} patterns - Raw gap descriptions
1853
- * @returns {Array<{pattern: string, count: number}>}
1854
- */
1855
- function groupGapPatterns(patterns) {
1856
- const groups = {};
1857
- for (const p of patterns) {
1858
- const key = p.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
1859
- const words = key.split(/\s+/).slice(0, 3).join(' ');
1860
- groups[words] = (groups[words] || 0) + 1;
1861
- }
1862
- return Object.entries(groups)
1863
- .map(([pattern, count]) => ({ pattern, count }))
1864
- .sort((a, b) => b.count - a.count)
1865
- .slice(0, 10);
1866
- }
1867
-
1868
- /**
1869
- * Milestone retrospective — analyze historical .planning/ data for process improvement.
1870
- * @param {string} cwd - Working directory
1871
- * @param {boolean} raw - Raw output flag
1872
- */
1873
- function cmdRetro(cwd, raw, args) {
1874
- const roadmapPath = path.join(planningPath(cwd), ROADMAP_FILE);
1875
- const roadmapContent = safeReadFile(roadmapPath);
1876
- if (!roadmapContent) {
1877
- return output({ error: 'roadmap.md not found' }, raw, 'roadmap.md not found');
1878
- }
1879
-
1880
- const phases = countRoadmapPhases(roadmapContent);
1881
- const pDir = phasesPath(cwd);
1882
- const verification = collectVerificationStats(pDir);
1883
- const gapGroups = groupGapPatterns(verification.gap_patterns);
1884
-
1885
- // Estimation accuracy: planned phases vs actual (including decimal gap closures)
1886
- const basePlanned = phases.planned - phases.decimal_phases;
1887
- const estimationAccuracy = basePlanned > 0
1888
- ? Math.round((basePlanned / phases.planned) * 100)
1889
- : 100;
1890
-
1891
- const result = {
1892
- phases_planned: phases.planned,
1893
- phases_completed: phases.completed,
1894
- phases_decimal: phases.decimal_phases,
1895
- estimation_accuracy_pct: estimationAccuracy,
1896
- verifications_total: verification.total,
1897
- verifications_passed_first_try: verification.passed,
1898
- verifications_gaps_found: verification.gaps_found,
1899
- verifications_human_needed: verification.human_needed,
1900
- first_try_rate_pct: verification.total > 0
1901
- ? Math.round((verification.passed / verification.total) * 100)
1902
- : null,
1903
- common_gap_patterns: gapGroups,
1904
- };
1905
-
1906
- // E-4: optional memory write. Top gap patterns become lessons for pan-planner
1907
- // (they surface what plans routinely miss). First-try rate deltas feed
1908
- // pan-verifier memory.
1909
- const argsList = Array.isArray(args) ? args : [];
1910
- if (argsList.includes('--write-memory')) {
1911
- const { appendMemory } = require('./memory.cjs');
1912
- const lessons_written = { 'pan-planner': 0, 'pan-verifier': 0 };
1913
- const maxIdx = argsList.indexOf('--max');
1914
- const maxLessons = maxIdx !== -1 && argsList[maxIdx + 1]
1915
- ? Math.max(1, Math.min(10, Number(argsList[maxIdx + 1]) || 3))
1916
- : 3;
1917
-
1918
- // Top N gap patterns → planner memory as single-line lessons.
1919
- const top = gapGroups.slice(0, maxLessons);
1920
- for (const g of top) {
1921
- const lesson = `Recurring plan gap (${g.count}x across phases): "${g.pattern}" — factor into plan-checker inputs`;
1922
- const r = appendMemory(cwd, 'pan-planner', lesson);
1923
- if (r.appended) lessons_written['pan-planner'] += 1;
1924
- }
1925
-
1926
- // Low first-try rate → verifier memory.
1927
- if (verification.total >= 3 && result.first_try_rate_pct != null && result.first_try_rate_pct < 60) {
1928
- const lesson = `First-try verification rate ${result.first_try_rate_pct}% over ${verification.total} runs — tighten verification criteria and pre-exec checks`;
1929
- const r = appendMemory(cwd, 'pan-verifier', lesson);
1930
- if (r.appended) lessons_written['pan-verifier'] += 1;
1931
- }
1932
-
1933
- result.memory = { wrote: lessons_written, max: maxLessons };
1934
- }
1935
-
1936
- const rawLines = [
1937
- `Phases: ${phases.completed}/${phases.planned} completed (${phases.decimal_phases} gap closures)`,
1938
- `Estimation accuracy: ${estimationAccuracy}%`,
1939
- `Verifications: ${verification.passed}/${verification.total} passed first try`,
1940
- `Gaps found: ${verification.gaps_found}, Human needed: ${verification.human_needed}`,
1941
- ];
1942
- if (gapGroups.length > 0) {
1943
- rawLines.push('Common gap patterns:');
1944
- for (const g of gapGroups) rawLines.push(` - ${g.pattern} (${g.count}x)`);
1945
- }
1946
- if (result.memory) {
1947
- rawLines.push(`Memory: wrote ${result.memory.wrote['pan-planner']} planner + ${result.memory.wrote['pan-verifier']} verifier lessons`);
1948
- }
1949
-
1950
- output(result, raw, rawLines.join('\n'));
1951
- }
1952
-
1953
- // ─── Deployment Validation ──────────────────────────────────────────────────
1954
-
1955
- /**
1956
- * Detect which PAN runtimes are installed in cwd.
1957
- * @param {string} cwd
1958
- * @returns {Array<{runtime: string, configDir: string}>}
1959
- */
1960
- function detectInstalledRuntimes(cwd) {
1961
- const RUNTIME_DIRS = [
1962
- { runtime: 'claude', configDir: '.claude' },
1963
- { runtime: 'opencode', configDir: '.opencode' },
1964
- { runtime: 'gemini', configDir: '.gemini' },
1965
- { runtime: 'codex', configDir: '.codex' },
1966
- { runtime: 'copilot', configDir: '.github' },
1967
- ];
1968
- const found = [];
1969
- for (const rt of RUNTIME_DIRS) {
1970
- const manifestPath = path.join(cwd, rt.configDir, 'pan-file-manifest.json');
1971
- try {
1972
- fs.accessSync(manifestPath);
1973
- found.push(rt);
1974
- } catch (_) { /* not installed */ }
1975
- }
1976
- return found;
1977
- }
1978
-
1979
- /**
1980
- * Validate a single PAN runtime installation.
1981
- * Checks: manifest files exist, hashes match, settings integrity.
1982
- * @param {string} cwd
1983
- * @param {string} configDir - e.g. '.claude'
1984
- * @param {string} runtime - e.g. 'claude'
1985
- * @returns {{ status: string, version: string, total_files: number, missing: string[], modified: string[], orphaned: string[], settings_ok: boolean, settings_issues: string[] }}
1986
- */
1987
- function validateRuntimeInstall(cwd, configDir, runtime) {
1988
- const crypto = require('crypto');
1989
- const baseDir = path.join(cwd, configDir);
1990
- const manifestPath = path.join(baseDir, 'pan-file-manifest.json');
1991
-
1992
- let manifest;
1993
- try {
1994
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1995
- } catch (e) {
1996
- return { status: 'broken', version: null, error: `Cannot read manifest: ${e.message}`, total_files: 0, missing: [], modified: [], orphaned: [], settings_ok: false, settings_issues: ['manifest unreadable'] };
1997
- }
1998
-
1999
- const missing = [];
2000
- const modified = [];
2001
- const files = manifest.files || {};
2002
- const totalFiles = Object.keys(files).length;
2003
-
2004
- for (const [relPath, expectedHash] of Object.entries(files)) {
2005
- const absPath = path.join(baseDir, relPath);
2006
- try {
2007
- const content = fs.readFileSync(absPath);
2008
- const actualHash = crypto.createHash('sha256').update(content).digest('hex');
2009
- if (actualHash !== expectedHash) {
2010
- modified.push(relPath);
2011
- }
2012
- } catch (_) {
2013
- missing.push(relPath);
2014
- }
2015
- }
2016
-
2017
- // Check settings integrity (hook paths resolve to real files)
2018
- const settingsIssues = [];
2019
- const settingsFile = runtime === 'copilot' ? 'config.json' : 'settings.json';
2020
- const settingsPath = path.join(baseDir, settingsFile);
2021
- let settingsOk = true;
2022
- try {
2023
- const settingsContent = fs.readFileSync(settingsPath, 'utf8');
2024
- const settings = JSON.parse(settingsContent);
2025
- // Check hook paths in settings
2026
- // Collect all hook command strings from settings
2027
- const hookCommands = [];
2028
- const hooks = settings.hooks;
2029
- if (hooks && typeof hooks === 'object') {
2030
- for (const hookArr of Object.values(hooks)) {
2031
- if (!Array.isArray(hookArr)) continue;
2032
- for (const hook of hookArr) {
2033
- if (hook.command) hookCommands.push(hook.command);
2034
- }
2035
- }
2036
- }
2037
- // Copilot/Gemini statusLine
2038
- if (settings.statusLine && settings.statusLine.command) {
2039
- hookCommands.push(settings.statusLine.command);
2040
- }
2041
- // Claude statusline
2042
- if (settings.statusline && settings.statusline.command) {
2043
- hookCommands.push(settings.statusline.command);
2044
- }
2045
- for (const cmd of hookCommands) {
2046
- const parts = cmd.split(/\s+/);
2047
- const hookFile = parts.find(p => p.endsWith('.js'));
2048
- if (hookFile) {
2049
- // Hook paths are relative to cwd, not to config dir
2050
- const resolvedPath = path.isAbsolute(hookFile) ? hookFile : path.join(cwd, hookFile);
2051
- try { fs.accessSync(resolvedPath); } catch (_) {
2052
- settingsIssues.push(`Hook path not found: ${hookFile}`);
2053
- settingsOk = false;
2054
- }
2055
- }
2056
- }
2057
- } catch (_) {
2058
- // No settings file is OK for some runtimes (codex, opencode)
2059
- if (runtime !== 'codex' && runtime !== 'opencode') {
2060
- settingsIssues.push(`${settingsFile} missing or unreadable`);
2061
- settingsOk = false;
2062
- }
2063
- }
2064
-
2065
- const status = missing.length > 0 ? 'broken' : modified.length > 0 ? 'modified' : 'clean';
2066
-
2067
- return {
2068
- status,
2069
- version: manifest.version || null,
2070
- total_files: totalFiles,
2071
- missing,
2072
- modified,
2073
- orphaned: [],
2074
- settings_ok: settingsOk,
2075
- settings_issues: settingsIssues,
2076
- };
2077
- }
2078
-
2079
- /**
2080
- * CLI command: validate deployment
2081
- * Validates PAN installations in the current directory.
2082
- * @param {string} cwd
2083
- * @param {boolean} raw
2084
- */
2085
- function cmdValidateDeployment(cwd, raw) {
2086
- const runtimes = detectInstalledRuntimes(cwd);
2087
- if (runtimes.length === 0) {
2088
- output({ error: 'No PAN installations found in this directory' }, raw);
2089
- return;
2090
- }
2091
-
2092
- const results = {};
2093
- let overallStatus = 'clean';
2094
-
2095
- for (const { runtime, configDir } of runtimes) {
2096
- const result = validateRuntimeInstall(cwd, configDir, runtime);
2097
- results[runtime] = result;
2098
- if (result.status === 'broken') overallStatus = 'broken';
2099
- else if (result.status === 'modified' && overallStatus !== 'broken') overallStatus = 'modified';
2100
- }
2101
-
2102
- const summary = {
2103
- status: overallStatus,
2104
- runtimes_found: runtimes.length,
2105
- runtimes: results,
2106
- };
2107
-
2108
- const rawLines = [`Deployment status: ${overallStatus} (${runtimes.length} runtimes)`];
2109
- for (const [rt, r] of Object.entries(results)) {
2110
- rawLines.push(` ${rt}: ${r.status} (${r.total_files} files, ${r.missing.length} missing, ${r.modified.length} modified)`);
2111
- }
2112
-
2113
- output(summary, raw, rawLines.join('\n'));
2114
- }
1326
+ // ─── Retrospective analysis — extracted to verify-retro.cjs (re-exported below)
1327
+ // ─── Deployment validation extracted to verify-deploy.cjs (re-exported below)
2115
1328
 
2116
1329
  module.exports = {
2117
1330
  cmdVerifySummary,