pan-wizard 3.32.0 → 3.34.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 (209) hide show
  1. package/README.md +23 -20
  2. package/agents/pan-conductor.md +18 -17
  3. package/agents/pan-counterfactual.md +2 -2
  4. package/agents/pan-debugger.md +27 -8
  5. package/agents/pan-design-checker.md +1 -1
  6. package/agents/pan-designer.md +2 -2
  7. package/agents/pan-distiller.md +3 -3
  8. package/agents/pan-document_code.md +16 -16
  9. package/agents/pan-executor.md +15 -11
  10. package/agents/pan-experiment-runner.md +15 -17
  11. package/agents/pan-hardener.md +15 -15
  12. package/agents/pan-integration-checker.md +2 -2
  13. package/agents/pan-knowledge.md +2 -2
  14. package/agents/pan-meta-reviewer.md +5 -5
  15. package/agents/pan-optimizer.md +40 -44
  16. package/agents/pan-phase-researcher.md +10 -10
  17. package/agents/pan-plan-checker.md +36 -30
  18. package/agents/pan-planner.md +77 -53
  19. package/agents/pan-previewer.md +1 -1
  20. package/agents/pan-project-researcher.md +21 -78
  21. package/agents/pan-release.md +1 -1
  22. package/agents/pan-research-synthesizer.md +2 -2
  23. package/agents/pan-reviewer.md +24 -2
  24. package/agents/pan-roadmapper.md +20 -62
  25. package/agents/pan-verifier.md +53 -30
  26. package/bin/install-lib.cjs +228 -38
  27. package/bin/install.js +86 -15
  28. package/commands/pan/army.md +16 -12
  29. package/commands/pan/assumptions.md +2 -2
  30. package/commands/pan/audit-deployment.md +11 -12
  31. package/commands/pan/cost.md +8 -4
  32. package/commands/pan/debug.md +10 -7
  33. package/commands/pan/design-phase.md +3 -3
  34. package/commands/pan/discuss-phase.md +2 -0
  35. package/commands/pan/exec-phase.md +21 -37
  36. package/commands/pan/experiment.md +11 -10
  37. package/commands/pan/focus-auto.md +33 -27
  38. package/commands/pan/focus-design.md +9 -6
  39. package/commands/pan/focus-doc-audit.md +4 -3
  40. package/commands/pan/focus-drift-walking.md +4 -3
  41. package/commands/pan/focus-exec.md +7 -7
  42. package/commands/pan/focus-plan.md +12 -11
  43. package/commands/pan/focus-scan.md +7 -10
  44. package/commands/pan/focus-sync.md +3 -2
  45. package/commands/pan/git.md +8 -8
  46. package/commands/pan/health.md +2 -2
  47. package/commands/pan/hud.md +3 -2
  48. package/commands/pan/hygiene.md +14 -7
  49. package/commands/pan/knowledge.md +9 -9
  50. package/commands/pan/learn.md +10 -7
  51. package/commands/pan/links.md +4 -1
  52. package/commands/pan/map-codebase.md +5 -5
  53. package/commands/pan/mcp-bridge.md +18 -18
  54. package/commands/pan/milestone-done.md +7 -5
  55. package/commands/pan/milestone-new.md +1 -1
  56. package/commands/pan/new-project.md +10 -10
  57. package/commands/pan/optimize.md +16 -14
  58. package/commands/pan/patches.md +2 -6
  59. package/commands/pan/pause.md +1 -1
  60. package/commands/pan/phase-tests.md +4 -3
  61. package/commands/pan/plan-phase.md +9 -19
  62. package/commands/pan/preview.md +1 -1
  63. package/commands/pan/profile.md +4 -4
  64. package/commands/pan/quick.md +1 -1
  65. package/commands/pan/remove-phase.md +1 -1
  66. package/commands/pan/report.md +2 -2
  67. package/commands/pan/research-phase.md +14 -15
  68. package/commands/pan/resume.md +9 -11
  69. package/commands/pan/retro.md +5 -3
  70. package/commands/pan/review-deep.md +10 -10
  71. package/commands/pan/settings.md +1 -1
  72. package/commands/pan/todo-add.md +2 -2
  73. package/commands/pan/todo-check.md +2 -2
  74. package/commands/pan/verify-phase.md +0 -11
  75. package/commands/pan/what-if.md +2 -2
  76. package/hooks/dist/pan-context-monitor.js +343 -44
  77. package/hooks/dist/pan-cost-logger.js +30 -0
  78. package/hooks/dist/pan-state-reinject.js +140 -15
  79. package/hooks/dist/pan-statusline.js +68 -22
  80. package/hooks/dist/pan-stop-guard.js +7 -2
  81. package/package.json +1 -1
  82. package/pan-wizard-core/bin/lib/agents-md.cjs +48 -7
  83. package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
  84. package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
  85. package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
  86. package/pan-wizard-core/bin/lib/commands.cjs +126 -5
  87. package/pan-wizard-core/bin/lib/config.cjs +112 -1
  88. package/pan-wizard-core/bin/lib/constants.cjs +29 -15
  89. package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
  90. package/pan-wizard-core/bin/lib/core.cjs +77 -23
  91. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
  92. package/pan-wizard-core/bin/lib/cost.cjs +149 -10
  93. package/pan-wizard-core/bin/lib/distill.cjs +6 -3
  94. package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
  95. package/pan-wizard-core/bin/lib/findings.cjs +15 -4
  96. package/pan-wizard-core/bin/lib/focus.cjs +13 -4
  97. package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
  98. package/pan-wizard-core/bin/lib/hygiene.cjs +178 -17
  99. package/pan-wizard-core/bin/lib/init.cjs +12 -5
  100. package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
  101. package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
  102. package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
  103. package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
  104. package/pan-wizard-core/bin/lib/memory.cjs +508 -54
  105. package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
  106. package/pan-wizard-core/bin/lib/optimize.cjs +84 -16
  107. package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
  108. package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
  109. package/pan-wizard-core/bin/lib/phase.cjs +67 -15
  110. package/pan-wizard-core/bin/lib/preview.cjs +3 -1
  111. package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
  112. package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
  113. package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
  114. package/pan-wizard-core/bin/lib/runner.cjs +2 -1
  115. package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
  116. package/pan-wizard-core/bin/lib/state.cjs +47 -31
  117. package/pan-wizard-core/bin/lib/template.cjs +11 -4
  118. package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
  119. package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
  120. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
  121. package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
  122. package/pan-wizard-core/bin/lib/verify-scope.cjs +74 -4
  123. package/pan-wizard-core/bin/lib/verify.cjs +117 -35
  124. package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
  125. package/pan-wizard-core/bin/pan-tools.cjs +111 -17
  126. package/pan-wizard-core/learnings/index.json +5 -5
  127. package/pan-wizard-core/mcp/server.cjs +18 -9
  128. package/pan-wizard-core/opencode/pan-wizard.js +111 -0
  129. package/pan-wizard-core/references/checkpoints.md +7 -5
  130. package/pan-wizard-core/references/continuation-format.md +3 -3
  131. package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
  132. package/pan-wizard-core/references/design-methodology.md +1 -1
  133. package/pan-wizard-core/references/git-integration.md +4 -6
  134. package/pan-wizard-core/references/git-planning-commit.md +2 -2
  135. package/pan-wizard-core/references/guardrails.md +2 -2
  136. package/pan-wizard-core/references/handoff-decisions.md +3 -3
  137. package/pan-wizard-core/references/model-profile-resolution.md +5 -5
  138. package/pan-wizard-core/references/model-profiles.md +14 -9
  139. package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
  140. package/pan-wizard-core/references/planning-config.md +4 -4
  141. package/pan-wizard-core/references/tdd.md +10 -1
  142. package/pan-wizard-core/references/verdict-contract.md +2 -1
  143. package/pan-wizard-core/references/verification-patterns.md +5 -5
  144. package/pan-wizard-core/templates/codebase/architecture.md +2 -2
  145. package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
  146. package/pan-wizard-core/templates/codebase/concerns.md +3 -3
  147. package/pan-wizard-core/templates/codebase/integrations.md +6 -6
  148. package/pan-wizard-core/templates/codebase/relationships.md +2 -2
  149. package/pan-wizard-core/templates/codebase/stack.md +2 -2
  150. package/pan-wizard-core/templates/codebase/structure.md +4 -4
  151. package/pan-wizard-core/templates/codebase/testing.md +4 -4
  152. package/pan-wizard-core/templates/continue-here.md +1 -1
  153. package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
  154. package/pan-wizard-core/templates/debug.md +15 -1
  155. package/pan-wizard-core/templates/discovery.md +1 -1
  156. package/pan-wizard-core/templates/milestone-archive.md +6 -6
  157. package/pan-wizard-core/templates/milestone.md +1 -1
  158. package/pan-wizard-core/templates/phase-prompt.md +31 -18
  159. package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
  160. package/pan-wizard-core/templates/research-project/architecture.md +3 -3
  161. package/pan-wizard-core/templates/research-project/features.md +1 -1
  162. package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
  163. package/pan-wizard-core/templates/research-project/stack.md +3 -3
  164. package/pan-wizard-core/templates/research.md +5 -5
  165. package/pan-wizard-core/templates/retrospective.md +1 -1
  166. package/pan-wizard-core/templates/roadmap.md +15 -15
  167. package/pan-wizard-core/templates/summary-complex.md +1 -0
  168. package/pan-wizard-core/templates/summary-minimal.md +1 -0
  169. package/pan-wizard-core/templates/summary-standard.md +2 -1
  170. package/pan-wizard-core/templates/summary.md +18 -2
  171. package/pan-wizard-core/templates/uat.md +3 -3
  172. package/pan-wizard-core/templates/user-setup.md +8 -8
  173. package/pan-wizard-core/templates/verification-report.md +54 -4
  174. package/pan-wizard-core/workflows/add-phase.md +1 -1
  175. package/pan-wizard-core/workflows/assumptions.md +1 -1
  176. package/pan-wizard-core/workflows/discuss-phase.md +9 -8
  177. package/pan-wizard-core/workflows/exec-phase.md +34 -60
  178. package/pan-wizard-core/workflows/execute-plan.md +8 -7
  179. package/pan-wizard-core/workflows/health.md +13 -11
  180. package/pan-wizard-core/workflows/help.md +8 -9
  181. package/pan-wizard-core/workflows/learn.md +13 -11
  182. package/pan-wizard-core/workflows/map-codebase.md +10 -10
  183. package/pan-wizard-core/workflows/milestone-audit.md +10 -10
  184. package/pan-wizard-core/workflows/milestone-gaps.md +17 -24
  185. package/pan-wizard-core/workflows/milestone-new.md +7 -6
  186. package/pan-wizard-core/workflows/new-project.md +10 -10
  187. package/pan-wizard-core/workflows/optimize.md +11 -9
  188. package/pan-wizard-core/workflows/pause.md +6 -1
  189. package/pan-wizard-core/workflows/phase-tests.md +4 -4
  190. package/pan-wizard-core/workflows/plan-phase.md +31 -34
  191. package/pan-wizard-core/workflows/profile.md +2 -2
  192. package/pan-wizard-core/workflows/progress.md +5 -7
  193. package/pan-wizard-core/workflows/quick.md +10 -8
  194. package/pan-wizard-core/workflows/remove-phase.md +1 -1
  195. package/pan-wizard-core/workflows/research-phase.md +9 -5
  196. package/pan-wizard-core/workflows/resume-project.md +4 -6
  197. package/pan-wizard-core/workflows/retro.md +3 -1
  198. package/pan-wizard-core/workflows/settings.md +16 -24
  199. package/pan-wizard-core/workflows/todo-check.md +1 -1
  200. package/pan-wizard-core/workflows/transition.md +12 -12
  201. package/pan-wizard-core/workflows/update.md +25 -17
  202. package/pan-wizard-core/workflows/verify-phase.md +24 -17
  203. package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
  204. package/pan-zcode/README.md +7 -7
  205. package/pan-zcode/bin/install-zcode.js +1 -1
  206. package/scripts/build-agent-plugin.js +19 -5
  207. package/scripts/build-plugin.js +3 -1
  208. package/scripts/generate-skills-docs.py +13 -4
  209. package/scripts/test-surface.cjs +4 -0
@@ -3,13 +3,15 @@
3
3
  */
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveModelDetailed, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix } = require('./core.cjs');
6
+ const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveModelDetailed, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix, toLf } = require('./core.cjs');
7
7
  const { extractFrontmatter } = require('./frontmatter.cjs');
8
8
  const { PHASES_DIR, MILESTONES_DIR, QUICK_DIR, STATE_FILE, ROADMAP_FILE, PROJECT_FILE, PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, CONTEXT_SUFFIX, UAT_SUFFIX, VERIFICATION_SUFFIX, isPlanFile, isSummaryFile, ARCHIVE_DIR_RE, PHASE_DIR_RE, CONTEXT_WINDOW, WARNING_THRESHOLD, CRITICAL_THRESHOLD, VALID_COMMIT_TYPES, DEFAULT_SENSITIVE_PATTERNS, STATE_CONTRACT } = require('./constants.cjs');
9
9
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, planningRel } = require('./utils.cjs');
10
10
  const { estimateTokens } = require('./context-budget.cjs');
11
11
  const { collectPhaseSummaries, readErrorPatterns, appendErrorPattern, appendSessionSummary, parseLearnings, formatLearningEntry, cmdLearningsExtract, cmdLearningsList, cmdLearningsPrune } = require('./commands-learnings.cjs');
12
12
  const { planningRootRel } = require('./planning-root.cjs');
13
+ const { verificationFreshness } = require('./verify-scope.cjs');
14
+ const { isVerificationFile } = require('./constants.cjs');
13
15
 
14
16
  /**
15
17
  * Generate a URL-safe slug from text by lowercasing and replacing non-alphanumeric chars.
@@ -151,9 +153,12 @@ function buildDigest(summaries, techStack) {
151
153
  frontmatter.provides.forEach(item => phases[phaseNum].provides.add(item));
152
154
  }
153
155
 
154
- // Merge affects from nested dependency-graph
156
+ // Merge affects from either nested dependency-graph or the flat `affects:` list the
157
+ // summary templates write (only the nested form was read, so `affects` stayed empty)
155
158
  if (frontmatter['dependency-graph'] && Array.isArray(frontmatter['dependency-graph'].affects)) {
156
159
  frontmatter['dependency-graph'].affects.forEach(item => phases[phaseNum].affects.add(item));
160
+ } else if (Array.isArray(frontmatter.affects)) {
161
+ frontmatter.affects.forEach(item => phases[phaseNum].affects.add(item));
157
162
  }
158
163
 
159
164
  // Merge established patterns
@@ -313,7 +318,9 @@ function runCommitSafetyChecks(cwd, config, force) {
313
318
  if (statusResult.exitCode === 0 && statusResult.stdout) {
314
319
  for (const line of statusResult.stdout.split('\n').filter(Boolean)) {
315
320
  if (line.startsWith(' D') || line.startsWith('D ') || line.startsWith('D')) {
316
- const fileName = line.slice(3).trim();
321
+ // execGit trims stdout, so a first line of ` D gone.txt` arrives as `D gone.txt`:
322
+ // strip the status code by pattern, not by a fixed three-character slice
323
+ const fileName = line.replace(/^\s*[A-Z?!]{1,2}\s+/, '').trim();
317
324
  if (fileName) safetyChecks.deleted_files.push(fileName);
318
325
  }
319
326
  }
@@ -331,7 +338,13 @@ function runCommitSafetyChecks(cwd, config, force) {
331
338
  if (patterns.length > 0) {
332
339
  const stagedFiles = stagedResult.stdout.split('\n').filter(Boolean);
333
340
  const regexes = patterns.map(p => { try { return new RegExp(p, 'i'); } catch { return null; } }).filter(Boolean);
341
+ // PAN's own cost ledger matches the bare `token` pattern, which blocked every
342
+ // commit that staged .planning/ once the cost logger had written to it. It holds
343
+ // token counts, never a credential. The hook always writes the root tree's copy.
344
+ const { METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
345
+ const ownLedger = new Set([planningRel(METRICS_DIR, TOKENS_FILE), `.planning/${METRICS_DIR}/${TOKENS_FILE}`]);
334
346
  for (const f of stagedFiles) {
347
+ if (ownLedger.has(f)) continue;
335
348
  if (regexes.some(re => re.test(f))) safetyChecks.sensitive_files_blocked.push(f);
336
349
  }
337
350
  }
@@ -461,6 +474,22 @@ function cmdCommit(cwd, message, files, raw, amend, opts) {
461
474
  output({ committed: true, hash, reason: 'committed', type: commitType || null, safety_checks: safety.safetyChecks }, raw, hash || 'committed');
462
475
  }
463
476
 
477
+ /**
478
+ * A summary's one-liner: the `one-liner` frontmatter key, else the bold line under the
479
+ * `# Phase … Summary` title, which is where the summary templates put it (none writes
480
+ * the key, so reading only the key gave `one_liner: null` for every summary PAN writes).
481
+ * Read on LF: the body match failed on a CRLF summary.
482
+ * @param {string} content - Whole summary.md
483
+ * @returns {string|null}
484
+ */
485
+ function summaryOneLiner(content) {
486
+ const text = toLf(String(content || ''));
487
+ const fm = extractFrontmatter(text);
488
+ if (fm['one-liner']) return fm['one-liner'];
489
+ const m = text.replace(/^?---\n[\s\S]*?\n---\n/, '').match(/^#\s[^\n]*\n+\*\*([^\n*][^\n]*?)\*\*\s*$/m);
490
+ return m ? m[1].trim() : null;
491
+ }
492
+
464
493
  /**
465
494
  * Extract structured data from a summary.md frontmatter with optional field filtering.
466
495
  * @param {string} cwd - Working directory path
@@ -500,10 +529,11 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
500
529
  });
501
530
  };
502
531
 
532
+
503
533
  // Build full result
504
534
  const fullResult = {
505
535
  path: summaryPath,
506
- one_liner: frontmatter['one-liner'] || null,
536
+ one_liner: summaryOneLiner(content),
507
537
  key_files: frontmatter['key-files'] || [],
508
538
  tech_added: (frontmatter['tech-stack'] && frontmatter['tech-stack'].added) || [],
509
539
  patterns: frontmatter['patterns-established'] || [],
@@ -631,6 +661,7 @@ function cmdProgressRender(cwd, format, raw) {
631
661
  const milestone = getMilestoneInfo(cwd);
632
662
 
633
663
  const phases = [];
664
+ const verifiedPhases = []; // indexes into phases[] of phases with a verification file
634
665
  let totalPlans = 0;
635
666
  let totalSummaries = 0;
636
667
 
@@ -661,12 +692,25 @@ function cmdProgressRender(cwd, format, raw) {
661
692
  else if (summaryCount > 0) status = 'In Progress';
662
693
  else status = 'Planned';
663
694
 
695
+ if (phaseFiles.some(isVerificationFile)) verifiedPhases.push(phases.length);
664
696
  phases.push({ number: phaseNum, name: phaseName, plans: planCount, summaries: summaryCount, status });
665
697
  }
666
698
  } catch {
667
699
  // Phases directory does not exist or is unreadable; return empty progress
668
700
  }
669
701
 
702
+ // A verified phase whose covered files changed since the verified commit is
703
+ // stale (market-ideas M27). Not for the bar or the health score: the statusline
704
+ // calls the bar, which never shows it, and each check costs a git call.
705
+ if (format !== 'bar' && format !== 'health') {
706
+ for (const i of verifiedPhases) {
707
+ const f = verificationFreshness(cwd, phases[i].number);
708
+ if (f.error) continue;
709
+ phases[i].verification = f.state;
710
+ if (f.state === 'stale') phases[i].changed_since_verification = f.changed_since;
711
+ }
712
+ }
713
+
670
714
  const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
671
715
 
672
716
  if (format === 'health') {
@@ -682,7 +726,7 @@ function cmdProgressRender(cwd, format, raw) {
682
726
  rendered += '| Phase | Name | Plans | Status |\n';
683
727
  rendered += '|-------|------|-------|--------|\n';
684
728
  for (const phase of phases) {
685
- rendered += `| ${phase.number} | ${phase.name} | ${phase.summaries}/${phase.plans} | ${phase.status} |\n`;
729
+ rendered += `| ${phase.number} | ${phase.name} | ${phase.summaries}/${phase.plans} | ${phase.status}${phase.verification === 'stale' ? ' (verification stale)' : ''} |\n`;
686
730
  }
687
731
  output({ rendered }, raw, rendered);
688
732
  } else if (format === 'bar') {
@@ -1027,9 +1071,85 @@ function shouldSkipTests(files) {
1027
1071
  return files.every(f => /\.md$/i.test(f));
1028
1072
  }
1029
1073
 
1074
+ // ─── version (market-ideas M29) ──────────────────────────────────────────────
1075
+ // The version of the PAN core that is running, where it lives, and, with --check,
1076
+ // the update state the SessionStart update check last recorded. Reads only local
1077
+ // files: the check itself (`npm view`, up to 10 s) stays in the hook's background
1078
+ // child, so `version --check` never waits on the network and never throws offline.
1079
+
1080
+ // Where each install directory's update-check hook writes its cache: the hook's
1081
+ // `join(homeDir, '.claude', 'cache')` is rewritten to the runtime's GLOBAL config dir
1082
+ // (getConfigDirFromHome(runtime, true) in the installer).
1083
+ const UPDATE_CACHE_HOME_DIR = {
1084
+ '.claude': ['.claude'], '.codex': ['.codex'], '.gemini': ['.gemini'],
1085
+ '.opencode': ['.config', 'opencode'], opencode: ['.config', 'opencode'],
1086
+ '.github': ['.copilot'], '.copilot': ['.copilot'],
1087
+ };
1088
+
1089
+ /** The running core's version, its directory, and the install directory holding it. */
1090
+ function panVersionInfo(coreDir = path.resolve(__dirname, '..', '..')) {
1091
+ let version = null;
1092
+ // VERSION is written by the installer; a source checkout has the package.json instead.
1093
+ try { version = fs.readFileSync(path.join(coreDir, 'VERSION'), 'utf8').trim() || null; } catch { /* not an install */ }
1094
+ if (!version) {
1095
+ try { version = JSON.parse(fs.readFileSync(path.join(coreDir, '..', 'package.json'), 'utf8')).version || null; } catch { /* neither */ }
1096
+ }
1097
+ return { version, core: toPosix(coreDir), install_dir: path.basename(path.dirname(coreDir)) };
1098
+ }
1099
+
1100
+ /** The update-check cache for this install's runtime, else the newest one any runtime wrote. */
1101
+ function readUpdateCache(homeDir, installDir) {
1102
+ const read = (segments) => {
1103
+ const file = path.join(homeDir, ...segments, 'cache', 'pan-update-check.json');
1104
+ try { return { file: toPosix(file), data: JSON.parse(fs.readFileSync(file, 'utf8')) }; } catch { return null; }
1105
+ };
1106
+ const own = UPDATE_CACHE_HOME_DIR[installDir] ? read(UPDATE_CACHE_HOME_DIR[installDir]) : null;
1107
+ if (own) return own;
1108
+ const seen = new Set();
1109
+ const all = [];
1110
+ for (const segs of Object.values(UPDATE_CACHE_HOME_DIR)) {
1111
+ const key = segs.join('/');
1112
+ if (seen.has(key)) continue;
1113
+ seen.add(key);
1114
+ const c = read(segs);
1115
+ if (c) all.push(c);
1116
+ }
1117
+ all.sort((a, b) => (Number(b.data.checked) || 0) - (Number(a.data.checked) || 0));
1118
+ return all[0] || null;
1119
+ }
1120
+
1121
+ /**
1122
+ * The update state from a cache record: `unchecked` (no check has run), `offline`
1123
+ * (the last check could not reach npm), `available`, `current`, or `ahead` (this
1124
+ * core is newer than npm's latest, e.g. a prerelease or a source checkout).
1125
+ */
1126
+ function updateState(version, cache) {
1127
+ if (!cache) return { state: 'unchecked', latest: null, checked_at: null, cache: null };
1128
+ const checkedAt = Number(cache.data.checked) ? new Date(Number(cache.data.checked) * 1000).toISOString() : null;
1129
+ const latest = cache.data.latest && cache.data.latest !== 'unknown' ? String(cache.data.latest) : null;
1130
+ if (!latest) return { state: 'offline', latest: null, checked_at: checkedAt, cache: cache.file };
1131
+ const { compareVersions } = require('./hygiene.cjs');
1132
+ const cmp = version ? compareVersions(version, latest) : -1;
1133
+ return { state: cmp < 0 ? 'available' : cmp > 0 ? 'ahead' : 'current', latest, checked_at: checkedAt, cache: cache.file };
1134
+ }
1135
+
1136
+ function cmdVersion(_cwd, opts, raw) {
1137
+ const info = panVersionInfo();
1138
+ const result = { version: info.version, core: info.core, install_dir: info.install_dir, node: process.version };
1139
+ if (opts.check) result.update = updateState(info.version, readUpdateCache(require('os').homedir(), info.install_dir));
1140
+ const text = opts.check
1141
+ ? `${info.version || 'unknown'} (${result.update.state}${result.update.latest ? `, npm latest ${result.update.latest}` : ''})`
1142
+ : String(info.version || 'unknown');
1143
+ output(result, raw, text);
1144
+ }
1145
+
1030
1146
  // ---- Error patterns, session history, learnings — extracted to commands-learnings.cjs (re-exported below)
1031
1147
 
1032
1148
  module.exports = {
1149
+ cmdVersion,
1150
+ panVersionInfo,
1151
+ readUpdateCache,
1152
+ updateState,
1033
1153
  cmdGenerateSlug,
1034
1154
  cmdCurrentTimestamp,
1035
1155
  cmdListTodos,
@@ -1039,6 +1159,7 @@ module.exports = {
1039
1159
  cmdEstimateCost,
1040
1160
  cmdCommit,
1041
1161
  cmdSummaryExtract,
1162
+ summaryOneLiner,
1042
1163
  cmdWebsearch,
1043
1164
  cmdProgressRender,
1044
1165
  cmdTodoComplete,
@@ -103,6 +103,102 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
103
103
  };
104
104
  }
105
105
 
106
+ // What PAN reads beyond the keys buildConfigDefaults writes (market-ideas M26). The
107
+ // rest of the known set is derived from the defaults, so a new default is known the
108
+ // day it is added. Each entry has a reader: loadConfig's flat aliases and alias
109
+ // sections (`planning.*`, `git.*`), the stop guard (`mode`, `workflow.stop_guard`),
110
+ // the roadmapper and new-project (`depth`), memory-optimize (`memory`), and the
111
+ // workflows that config-get `workflow.auto_advance`. tests/config-keys in
112
+ // tests/config.test.cjs pins this list against the keys shipped code and prose use.
113
+ const CONFIG_KEYS_BEYOND_DEFAULTS = Object.freeze({
114
+ top: Object.freeze(['research', 'plan_checker', 'verifier', 'nyquist_validation', 'mode', 'depth', 'planning', 'git',
115
+ 'focus', 'model_overrides', 'effort_overrides', 'cost', 'cache', 'build', 'verification', 'concurrency', 'memory']),
116
+ sections: Object.freeze({
117
+ workflow: Object.freeze(['auto_advance', 'stop_guard']),
118
+ planning: Object.freeze(['commit_docs', 'search_gitignored']),
119
+ git: Object.freeze(['branching_strategy', 'phase_branch_template', 'milestone_branch_template']),
120
+ }),
121
+ });
122
+ // Sections whose keys are the user's own (model ids, agent names, file lists,
123
+ // commands), so nothing below them is checked.
124
+ const OPEN_CONFIG_SECTIONS = Object.freeze(['focus', 'model_overrides', 'effort_overrides', 'cost', 'cache', 'build', 'verification', 'concurrency', 'memory']);
125
+
126
+ /** The known top-level keys, and the known keys one level inside each closed section. */
127
+ function knownConfigKeys() {
128
+ const defaults = buildConfigDefaults(false, {});
129
+ const top = new Set([...Object.keys(defaults), ...CONFIG_KEYS_BEYOND_DEFAULTS.top]);
130
+ const sections = new Map();
131
+ for (const [k, v] of Object.entries(defaults)) {
132
+ if (v && typeof v === 'object' && !Array.isArray(v)) sections.set(k, new Set(Object.keys(v)));
133
+ }
134
+ for (const [k, list] of Object.entries(CONFIG_KEYS_BEYOND_DEFAULTS.sections)) {
135
+ sections.set(k, new Set([...(sections.get(k) || []), ...list]));
136
+ }
137
+ for (const k of OPEN_CONFIG_SECTIONS) sections.delete(k);
138
+ return { top, sections };
139
+ }
140
+
141
+ /**
142
+ * Whether PAN reads a dot-notation key path: the top-level key is known, and inside a
143
+ * closed section so is the second segment. Deeper segments are not checked.
144
+ * @param {string} keyPath
145
+ * @returns {boolean}
146
+ */
147
+ function isKnownConfigPath(keyPath) {
148
+ const [head, sub] = String(keyPath).split('.');
149
+ const { top, sections } = knownConfigKeys();
150
+ if (!top.has(head)) return false;
151
+ return sub === undefined || !sections.has(head) || sections.get(head).has(sub);
152
+ }
153
+
154
+ /**
155
+ * The key paths in a parsed config.json that PAN does not read (market-ideas M26):
156
+ * unknown top-level keys, and unknown keys one level inside a closed section.
157
+ * @param {Object} config
158
+ * @returns {string[]}
159
+ */
160
+ function unknownConfigKeys(config) {
161
+ if (!config || typeof config !== 'object' || Array.isArray(config)) return [];
162
+ const { top, sections } = knownConfigKeys();
163
+ const unknown = [];
164
+ for (const [k, v] of Object.entries(config)) {
165
+ if (!top.has(k)) { unknown.push(k); continue; }
166
+ const known = sections.get(k);
167
+ if (!known || !v || typeof v !== 'object' || Array.isArray(v)) continue;
168
+ for (const sub of Object.keys(v)) if (!known.has(sub)) unknown.push(`${k}.${sub}`);
169
+ }
170
+ return unknown;
171
+ }
172
+
173
+ /** The known key path nearest an unknown one (edit distance 2 or less), or null. */
174
+ function nearestConfigKey(keyPath) {
175
+ const { top, sections } = knownConfigKeys();
176
+ const candidates = [...top];
177
+ for (const [k, subs] of sections) for (const s of subs) candidates.push(`${k}.${s}`);
178
+ const want = String(keyPath).split('.').slice(0, 2).join('.');
179
+ let best = null;
180
+ let bestD = 3;
181
+ for (const c of candidates) {
182
+ const d = editDistance(want, c);
183
+ if (d < bestD) { best = c; bestD = d; }
184
+ }
185
+ return best;
186
+ }
187
+
188
+ function editDistance(a, b) {
189
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
190
+ for (let i = 1; i <= a.length; i++) {
191
+ let prev = row[0];
192
+ row[0] = i;
193
+ for (let j = 1; j <= b.length; j++) {
194
+ const tmp = row[j];
195
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
196
+ prev = tmp;
197
+ }
198
+ }
199
+ return row[b.length];
200
+ }
201
+
106
202
  function cmdConfigEnsureSection(cwd, raw) {
107
203
  const configPath = path.join(planningPath(cwd), CONFIG_FILE);
108
204
 
@@ -216,6 +312,14 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
216
312
  try {
217
313
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
218
314
  const result = { updated: true, key: keyPath, value: parsedValue };
315
+ // A key PAN does not read is still written (a newer PAN, or the user's own
316
+ // note, may want it), but never silently: a typo used to look like a setting
317
+ // that took effect (market-ideas M26).
318
+ if (!isKnownConfigPath(keyPath)) {
319
+ const near = nearestConfigKey(keyPath);
320
+ result.warning = `unknown config key "${keyPath}": PAN does not read it` + (near ? `; did you mean "${near}"?` : '');
321
+ process.stderr.write(`pan-tools: ${result.warning}\n`);
322
+ }
219
323
  output(result, raw, `${keyPath}=${parsedValue}`);
220
324
  } catch (err) {
221
325
  // Config file could not be written — disk full, permissions, etc.
@@ -261,7 +365,8 @@ function cmdConfigGet(cwd, keyPath, raw) {
261
365
  error(`Key not found: ${keyPath}`);
262
366
  }
263
367
 
264
- output(current, raw, String(current));
368
+ // An object or array prints as JSON under --raw: String() gave "[object Object]".
369
+ output(current, raw, current !== null && typeof current === 'object' ? JSON.stringify(current) : String(current));
265
370
  }
266
371
 
267
372
  // ─── Standards commands ─────────────────────────────────────────────────────
@@ -664,6 +769,12 @@ function cmdStandardsTools(cwd, standardId, raw) {
664
769
 
665
770
  module.exports = {
666
771
  buildConfigDefaults,
772
+ CONFIG_KEYS_BEYOND_DEFAULTS,
773
+ OPEN_CONFIG_SECTIONS,
774
+ knownConfigKeys,
775
+ isKnownConfigPath,
776
+ unknownConfigKeys,
777
+ nearestConfigKey,
667
778
  cmdConfigEnsureSection,
668
779
  cmdConfigSet,
669
780
  cmdConfigGet,
@@ -34,6 +34,9 @@ const RESEARCH_SUFFIX = '-research.md';
34
34
  const VERIFICATION_SUFFIX = '-verification.md';
35
35
  const UAT_SUFFIX = '-uat.md';
36
36
  const VALIDATION_SUFFIX = '-validation.md';
37
+ // A phase's roadmap slice (roadmap slice --write), which phase agents and plans read
38
+ // in place of the whole roadmap.md and requirements.md.
39
+ const ROADMAP_SLICE_SUFFIX = '-roadmap-slice.md';
37
40
 
38
41
  // ─── File matching helpers ───────────────────────────────────────────────────
39
42
 
@@ -207,16 +210,18 @@ const STANDARDS_CATALOG = {
207
210
  level: 'foundational',
208
211
  url: 'https://owasp.org/www-project-top-ten/',
209
212
  checklist: [
210
- 'A01: Broken Access Control — verify authorization checks on all endpoints',
211
- 'A02: Cryptographic Failures — verify sensitive data encryption at rest and in transit',
212
- 'A03: Injection — verify input validation and parameterized queries',
213
- 'A04: Insecure Design — verify threat modeling and secure design patterns',
214
- 'A05: Security Misconfiguration — verify default credentials removed, headers set',
215
- 'A06: Vulnerable Components — verify dependency scanning, no known CVEs',
213
+ // The 2025 list (top10.owasp.org): until 2026-10-03 this held the 2021 list
214
+ // under the 2025 name (prompt audit, market-ideas M34). SSRF is now part of A01.
215
+ 'A01: Broken Access Control — verify authorization checks on all endpoints, and allowlists for server-side requests (SSRF)',
216
+ 'A02: Security Misconfiguration — verify default credentials removed, headers set',
217
+ 'A03: Software Supply Chain Failures — verify dependency scanning, no known CVEs, pinned and verified packages and build tooling',
218
+ 'A04: Cryptographic Failures — verify sensitive data encryption at rest and in transit',
219
+ 'A05: Injection — verify input validation and parameterized queries',
220
+ 'A06: Insecure Design — verify threat modeling and secure design patterns',
216
221
  'A07: Authentication Failures — verify MFA support, session management, credential storage',
217
- 'A08: Software and Data Integrity — verify CI/CD pipeline integrity, signed updates',
218
- 'A09: Logging and Monitoring — verify security events logged, alerts configured',
219
- 'A10: SSRF — verify server-side request validation, allowlists',
222
+ 'A08: Software or Data Integrity Failures — verify CI/CD pipeline integrity, signed updates',
223
+ 'A09: Security Logging and Alerting Failures — verify security events logged, alerts configured',
224
+ 'A10: Mishandling of Exceptional Conditions — verify errors fail closed, are handled, and release resources',
220
225
  ],
221
226
  },
222
227
  'owasp-asvs-l1': {
@@ -574,17 +579,23 @@ const TOTAL_BUDGET_MIN = 5;
574
579
  const TOTAL_BUDGET_MAX = 5000;
575
580
 
576
581
  // Memory read/budget (ADR-0036 FW-2/FW-3): distill-and-select on the memory axis.
577
- const MEMORY_SELECT_BUDGET_TOKENS = 2000; // per-agent cap for cue-scoped memory injection
582
+ const MEMORY_SELECT_BUDGET_TOKENS = 2000; // per-agent cap for a cue-scoped `memory select`
578
583
  const MEMORY_RECENCY_FLOOR = 5; // always keep this many newest entries (recall never empty)
579
584
  const MEMORY_SOFT_CAP_MULT = 2; // soft auto-compaction trigger = DEFAULT_MAX_ENTRIES × this
580
585
  const MEMORY_LOAD_WARN_TOKENS = 4000; // memory-budget telemetry: warn threshold (absolute tokens)
581
586
  const MEMORY_LOAD_CRIT_TOKENS = 8000; // memory-budget telemetry: critical threshold (absolute tokens)
582
587
  const MEMORY_LOAD_MAX_FRACTION = 0.15; // memory-budget telemetry: max fraction of median agent input
583
-
584
- // Cached prompt context (ADR-0044). The files in CACHEABLE_CONTEXT_FILES are
585
- // re-read into EVERY agent call, so their combined size is the single largest
586
- // recurring cost in a PAN project — cache reads dominate token traffic by roughly
587
- // two orders of magnitude over generation. These thresholds mirror the
588
+ // Cited, verified, expiring memory (memory optimisation O4; the Copilot Memory
589
+ // pattern). An entry not used for this many days is no longer selected and
590
+ // `memory prune` archives it; `memory select --mark-used` refreshes it.
591
+ const MEMORY_EXPIRE_DAYS = 60;
592
+ const MEMORY_CITED_FILE_MAX_BYTES = 2 * 1024 * 1024; // larger cited files: existence is the evidence
593
+
594
+ // Stable planning files (ADR-0044): what `cache prime`, `context-budget` and
595
+ // hygiene's cache-context check measure. state.md among them is re-read on every
596
+ // agent call, the largest recurring cost in a PAN project; roadmap.md and
597
+ // requirements.md are read whole only by the roadmapper and milestone work, since
598
+ // phase agents read `roadmap slice` (O2). These thresholds mirror the
588
599
  // MEMORY_LOAD_* pattern above: measured, classified, and surfaced.
589
600
  const CACHE_BLOCK_WARN_TOKENS = 15000; // cached context block: warn threshold (absolute tokens)
590
601
  const CACHE_BLOCK_CRIT_TOKENS = 25000; // cached context block: critical threshold
@@ -778,6 +789,7 @@ module.exports = {
778
789
  VERIFICATION_SUFFIX,
779
790
  UAT_SUFFIX,
780
791
  VALIDATION_SUFFIX,
792
+ ROADMAP_SLICE_SUFFIX,
781
793
  // File matchers
782
794
  isPlanFile,
783
795
  isSummaryFile,
@@ -860,6 +872,8 @@ module.exports = {
860
872
  MEMORY_LOAD_WARN_TOKENS,
861
873
  MEMORY_LOAD_CRIT_TOKENS,
862
874
  MEMORY_LOAD_MAX_FRACTION,
875
+ MEMORY_EXPIRE_DAYS,
876
+ MEMORY_CITED_FILE_MAX_BYTES,
863
877
  // Hygiene
864
878
  CACHE_BLOCK_WARN_TOKENS,
865
879
  CACHE_BLOCK_CRIT_TOKENS,
@@ -267,8 +267,8 @@ function cmdContextBudget(cwd, raw) {
267
267
  }
268
268
  }
269
269
 
270
- // E-8: cache metrics — surface how much of the total context would be
271
- // served from prompt cache when Opus 4.7 cache_control is active.
270
+ // E-8: cache metrics — how much of the total context is the stable set that
271
+ // agents re-read, which the host serves from its prompt cache when it can.
272
272
  const { buildCachedContext } = require('./core.cjs');
273
273
  let cache = null;
274
274
  try {
@@ -277,10 +277,10 @@ function cmdContextBudget(cwd, raw) {
277
277
  const eligiblePct = totalTokens > 0
278
278
  ? Math.round((cacheTokens / totalTokens) * 1000) / 10
279
279
  : 0;
280
- // The cached block is re-read into EVERY agent call, so its size is the
281
- // project's largest recurring cost. This used to be measured and reported
282
- // with no threshold attached, which meant a block that had grown to ~28k
283
- // tokens of mostly closed history looked exactly like a healthy one.
280
+ // The stable planning files; state.md among them is re-read on every agent
281
+ // call, the project's largest recurring cost. This used to be measured and
282
+ // reported with no threshold attached, which meant a block that had grown to
283
+ // ~28k tokens of mostly closed history looked exactly like a healthy one.
284
284
  // Classifying it is what turns the measurement into a signal.
285
285
  const { CACHE_BLOCK_WARN_TOKENS, CACHE_BLOCK_CRIT_TOKENS, CACHE_FILE_WARN_TOKENS } = require('./constants.cjs');
286
286
  const largest = cached.blocks
@@ -292,12 +292,15 @@ function cmdContextBudget(cwd, raw) {
292
292
  else if (cacheTokens >= CACHE_BLOCK_CRIT_TOKENS) cacheStatus = 'critical';
293
293
  else if (cacheTokens >= CACHE_BLOCK_WARN_TOKENS) cacheStatus = 'warn';
294
294
 
295
+ // PAN primes no cache (ADR-0023, amended): an absent block means nothing to
296
+ // measure, not that the host stops caching.
295
297
  const advice = cacheStatus === 'absent'
296
- ? 'no cacheable context files — every agent call re-sends its context uncached'
298
+ ? 'none of the stable planning files exist yet, so there is nothing to measure'
297
299
  : cacheStatus === 'ok'
298
300
  ? null
299
- : `cached context is re-read on every agent call; largest file ${largest[0].path} (~${largest[0].tokens} tokens)`
300
- + (largest[0].path.endsWith('state.md') ? ' — run `pan-tools state compact`' : '');
301
+ : `stable planning files past the budget; largest file ${largest[0].path} (~${largest[0].tokens} tokens)`
302
+ + (largest[0].path.endsWith('state.md') ? ' — re-read on every agent call; run `pan-tools state compact`'
303
+ : largest[0].path.endsWith('roadmap.md') ? ' — read whole by the roadmapper and milestone work; run `pan-tools roadmap compact`' : '');
301
304
 
302
305
  // Lifetime signal from the ledger (suspect rows excluded — they carry
303
306
  // poisoned counters, not real writes). Absent ledger → zero rows, no advice.