pan-wizard 3.33.0 → 3.35.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 (214) hide show
  1. package/README.md +29 -26
  2. package/agents/pan-conductor.md +18 -17
  3. package/agents/pan-counterfactual.md +2 -2
  4. package/agents/pan-debugger.md +30 -9
  5. package/agents/pan-design-checker.md +2 -1
  6. package/agents/pan-designer.md +2 -2
  7. package/agents/pan-distiller.md +3 -3
  8. package/agents/pan-document_code.md +14 -14
  9. package/agents/pan-executor.md +30 -9
  10. package/agents/pan-experiment-runner.md +9 -8
  11. package/agents/pan-hardener.md +1 -1
  12. package/agents/pan-integration-checker.md +2 -2
  13. package/agents/pan-knowledge.md +1 -1
  14. package/agents/pan-meta-reviewer.md +4 -4
  15. package/agents/pan-optimizer.md +39 -43
  16. package/agents/pan-phase-researcher.md +10 -10
  17. package/agents/pan-plan-checker.md +37 -34
  18. package/agents/pan-planner.md +57 -44
  19. package/agents/pan-previewer.md +1 -1
  20. package/agents/pan-project-researcher.md +12 -12
  21. package/agents/pan-release.md +1 -1
  22. package/agents/pan-research-synthesizer.md +3 -3
  23. package/agents/pan-reviewer.md +26 -3
  24. package/agents/pan-roadmapper.md +15 -14
  25. package/agents/pan-verifier.md +33 -25
  26. package/bin/install-lib.cjs +176 -38
  27. package/bin/install.js +81 -63
  28. package/commands/pan/army.md +15 -11
  29. package/commands/pan/assumptions.md +2 -2
  30. package/commands/pan/audit-deployment.md +11 -12
  31. package/commands/pan/cost.md +8 -8
  32. package/commands/pan/dashboard.md +0 -3
  33. package/commands/pan/debug.md +10 -7
  34. package/commands/pan/design-phase.md +5 -5
  35. package/commands/pan/discuss-phase.md +2 -0
  36. package/commands/pan/exec-phase.md +21 -37
  37. package/commands/pan/experiment.md +9 -8
  38. package/commands/pan/focus-auto.md +22 -20
  39. package/commands/pan/focus-design.md +7 -6
  40. package/commands/pan/focus-doc-audit.md +4 -3
  41. package/commands/pan/focus-drift-walking.md +4 -3
  42. package/commands/pan/focus-exec.md +6 -6
  43. package/commands/pan/focus-plan.md +12 -11
  44. package/commands/pan/focus-scan.md +5 -3
  45. package/commands/pan/focus-sync.md +3 -2
  46. package/commands/pan/git.md +8 -8
  47. package/commands/pan/health.md +2 -2
  48. package/commands/pan/hud.md +4 -7
  49. package/commands/pan/hygiene.md +14 -7
  50. package/commands/pan/knowledge.md +9 -10
  51. package/commands/pan/learn.md +10 -7
  52. package/commands/pan/links.md +4 -1
  53. package/commands/pan/map-codebase.md +1 -1
  54. package/commands/pan/mcp-bridge.md +19 -21
  55. package/commands/pan/milestone-done.md +6 -3
  56. package/commands/pan/milestone-new.md +1 -1
  57. package/commands/pan/new-project.md +10 -10
  58. package/commands/pan/optimize.md +18 -14
  59. package/commands/pan/patches.md +2 -6
  60. package/commands/pan/pause.md +1 -1
  61. package/commands/pan/phase-tests.md +3 -2
  62. package/commands/pan/plan-phase.md +9 -19
  63. package/commands/pan/preview.md +2 -3
  64. package/commands/pan/profile.md +2 -2
  65. package/commands/pan/quick.md +1 -1
  66. package/commands/pan/remove-phase.md +1 -1
  67. package/commands/pan/report.md +3 -7
  68. package/commands/pan/research-phase.md +16 -15
  69. package/commands/pan/resume.md +9 -11
  70. package/commands/pan/retro.md +5 -3
  71. package/commands/pan/review-deep.md +7 -9
  72. package/commands/pan/settings.md +1 -1
  73. package/commands/pan/verify-phase.md +0 -11
  74. package/commands/pan/what-if.md +3 -4
  75. package/hooks/dist/pan-context-monitor.js +352 -44
  76. package/hooks/dist/pan-cost-logger.js +30 -0
  77. package/hooks/dist/pan-guard.js +244 -0
  78. package/hooks/dist/pan-statusline.js +73 -24
  79. package/hooks/dist/pan-stop-guard.js +7 -2
  80. package/package.json +1 -1
  81. package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
  82. package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
  83. package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
  84. package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
  85. package/pan-wizard-core/bin/lib/commands.cjs +57 -6
  86. package/pan-wizard-core/bin/lib/config.cjs +7 -1
  87. package/pan-wizard-core/bin/lib/constants.cjs +18 -6
  88. package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
  89. package/pan-wizard-core/bin/lib/core.cjs +53 -19
  90. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
  91. package/pan-wizard-core/bin/lib/cost.cjs +142 -13
  92. package/pan-wizard-core/bin/lib/distill.cjs +6 -3
  93. package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
  94. package/pan-wizard-core/bin/lib/findings.cjs +135 -21
  95. package/pan-wizard-core/bin/lib/focus.cjs +13 -4
  96. package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
  97. package/pan-wizard-core/bin/lib/hygiene.cjs +184 -17
  98. package/pan-wizard-core/bin/lib/init.cjs +13 -4
  99. package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
  100. package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
  101. package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
  102. package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
  103. package/pan-wizard-core/bin/lib/memory.cjs +508 -54
  104. package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
  105. package/pan-wizard-core/bin/lib/optimize.cjs +97 -17
  106. package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
  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 +39 -13
  110. package/pan-wizard-core/bin/lib/preview.cjs +34 -31
  111. package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
  112. package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
  113. package/pan-wizard-core/bin/lib/runner.cjs +2 -1
  114. package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
  115. package/pan-wizard-core/bin/lib/state.cjs +43 -28
  116. package/pan-wizard-core/bin/lib/template.cjs +11 -4
  117. package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
  118. package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
  119. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
  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 +11 -7
  123. package/pan-wizard-core/bin/lib/verify.cjs +30 -28
  124. package/pan-wizard-core/bin/lib/whatif.cjs +53 -9
  125. package/pan-wizard-core/bin/pan-tools.cjs +114 -19
  126. package/pan-wizard-core/learnings/index.json +5 -5
  127. package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
  128. package/pan-wizard-core/mcp/native-tools.cjs +3 -3
  129. package/pan-wizard-core/opencode/pan-wizard.js +139 -0
  130. package/pan-wizard-core/references/checkpoints.md +3 -3
  131. package/pan-wizard-core/references/continuation-format.md +3 -3
  132. package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
  133. package/pan-wizard-core/references/design-methodology.md +1 -1
  134. package/pan-wizard-core/references/git-integration.md +4 -6
  135. package/pan-wizard-core/references/git-planning-commit.md +2 -2
  136. package/pan-wizard-core/references/guardrails.md +2 -2
  137. package/pan-wizard-core/references/handoff-decisions.md +3 -3
  138. package/pan-wizard-core/references/model-profile-resolution.md +5 -5
  139. package/pan-wizard-core/references/model-profiles.md +15 -10
  140. package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
  141. package/pan-wizard-core/references/planning-config.md +7 -7
  142. package/pan-wizard-core/references/tdd.md +10 -1
  143. package/pan-wizard-core/references/verdict-contract.md +6 -1
  144. package/pan-wizard-core/references/verification-patterns.md +5 -5
  145. package/pan-wizard-core/templates/codebase/architecture.md +2 -2
  146. package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
  147. package/pan-wizard-core/templates/codebase/concerns.md +3 -3
  148. package/pan-wizard-core/templates/codebase/integrations.md +6 -6
  149. package/pan-wizard-core/templates/codebase/relationships.md +2 -2
  150. package/pan-wizard-core/templates/codebase/stack.md +2 -2
  151. package/pan-wizard-core/templates/codebase/structure.md +4 -4
  152. package/pan-wizard-core/templates/codebase/testing.md +4 -4
  153. package/pan-wizard-core/templates/config.json +2 -6
  154. package/pan-wizard-core/templates/continue-here.md +1 -1
  155. package/pan-wizard-core/templates/debug-subagent-prompt.md +5 -3
  156. package/pan-wizard-core/templates/debug.md +15 -1
  157. package/pan-wizard-core/templates/milestone-archive.md +6 -6
  158. package/pan-wizard-core/templates/milestone.md +1 -1
  159. package/pan-wizard-core/templates/phase-prompt.md +31 -17
  160. package/pan-wizard-core/templates/planner-subagent-prompt.md +5 -5
  161. package/pan-wizard-core/templates/preview-report.md +1 -1
  162. package/pan-wizard-core/templates/research-project/architecture.md +3 -3
  163. package/pan-wizard-core/templates/research-project/features.md +1 -1
  164. package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
  165. package/pan-wizard-core/templates/research-project/stack.md +3 -3
  166. package/pan-wizard-core/templates/research.md +5 -5
  167. package/pan-wizard-core/templates/retrospective.md +1 -1
  168. package/pan-wizard-core/templates/roadmap.md +15 -15
  169. package/pan-wizard-core/templates/state.md +4 -4
  170. package/pan-wizard-core/templates/summary-complex.md +1 -0
  171. package/pan-wizard-core/templates/summary-minimal.md +1 -0
  172. package/pan-wizard-core/templates/summary-standard.md +2 -1
  173. package/pan-wizard-core/templates/summary.md +18 -2
  174. package/pan-wizard-core/templates/uat.md +3 -3
  175. package/pan-wizard-core/templates/user-setup.md +8 -8
  176. package/pan-wizard-core/templates/verification-report.md +53 -8
  177. package/pan-wizard-core/workflows/add-phase.md +1 -1
  178. package/pan-wizard-core/workflows/assumptions.md +1 -1
  179. package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
  180. package/pan-wizard-core/workflows/discuss-phase.md +10 -8
  181. package/pan-wizard-core/workflows/exec-phase.md +75 -95
  182. package/pan-wizard-core/workflows/execute-plan.md +8 -7
  183. package/pan-wizard-core/workflows/health.md +13 -12
  184. package/pan-wizard-core/workflows/help.md +8 -9
  185. package/pan-wizard-core/workflows/learn.md +13 -11
  186. package/pan-wizard-core/workflows/map-codebase.md +10 -10
  187. package/pan-wizard-core/workflows/milestone-audit.md +10 -10
  188. package/pan-wizard-core/workflows/milestone-gaps.md +18 -25
  189. package/pan-wizard-core/workflows/milestone-new.md +7 -6
  190. package/pan-wizard-core/workflows/new-project.md +17 -25
  191. package/pan-wizard-core/workflows/optimize.md +11 -9
  192. package/pan-wizard-core/workflows/pause.md +6 -1
  193. package/pan-wizard-core/workflows/phase-tests.md +4 -4
  194. package/pan-wizard-core/workflows/plan-phase.md +53 -42
  195. package/pan-wizard-core/workflows/profile.md +2 -2
  196. package/pan-wizard-core/workflows/progress.md +6 -8
  197. package/pan-wizard-core/workflows/quick.md +7 -5
  198. package/pan-wizard-core/workflows/remove-phase.md +1 -1
  199. package/pan-wizard-core/workflows/research-phase.md +9 -5
  200. package/pan-wizard-core/workflows/resume-project.md +4 -6
  201. package/pan-wizard-core/workflows/retro.md +3 -1
  202. package/pan-wizard-core/workflows/settings.md +16 -24
  203. package/pan-wizard-core/workflows/todo-check.md +4 -3
  204. package/pan-wizard-core/workflows/transition.md +16 -12
  205. package/pan-wizard-core/workflows/update.md +25 -17
  206. package/pan-wizard-core/workflows/verify-phase.md +22 -20
  207. package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
  208. package/pan-zcode/README.md +7 -7
  209. package/pan-zcode/bin/install-zcode.js +1 -1
  210. package/scripts/build-hooks.js +1 -0
  211. package/scripts/build-plugin.js +3 -1
  212. package/scripts/generate-skills-docs.py +15 -6
  213. package/scripts/prompt-lint.cjs +167 -0
  214. package/scripts/test-surface.cjs +1 -0
@@ -122,9 +122,11 @@ function cmdRetro(cwd, raw, args) {
122
122
  common_gap_patterns: gapGroups,
123
123
  };
124
124
 
125
- // E-4: optional memory write. Top gap patterns become lessons for pan-planner
126
- // (they surface what plans routinely miss). First-try rate deltas feed
127
- // pan-verifier memory.
125
+ // E-4: optional memory write. Top gap patterns become lessons in pan-planner's
126
+ // log (they surface what plans routinely miss), first-try rate deltas in
127
+ // pan-verifier's. Stored for /pan:knowledge: PAN's workflows no longer load agent
128
+ // memory into agents (ADR-0036, amended 2026-10-04), and /pan:army no longer
129
+ // passes the flag.
128
130
  const argsList = Array.isArray(args) ? args : [];
129
131
  if (argsList.includes('--write-memory')) {
130
132
  const { appendMemory } = require('./memory.cjs');
@@ -27,8 +27,11 @@ const LOCKFILES = new Set([
27
27
  ]);
28
28
  // PAN's own installed trees: a phase commit that touches them is PAN bookkeeping,
29
29
  // never the project's scope (pan-reviewer skips them for the same reason).
30
- const PAN_OWNED_PREFIXES = ['.claude/', '.codex/', '.gemini/', '.opencode/', '.agents/', '.github/pan-wizard-core/', '.github/agents/', '.github/prompts/', '.github/skills/'];
31
- const PAN_OWNED_FILES = new Set(['.github/copilot-instructions.md', '.github/hooks/pan.json']);
30
+ // A Copilot install shares `.github/` with the project: only what the installer writes as
31
+ // its own is listed, so the project's workflows, hooks and Copilot instructions stay in scope,
32
+ // and so do the files PAN merges into (`mcp.json`, `copilot/settings.json`).
33
+ const PAN_OWNED_PREFIXES = ['.claude/', '.codex/', '.gemini/', '.opencode/', '.agents/', '.github/pan-wizard-core/', '.github/pan-local-patches/', '.github/agents/pan-', '.github/skills/pan-', '.github/hooks/pan-'];
34
+ const PAN_OWNED_FILES = new Set(['.github/hooks/pan.json', '.github/package.json', '.github/pan-file-manifest.json']);
32
35
  const TEST_PATH_RE = /(^|\/)(tests?|__tests__|specs?)\//i;
33
36
  const TEST_FILE_RE = /\.(test|spec)\.[a-z0-9]+$/i;
34
37
  const COMMIT_SCAN_LIMIT = 2000;
@@ -65,27 +68,28 @@ function exclusionReason(file, planningPrefix) {
65
68
 
66
69
  /**
67
70
  * Files the phase's plan commits touched, paths relative to `cwd` (git's --relative).
68
- * @returns {{git: boolean, commits: number, files: Set<string>}}
71
+ * @returns {{git: boolean, commits: number, files: Set<string>, hashes: string[]}} hashes newest first
69
72
  */
70
73
  function committedFiles(cwd, phaseNumber) {
71
74
  const r = execGit(cwd, ['-c', 'core.quotepath=off', 'log', '--no-merges', '-n', String(COMMIT_SCAN_LIMIT), '--relative', '--name-only', '--format=@@%H%x09%s']);
72
- if (r.exitCode !== 0) return { git: false, commits: 0, files: new Set() };
75
+ if (r.exitCode !== 0) return { git: false, commits: 0, files: new Set(), hashes: [] };
73
76
  // `{type}({phase}-{plan}):` with the phase's zero padding optional ("feat(03-01):", "fix(3-02)!:").
74
77
  const unpadded = String(phaseNumber).replace(/^0+(?=\d)/, '');
75
78
  const subjectRe = new RegExp(`^[a-z]+\\(0*${escapeRegex(unpadded)}-[0-9]+\\)!?:`);
76
79
  const files = new Set();
80
+ const hashes = [];
77
81
  let commits = 0;
78
82
  let inPhase = false;
79
83
  for (const line of r.stdout.split('\n')) {
80
84
  if (line.startsWith('@@')) {
81
85
  const subject = line.slice(2).split('\t').slice(1).join('\t');
82
86
  inPhase = subjectRe.test(subject);
83
- if (inPhase) commits++;
87
+ if (inPhase) { commits++; hashes.push(line.slice(2).split('\t')[0]); }
84
88
  continue;
85
89
  }
86
90
  if (inPhase && line.trim()) files.add(normalisePath(line));
87
91
  }
88
- return { git: true, commits, files };
92
+ return { git: true, commits, files, hashes };
89
93
  }
90
94
 
91
95
  /**
@@ -220,4 +224,4 @@ function cmdVerifyStale(cwd, phase, raw) {
220
224
  output(r, raw, r.error ? undefined : r.state);
221
225
  }
222
226
 
223
- module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, LOCKFILES };
227
+ module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
@@ -25,6 +25,7 @@ const { detectInstalledRuntimes, validateRuntimeInstall, cmdValidateDeployment }
25
25
  const { cmdPreflight, cmdDepsValidate } = require('./verify-preflight.cjs');
26
26
  const { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale } = require('./verify-scope.cjs');
27
27
  const { planningRootRel } = require('./planning-root.cjs');
28
+ const { roadmapRequirementIds } = require('./roadmap.cjs');
28
29
 
29
30
  /**
30
31
  * Spot-check files mentioned in summary content.
@@ -439,11 +440,10 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
439
440
  // artifacts: [], key_links: [] }` (pan-wizard-core/templates/phase-prompt.md,
440
441
  // template.cjs generatePlanTemplate), so an empty block is the documented default
441
442
  // state of a scaffolded plan; presence of the block is enforced by
442
- // `verify plan-structure`, not here. (2) parseMustHavesBlock currently matches the
443
- // block header at 4-space indent while every shipped template and agent emits it at
444
- // 2 — so this branch is reached for *every* plan authored in PAN's own format, and
445
- // exiting non-zero would fail every real `verify artifacts` call. Reclassifying
446
- // this as a failure requires fixing that indentation mismatch first.
443
+ // `verify plan-structure`, not here. (2) parseMustHavesBlock once required a
444
+ // 4-space indent while every shipped template and agent writes 2, so this branch
445
+ // fired for every plan in PAN's own format. It reads relative indent now
446
+ // (frontmatter.cjs), so reaching it means the plan declares no artifacts.
447
447
  output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
448
448
  return;
449
449
  }
@@ -508,8 +508,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
508
508
  const r = checkKeyLinks(cwd, content);
509
509
  if (r.total === 0) {
510
510
  // EXIT_OK: same reasoning as cmdVerifyArtifacts above — an empty key_links block is
511
- // the shipped template default, and the parseMustHavesBlock indent mismatch means
512
- // this branch fires for every plan written in PAN's own format.
511
+ // the shipped template default, so a plan that declares none is a result, not a failure.
513
512
  output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
514
513
  return;
515
514
  }
@@ -550,10 +549,19 @@ function reconcilePhase(cwd, phaseNum) {
550
549
  const sm = verRaw.match(/^status:\s*([A-Za-z_-]+)/m);
551
550
  const status = sm ? sm[1].toLowerCase() : 'unknown';
552
551
  const claimsPass = /^(pass|passed|verified|complete|verified_pass)$/.test(status);
553
- const planFile = files.find(f => isPlanFile(f));
554
- const planContent = planFile ? (safeReadFile(path.join(dir, planFile)) || '') : '';
555
- const artifacts = checkArtifacts(cwd, planContent);
556
- const keyLinks = checkKeyLinks(cwd, planContent);
552
+ // Every plan's must_haves. Reading only the first plan let a later plan's failing
553
+ // artifact through under a passing verdict — the rubber stamp this check exists for.
554
+ const artifacts = { all_passed: true, passed: 0, total: 0, artifacts: [] };
555
+ const keyLinks = { all_verified: true, verified: 0, total: 0, links: [] };
556
+ for (const planFile of files.filter(f => isPlanFile(f)).sort()) {
557
+ const planContent = safeReadFile(path.join(dir, planFile)) || '';
558
+ const a = checkArtifacts(cwd, planContent);
559
+ const k = checkKeyLinks(cwd, planContent);
560
+ artifacts.passed += a.passed; artifacts.total += a.total; artifacts.artifacts.push(...a.artifacts.map(x => ({ plan: planFile, ...x })));
561
+ keyLinks.verified += k.verified; keyLinks.total += k.total; keyLinks.links.push(...k.links.map(x => ({ plan: planFile, ...x })));
562
+ }
563
+ artifacts.all_passed = artifacts.passed === artifacts.total;
564
+ keyLinks.all_verified = keyLinks.verified === keyLinks.total;
557
565
  const signals = artifacts.total + keyLinks.total;
558
566
  const contradictions = [];
559
567
  if (claimsPass) {
@@ -1236,11 +1244,10 @@ function syncRequirementCheckboxes(cwd) {
1236
1244
  // For each completed phase, find linked requirement IDs and check their boxes
1237
1245
  let fixed = 0;
1238
1246
  for (const phaseNum of completedPhases) {
1239
- const reqMatch = roadmapContent.match(
1240
- new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
1241
- );
1242
- if (!reqMatch) continue;
1243
- const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(id => id.trim()).filter(Boolean);
1247
+ // The phase's own requirements, as `phase complete` reads them: a bare "Phase N …
1248
+ // Requirements" search took the first section's line for every phase when the
1249
+ // checklist sits above the sections, as templates/roadmap.md lays it out.
1250
+ const reqIds = roadmapRequirementIds(roadmapContent, phaseNum);
1244
1251
  for (const reqId of reqIds) {
1245
1252
  const escaped = escapeRegex(reqId);
1246
1253
  const re = new RegExp(`(- \\[) (\\]\\s*\\*\\*${escaped}\\*\\*)`, 'gi');
@@ -1461,7 +1468,6 @@ function cmdValidateHealth(cwd, options, raw) {
1461
1468
  // Check 10 (optional): full validation — run tests and build
1462
1469
  let testStatus;
1463
1470
  let buildStatus;
1464
- let memoryBudget;
1465
1471
  if (options.full) {
1466
1472
  testStatus = runFullTestCheck(cwd);
1467
1473
  buildStatus = runFullBuildCheck(cwd);
@@ -1471,14 +1477,9 @@ function cmdValidateHealth(cwd, options, raw) {
1471
1477
  if (buildStatus.pass === false) {
1472
1478
  addIssue('error', 'BUILD_FAIL', `Build failed (exit code ${buildStatus.exitCode})`, 'Fix build errors');
1473
1479
  }
1474
- // Memory-load budget (ADR-0036 acceptance signal): keep per-agent memory
1475
- // injection bounded as logs grow. Read-only, non-blocking.
1476
- memoryBudget = require('./memory.cjs').memoryLoadBudget(cwd);
1477
- if (memoryBudget.status === 'critical') {
1478
- addIssue('warning', 'MEM_BUDGET', memoryBudget.advisory, "Run 'pan-tools memory compact <agent>' or scope injection with 'memory select'");
1479
- } else if (memoryBudget.status === 'warning') {
1480
- addIssue('info', 'MEM_BUDGET', memoryBudget.advisory, "Run 'pan-tools memory compact <agent>' or scope injection with 'memory select'");
1481
- }
1480
+ // No memory-load check: PAN's workflows no longer load agent memory into
1481
+ // agents (ADR-0036, amended 2026-10-04), so its size costs no agent context.
1482
+ // `pan-tools memory budget` still sizes the store on request.
1482
1483
  }
1483
1484
 
1484
1485
  // Check 11 (optional): drift analysis
@@ -1539,7 +1540,6 @@ function cmdValidateHealth(cwd, options, raw) {
1539
1540
  if (options.full) {
1540
1541
  result.test_status = testStatus;
1541
1542
  result.build_status = buildStatus;
1542
- result.memory_budget = memoryBudget;
1543
1543
  }
1544
1544
  if (options.drift) {
1545
1545
  result.drift_status = driftResult;
@@ -1549,8 +1549,10 @@ function cmdValidateHealth(cwd, options, raw) {
1549
1549
  }
1550
1550
 
1551
1551
  // Explicit verdict exit: `broken` → 1; `degraded` and `healthy` → 0 (warnings are
1552
- // not failures). Computed AFTER --repair ran, so the code reflects the post-repair
1553
- // state the JSON reports. See the note at the early-return site above.
1552
+ // not failures). The issue lists were collected before --repair ran and a repair
1553
+ // removes nothing from them, so the status and this code describe the tree as it
1554
+ // was before the repairs; `repairs_performed` says what changed, and a second run
1555
+ // shows the result. See the note at the early-return site above.
1554
1556
  output(result, raw, undefined, status === HEALTH_STATUS.BROKEN ? 1 : 0);
1555
1557
  }
1556
1558
 
@@ -187,10 +187,12 @@ function createWorktree(cwd, phaseNum, scenario, opts) {
187
187
  return { error: 'Not a git repo — what-if requires git worktree support' };
188
188
  }
189
189
  const slug = scenarioSlug(scenario);
190
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
191
- const branch = `${BRANCH_PREFIX}${phaseNum}-${slug}-${ts.slice(0, 15)}`;
190
+ // To the millisecond: cut to `slice(0, 15)` it had ten-minute resolution, so a repeat
191
+ // of the same phase and scenario within ten minutes failed on an existing branch.
192
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 23);
193
+ const branch = `${BRANCH_PREFIX}${phaseNum}-${slug}-${ts}`;
192
194
  const worktreeRoot = opts?.worktree_root
193
- || path.join(path.dirname(cwd), `pan-whatif-${phaseNum}-${slug}-${ts.slice(0, 15)}`);
195
+ || path.join(path.dirname(cwd), `pan-whatif-${phaseNum}-${slug}-${ts}`);
194
196
 
195
197
  // Base ref: current HEAD by default. Callers can override (e.g. to branch
196
198
  // off main for a clean comparison).
@@ -208,10 +210,38 @@ function createWorktree(cwd, phaseNum, scenario, opts) {
208
210
  };
209
211
  }
210
212
 
213
+ /** Registered worktrees as {path, branch}; branch is null on a detached HEAD. */
214
+ function listWorktrees(cwd) {
215
+ const r = execGit(cwd, ['worktree', 'list', '--porcelain']);
216
+ if (r.exitCode !== 0) return [];
217
+ const list = [];
218
+ for (const line of r.stdout.split(/\r?\n/)) {
219
+ if (line.startsWith('worktree ')) list.push({ path: line.slice('worktree '.length), branch: null });
220
+ else if (line.startsWith('branch ') && list.length) list[list.length - 1].branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
221
+ }
222
+ return list;
223
+ }
224
+
225
+ function samePath(a, b) {
226
+ const norm = (p) => {
227
+ let r = path.resolve(p);
228
+ try { r = fs.realpathSync.native(r); } catch { /* gone: compare the resolved form */ }
229
+ r = toPosix(r);
230
+ return process.platform === 'win32' ? r.toLowerCase() : r;
231
+ };
232
+ return norm(a) === norm(b);
233
+ }
234
+
211
235
  /**
212
236
  * Remove a worktree + its branch. Best-effort: errors are surfaced but
213
237
  * don't block subsequent cleanups.
214
238
  *
239
+ * Only what-if's own objects are touched: the branch must carry BRANCH_PREFIX and the
240
+ * worktree, when git still lists it, must be checked out on that branch (or, with no
241
+ * branch given, on some BRANCH_PREFIX branch). Anything else is refused before a single
242
+ * git write, because `branch -D` and `worktree remove --force` destroy unmerged commits
243
+ * and uncommitted files.
244
+ *
215
245
  * @param {string} cwd - Main project root (for the cleanup git call)
216
246
  * @param {string} worktreePath - Path returned by createWorktree
217
247
  * @param {string} branch - Branch name returned by createWorktree
@@ -222,15 +252,29 @@ function cleanupWorktree(cwd, worktreePath, branch, opts) {
222
252
  if (!isGitRepo(cwd)) {
223
253
  return { error: 'Not a git repo' };
224
254
  }
255
+ if (branch && !branch.startsWith(BRANCH_PREFIX)) {
256
+ return { error: `Refusing to delete branch '${branch}': what-if only cleans up branches it created (${BRANCH_PREFIX}*)` };
257
+ }
258
+ const entry = listWorktrees(cwd).find(w => samePath(w.path, worktreePath));
259
+ if (entry) {
260
+ const owned = entry.branch && entry.branch.startsWith(BRANCH_PREFIX) && (!branch || entry.branch === branch);
261
+ if (!owned) {
262
+ return { error: `Refusing to remove worktree ${toPosix(worktreePath)}: it is on '${entry.branch || 'a detached HEAD'}', not ${branch || BRANCH_PREFIX + '*'}` };
263
+ }
264
+ }
225
265
  const warnings = [];
226
266
  const force = opts?.force === true;
227
267
 
228
- const rmArgs = ['worktree', 'remove'];
229
- if (force) rmArgs.push('--force');
230
- rmArgs.push(worktreePath);
231
- const removeResult = execGit(cwd, rmArgs);
232
- if (removeResult.exitCode !== 0) {
233
- warnings.push(`worktree remove: ${removeResult.stderr}`);
268
+ if (entry) {
269
+ const rmArgs = ['worktree', 'remove'];
270
+ if (force) rmArgs.push('--force');
271
+ rmArgs.push(worktreePath);
272
+ const removeResult = execGit(cwd, rmArgs);
273
+ if (removeResult.exitCode !== 0) {
274
+ warnings.push(`worktree remove: ${removeResult.stderr}`);
275
+ }
276
+ } else {
277
+ warnings.push(`worktree remove: ${toPosix(worktreePath)} is not a registered worktree (already removed?)`);
234
278
  }
235
279
 
236
280
  // Branch cleanup — only if branch still exists.
@@ -31,7 +31,7 @@
31
31
  * findings record|list|dispose|debt Judges' verdicts and their findings ledger (evidence loop)
32
32
  * phase-plan-index <phase> Index plans with waves and status
33
33
  * websearch <query> Search web via Brave API (if configured)
34
- * [--limit N] [--freshness day|week|month]
34
+ * [--limit N] [--freshness day|week|month|year]
35
35
  *
36
36
  * Phase Listing:
37
37
  * phases list [--type plans|summaries] List phases with optional type filter
@@ -187,6 +187,35 @@
187
187
 
188
188
  const fs = require('fs');
189
189
  const path = require('path');
190
+
191
+ // Compile cache (Node 22.1+; a no-op before): V8 reuses the compiled code of pan-tools
192
+ // and its modules across processes, which workflows start many times per phase — a
193
+ // `version` call went from 100 to 75 ms median (measured 2026-10-10, bare node 44 ms).
194
+ // The cache sits in the per-user 0700 directory the hooks already use, never Node's
195
+ // default under the shared system temp directory, where another user could plant
196
+ // compiled code. A directory that is a symlink, not ours, or open to group or other
197
+ // disables it. NODE_COMPILE_CACHE, when set, is Node's own choice and wins; any failure
198
+ // runs without a cache.
199
+ function enableCompileCache() {
200
+ try {
201
+ const mod = require('module');
202
+ if (typeof mod.enableCompileCache !== 'function' || process.env.NODE_COMPILE_CACHE) return;
203
+ const os = require('os');
204
+ const uid = typeof process.getuid === 'function' ? process.getuid() : (process.env.USERNAME || 'win');
205
+ const parent = path.join(os.tmpdir(), `pan-hooks-${uid}`);
206
+ const dir = path.join(parent, 'compile-cache');
207
+ for (const d of [parent, dir]) {
208
+ fs.mkdirSync(d, { recursive: true, mode: 0o700 });
209
+ const st = fs.lstatSync(d);
210
+ if (st.isSymbolicLink()) return;
211
+ // POSIX only: Windows reports synthetic mode bits.
212
+ if (typeof process.getuid === 'function' && (st.uid !== process.getuid() || (st.mode & 0o077) !== 0)) return;
213
+ }
214
+ mod.enableCompileCache(dir);
215
+ } catch { /* best effort: run uncached */ }
216
+ }
217
+ enableCompileCache();
218
+
190
219
  const { error, output, buildCachedContext } = require('./lib/core.cjs');
191
220
  const state = require('./lib/state.cjs');
192
221
  const phase = require('./lib/phase.cjs');
@@ -208,6 +237,9 @@ const costRebuild = require('./lib/cost-rebuild.cjs');
208
237
  const preview = require('./lib/preview.cjs');
209
238
  const reviewDeep = require('./lib/review-deep.cjs');
210
239
  const findings = require('./lib/findings.cjs');
240
+ const verifyPlanSet = require('./lib/verify-plan-set.cjs');
241
+ const verifyIntegrity = require('./lib/verify-integrity.cjs');
242
+ const phaseGate = require('./lib/phase-gate.cjs');
211
243
  const knowledge = require('./lib/knowledge.cjs');
212
244
  const skillAlign = require('./lib/skill-align.cjs');
213
245
  const hygiene = require('./lib/hygiene.cjs');
@@ -333,7 +365,7 @@ async function main() {
333
365
  + ' --planning-dir <path> act on an arbitrary project-relative planning tree\n'
334
366
  + ' --all-tracks (hygiene) act on the root tree AND every discovered track\n'
335
367
  + ' env: PAN_TRACK, PAN_PLANNING_DIR (flags win)\n'
336
- + '\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, findings, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links';
368
+ + '\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, findings, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links, version';
337
369
 
338
370
  if (!command) {
339
371
  error(USAGE);
@@ -538,9 +570,16 @@ async function main() {
538
570
  const force = args.includes('--force');
539
571
  const failOnError = args.includes('--fail-on-error');
540
572
  const message = args[1] && !args[1].startsWith('--') ? args[1] : null;
541
- // Parse --files flag (collect args after --files, stopping at other flags)
573
+ // Parse --files flag: collect args after --files, stopping at the next flag. A
574
+ // filter here took every later bare argument, so `--files f --type docs` staged `docs`.
542
575
  const filesIndex = args.indexOf('--files');
543
- const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
576
+ const files = [];
577
+ if (filesIndex !== -1) {
578
+ for (const a of args.slice(filesIndex + 1)) {
579
+ if (a.startsWith('--')) break;
580
+ files.push(a);
581
+ }
582
+ }
544
583
  const commitType = getArgValue(args, '--type');
545
584
  commands.cmdCommit(cwd, message, files, raw, amend, { type: commitType, force, failOnError });
546
585
  break;
@@ -613,8 +652,12 @@ async function main() {
613
652
  verify.cmdVerifyScope(cwd, args[2], raw);
614
653
  } else if (subcommand === 'stale') {
615
654
  verify.cmdVerifyStale(cwd, args[2], raw);
655
+ } else if (subcommand === 'plan-set') {
656
+ verifyPlanSet.cmdVerifyPlanSet(cwd, args[2], raw);
657
+ } else if (subcommand === 'test-integrity') {
658
+ verifyIntegrity.cmdVerifyTestIntegrity(cwd, args[2], raw);
616
659
  } else {
617
- error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale');
660
+ error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale, plan-set, test-integrity');
618
661
  }
619
662
  break;
620
663
  }
@@ -689,8 +732,16 @@ async function main() {
689
732
  } else if (subcommand === 'update-plan-progress') {
690
733
  if (!args[2]) error('roadmap update-plan-progress requires a phase number');
691
734
  roadmap.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw);
735
+ } else if (subcommand === 'slice') {
736
+ if (!args[2]) error('roadmap slice requires a phase number');
737
+ roadmap.cmdRoadmapSlice(cwd, args[2], { write: args.includes('--write') }, raw);
738
+ } else if (subcommand === 'compact') {
739
+ const keepIdx = args.indexOf('--keep');
740
+ const keep = keepIdx !== -1 ? Number(args[keepIdx + 1]) : undefined;
741
+ if (keepIdx !== -1 && !(Number.isInteger(keep) && keep >= 0)) error('roadmap compact --keep needs a whole number of phases (0 or more)');
742
+ require('./lib/roadmap-compact.cjs').cmdRoadmapCompact(cwd, { apply: args.includes('--apply'), keep }, raw);
692
743
  } else {
693
- error('Unknown roadmap subcommand. Available: get-phase, analyze, update-plan-progress');
744
+ error('Unknown roadmap subcommand. Available: get-phase, analyze, update-plan-progress, slice, compact');
694
745
  }
695
746
  break;
696
747
  }
@@ -722,9 +773,11 @@ async function main() {
722
773
  } else if (subcommand === 'complete') {
723
774
  if (!args[2]) error('phase complete requires a phase number');
724
775
  const noCommit = args.includes('--no-commit');
725
- phase.cmdPhaseComplete(cwd, args[2], raw, { noCommit });
776
+ phase.cmdPhaseComplete(cwd, args[2], raw, { noCommit, force: args.includes('--force'), reason: getArgValue(args, '--reason') });
777
+ } else if (subcommand === 'gate') {
778
+ phaseGate.cmdPhaseGate(cwd, args[2], raw, { mode: getArgValue(args, '--mode') || undefined });
726
779
  } else {
727
- error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete');
780
+ error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete, gate');
728
781
  }
729
782
  break;
730
783
  }
@@ -1038,7 +1091,12 @@ async function main() {
1038
1091
  if (subcommand === 'read') {
1039
1092
  memory.cmdMemoryRead(cwd, args[2], raw);
1040
1093
  } else if (subcommand === 'append') {
1041
- memory.cmdMemoryAppend(cwd, args[2], args.slice(3).join(' '), raw);
1094
+ // `--cites <path[#symbol]>,...` is taken out before the entry words are joined.
1095
+ const rest = args.slice(3);
1096
+ const ci = rest.indexOf('--cites');
1097
+ const cites = ci === -1 ? undefined : rest[ci + 1];
1098
+ if (ci !== -1) rest.splice(ci, 2);
1099
+ memory.cmdMemoryAppend(cwd, args[2], rest.join(' '), raw, { cites });
1042
1100
  } else if (subcommand === 'list') {
1043
1101
  memory.cmdMemoryList(cwd, raw);
1044
1102
  } else if (subcommand === 'compact') {
@@ -1048,7 +1106,22 @@ async function main() {
1048
1106
  cue: getArgValue(args, '--cue'),
1049
1107
  tokenBudget: getArgValue(args, '--token-budget'),
1050
1108
  recencyFloor: getArgValue(args, '--recency-floor'),
1109
+ all: args.includes('--all'),
1110
+ markUsed: args.includes('--mark-used'),
1111
+ expireDays: getArgValue(args, '--days'),
1112
+ }, raw);
1113
+ } else if (subcommand === 'record') {
1114
+ memory.cmdMemoryRecord(cwd, args[2] && !args[2].startsWith('--') ? args[2] : null, {
1115
+ lesson: getArgValue(args, '--lesson'),
1116
+ finding: getArgValue(args, '--finding'),
1117
+ trace: getArgValue(args, '--trace'),
1118
+ cites: getArgValue(args, '--cites'),
1051
1119
  }, raw);
1120
+ } else if (subcommand === 'prune') {
1121
+ const daysArg = getArgValue(args, '--days');
1122
+ if (daysArg !== null && !/^\d+$/.test(daysArg)) error('memory prune --days needs a whole number of days (0 turns expiry off)');
1123
+ const target = args[2] && !args[2].startsWith('--') ? args[2] : null;
1124
+ memory.cmdMemoryPrune(cwd, target, { apply: args.includes('--apply'), days: daysArg === null ? undefined : Number(daysArg) }, raw);
1052
1125
  } else if (subcommand === 'budget') {
1053
1126
  memory.cmdMemoryBudget(cwd, raw);
1054
1127
  } else if (subcommand === 'optimize') {
@@ -1062,7 +1135,7 @@ async function main() {
1062
1135
  apply: args.includes('--apply'),
1063
1136
  }, raw);
1064
1137
  } else {
1065
- error('Unknown memory subcommand. Available: read, append, list, compact, select, budget, optimize, rebuild');
1138
+ error('Unknown memory subcommand. Available: read, append, list, compact, select, record, prune, budget, optimize, rebuild');
1066
1139
  }
1067
1140
  break;
1068
1141
  }
@@ -1117,7 +1190,14 @@ async function main() {
1117
1190
  case 'knowledge': {
1118
1191
  const subcommand = args[1];
1119
1192
  if (subcommand === 'ask') {
1120
- const question = args.slice(2).filter(a => !a.startsWith('--')).join(' ');
1193
+ // The question is every word after `ask` that is neither a flag nor a flag's
1194
+ // value: `--recall-cue "session tokens" --max-sources 3` used to join it.
1195
+ const words = [];
1196
+ for (let i = 2; i < args.length; i++) {
1197
+ if (args[i] === '--max-sources' || args[i] === '--recall-cue') { i++; continue; }
1198
+ if (!args[i].startsWith('--')) words.push(args[i]);
1199
+ }
1200
+ const question = words.join(' ');
1121
1201
  const maxSources = getArgValue(args, '--max-sources');
1122
1202
  knowledge.cmdKnowledgeAsk(cwd, question, {
1123
1203
  max_sources: maxSources ? Number(maxSources) : undefined,
@@ -1249,7 +1329,8 @@ async function main() {
1249
1329
 
1250
1330
  case 'cost': {
1251
1331
  const subcommand = args[1];
1252
- if (subcommand === 'report' || !subcommand) {
1332
+ // `report` is the default, so a leading flag (`cost --format chart`) means report, as for `links`.
1333
+ if (subcommand === 'report' || !subcommand || subcommand.startsWith('--')) {
1253
1334
  const format = getArgValue(args, '--format', 'json');
1254
1335
  const since = getArgValue(args, '--since');
1255
1336
  const until = getArgValue(args, '--until');
@@ -1276,8 +1357,10 @@ async function main() {
1276
1357
  mainThread: !args.includes('--no-main-thread'),
1277
1358
  claudeDir: getArgValue(args, '--claude-dir'),
1278
1359
  }, raw);
1360
+ } else if (subcommand === 'limits') {
1361
+ cost.cmdCostLimits(cwd, raw);
1279
1362
  } else {
1280
- error('Unknown cost subcommand. Available: report, append, clear, rebuild');
1363
+ error('Unknown cost subcommand. Available: report, append, clear, rebuild, limits');
1281
1364
  }
1282
1365
  break;
1283
1366
  }
@@ -1330,7 +1413,7 @@ async function main() {
1330
1413
  campaign.cmdCampaignSchedule(cwd, {
1331
1414
  goal: getArgValue(args, '--goal'),
1332
1415
  source: getArgValue(args, '--source'),
1333
- cadence: getArgValue(args, '--cadence', 'daily'),
1416
+ cadence: getArgValue(args, '--cadence'),
1334
1417
  daily_budget: budget != null ? Number(budget) : undefined,
1335
1418
  enabled: args.includes('--disable') ? false : undefined,
1336
1419
  paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
@@ -1539,9 +1622,11 @@ async function main() {
1539
1622
  ? sourceExpsCsv.split(',').map(s => s.trim()).filter(Boolean)
1540
1623
  : [];
1541
1624
  const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1625
+ const citesCsv = getArgValue(args, '--cites') || '';
1626
+ const cites = citesCsv ? citesCsv.split(',').map(s => s.trim()).filter(Boolean) : [];
1542
1627
 
1543
1628
  const result = optimize.promotePattern(
1544
- { id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments },
1629
+ { id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments, ...(cites.length ? { cites } : {}) },
1545
1630
  { scope, topic, sourceRoot }
1546
1631
  );
1547
1632
  output(result, raw);
@@ -1589,9 +1674,13 @@ async function main() {
1589
1674
  if (!agent) { error('learn topics-for requires --agent <name>'); }
1590
1675
  const minRelevance = getArgValue(args, '--min-relevance', 'medium');
1591
1676
  const tokenBudget = parseInt(getArgValue(args, '--token-budget', '5000'), 10);
1592
- const result = learnIndex.cmdTopicsFor(sourceRoot, { agent, minRelevance, tokenBudget });
1677
+ const cue = getArgValue(args, '--cue') || undefined;
1678
+ const result = learnIndex.cmdTopicsFor(sourceRoot, { agent, minRelevance, tokenBudget, cue });
1593
1679
  if (raw) {
1594
- const lines = [`Topics for "${agent}" (min ${minRelevance}, budget ${tokenBudget}):`, ``];
1680
+ const head = result.mode === 'cue'
1681
+ ? `Topics for "${agent}" matching the cue (budget ${tokenBudget}):`
1682
+ : `Topics for "${agent}" (min ${minRelevance}, budget ${tokenBudget})${cue ? ' — nothing matched the cue, so by role' : ''}:`;
1683
+ const lines = [head, ``];
1595
1684
  for (const t of result.selected) {
1596
1685
  lines.push(` [${t.relevance.padEnd(6)}] ${t.scope}/${t.name.padEnd(22)} ${t.tokens.toString().padStart(5)}t ${t.patterns.join(', ')}`);
1597
1686
  }
@@ -1646,18 +1735,24 @@ async function main() {
1646
1735
  // and to the suggestion index that is now parsed from those strings.
1647
1736
  // Publishing it fixes the error AND feeds the suggester, with no second
1648
1737
  // list to maintain.
1649
- if (subcommand) {
1738
+ //
1739
+ // A flag is not a subcommand: `learn --session <id>` and `learn --sessions N`
1740
+ // take the alias with `optimize learn`'s flags (the guard used to refuse both).
1741
+ if (subcommand && !subcommand.startsWith('--')) {
1650
1742
  error('Unknown learn subcommand. Available: promote, unpromote, list-promoted, build-index, topics-for, lint');
1651
1743
  }
1652
1744
  optimize.cmdOptimizeLearn(cwd, {
1653
1745
  sessionId: getArgValue(args, '--session'),
1746
+ sessions: getArgValue(args, '--sessions'),
1654
1747
  }, raw);
1655
1748
  break;
1656
1749
  }
1657
1750
 
1658
1751
  case 'links': {
1659
1752
  const subcommand = args[1];
1660
- if (subcommand === 'validate' || !subcommand) {
1753
+ // `validate` is the default, flags and all: `links --strict` used to be refused
1754
+ // as an unknown subcommand while bare `links` validated.
1755
+ if (subcommand === 'validate' || !subcommand || subcommand.startsWith('--')) {
1661
1756
  const collectMulti = (flag) => {
1662
1757
  const vals = [];
1663
1758
  for (let i = 0; i < args.length; i++) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "generated_at": "2026-08-07T13:36:47.532Z",
3
+ "generated_at": "2026-10-04T10:14:05.059Z",
4
4
  "topics": [
5
5
  {
6
6
  "name": "experiment-runner",
@@ -14,8 +14,8 @@
14
14
  "P-EXP-005",
15
15
  "P-NPRS-005"
16
16
  ],
17
- "size_bytes": 8889,
18
- "size_tokens_est": 2223,
17
+ "size_bytes": 8956,
18
+ "size_tokens_est": 2239,
19
19
  "agent_relevance": {
20
20
  "planner": "low",
21
21
  "executor": "low",
@@ -804,7 +804,7 @@
804
804
  "totals": {
805
805
  "topics": 47,
806
806
  "patterns": 93,
807
- "size_bytes": 150126,
808
- "size_tokens_est": 37549
807
+ "size_bytes": 150193,
808
+ "size_tokens_est": 37565
809
809
  }
810
810
  }
@@ -52,17 +52,28 @@ function writeJson(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true });
52
52
  * Stage a merge request. Mints a fresh per-request approval token (nonce), records the
53
53
  * exact branch + the (caller-attested) CI/verify flags, marks it awaiting_approval, and
54
54
  * returns the record. Does NOT merge. The operator sets PAN_MERGE_APPROVAL to
55
- * `record.approval_token` after reviewing. (ci_green/verify_pass are agent-attested
56
- * advisory context for the human's decision; the token is the sole unfakeable anchor.)
55
+ * `record.approval_token` after reviewing. ci_green is agent-attested advisory context.
56
+ * verify_pass is too, unless the request names a `phase`: then it is the phase gate's
57
+ * verdict (program V1), computed here, and the caller's flag is ignored. The token is
58
+ * the sole unfakeable anchor either way.
57
59
  */
58
60
  function requestMerge(cwd, opts = {}) {
59
61
  const branch = validateBranch(opts.branch);
62
+ let verifyPass = !!opts.verify_pass;
63
+ let gate = null;
64
+ if (opts.phase != null && String(opts.phase).trim() !== '') {
65
+ const g = require('../bin/lib/phase-gate.cjs').phaseGate(cwd, String(opts.phase).trim());
66
+ gate = g.error ? { phase: String(opts.phase), error: g.error } : { phase: g.phase, ok: g.ok, reasons: g.reasons.map((r) => r.code) };
67
+ verifyPass = !g.error && g.ok;
68
+ }
60
69
  const record = {
61
70
  id: slugify(branch),
62
71
  branch,
63
72
  approval_token: crypto.randomBytes(9).toString('hex'),
64
73
  ci_green: !!opts.ci_green,
65
- verify_pass: !!opts.verify_pass,
74
+ verify_pass: verifyPass,
75
+ verify_source: gate ? 'phase_gate' : 'caller',
76
+ ...(gate ? { gate } : {}),
66
77
  status: 'awaiting_approval',
67
78
  requested_at: opts.now || null,
68
79
  };
@@ -126,14 +126,14 @@ const NATIVE_TOOLS = [
126
126
  {
127
127
  name: 'pan_request_merge',
128
128
  title: 'Request a gated merge',
129
- description: 'Stage a squash-merge request for a branch and mark it awaiting human approval. Records intent only — does NOT merge.',
129
+ description: 'Stage a squash-merge request for a branch and mark it awaiting human approval. Records intent only — does NOT merge. Name the phase the branch delivers: verify_pass is then the phase gate\'s verdict, not the caller\'s flag.',
130
130
  readOnly: false, destructive: false,
131
131
  inputSchema: {
132
132
  type: 'object', additionalProperties: false, required: ['branch'],
133
- properties: { branch: { type: 'string' }, ci_green: { type: 'boolean' }, verify_pass: { type: 'boolean' } },
133
+ properties: { branch: { type: 'string' }, ci_green: { type: 'boolean' }, verify_pass: { type: 'boolean' }, phase: { type: 'string' } },
134
134
  },
135
135
  handler: ({ cwd, input }) => ({
136
- json: mergeGate.requestMerge(cwd, { branch: input.branch, ci_green: input.ci_green, verify_pass: input.verify_pass }),
136
+ json: mergeGate.requestMerge(cwd, { branch: input.branch, ci_green: input.ci_green, verify_pass: input.verify_pass, phase: input.phase }),
137
137
  }),
138
138
  },
139
139
  {