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
@@ -16,14 +16,16 @@ const {
16
16
  } = require('./constants.cjs');
17
17
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, fileAccessible, detectPlanningModel } = require('./utils.cjs');
18
18
  const { detectForeignPlanningTree } = require('./foreign-planning.cjs');
19
+ const { unknownConfigKeys, nearestConfigKey } = require('./config.cjs');
19
20
  // Drift detection lives in verify-drift.cjs; re-exported below so consumers of
20
21
  // verify.cjs are unaffected by the decomposition.
21
22
  const { runDriftCheck, parseConventionRules, checkFileConventions, calculateDriftScore, getChangedFiles, cmdDriftCheck } = require('./verify-drift.cjs');
22
23
  const { collectVerificationStats, countRoadmapPhases, groupGapPatterns, cmdRetro } = require('./verify-retro.cjs');
23
24
  const { detectInstalledRuntimes, validateRuntimeInstall, cmdValidateDeployment } = require('./verify-deploy.cjs');
24
25
  const { cmdPreflight, cmdDepsValidate } = require('./verify-preflight.cjs');
25
- const { scopePhase, cmdVerifyScope } = require('./verify-scope.cjs');
26
+ const { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale } = require('./verify-scope.cjs');
26
27
  const { planningRootRel } = require('./planning-root.cjs');
28
+ const { roadmapRequirementIds } = require('./roadmap.cjs');
27
29
 
28
30
  /**
29
31
  * Spot-check files mentioned in summary content.
@@ -116,6 +118,56 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
116
118
  }, raw, passed ? 'passed' : 'failed');
117
119
  }
118
120
 
121
+ // Longer than a signature, a type or a short config snippet: an implementation body.
122
+ const MAX_ACTION_CODE_LINES = 20;
123
+
124
+ /** The line count of the longest fenced code block in a piece of markdown (0 if none). */
125
+ function longestFencedBlock(text) {
126
+ let longest = 0;
127
+ for (const m of String(text).matchAll(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n[ \t]*\2[ \t]*(?=\n|$)/g)) {
128
+ longest = Math.max(longest, m[3].split('\n').length);
129
+ }
130
+ return longest;
131
+ }
132
+
133
+ // The elements each checkpoint type needs (references/checkpoints.md).
134
+ const CHECKPOINT_REQUIRED = {
135
+ 'checkpoint:human-verify': ['what-built', 'how-to-verify'],
136
+ 'checkpoint:decision': ['decision', 'options'],
137
+ 'checkpoint:human-action': ['action'],
138
+ };
139
+
140
+ /**
141
+ * Check one checkpoint task against its type's shape. A decision checkpoint may
142
+ * carry `auto_select="<option id>"`: the option auto mode takes. Without one, auto
143
+ * mode stops for a human instead of taking whichever option is listed first
144
+ * (market-ideas M22), and an `auto_select` naming no option is an error.
145
+ * @returns {{name: string, type: string, auto_select?: string|null}} the task entry
146
+ */
147
+ function checkCheckpointTask(type, attrs, body, errors, warnings) {
148
+ const keyText = (body.match(/<(?:decision|what-built|action)>([\s\S]*?)<\/(?:decision|what-built|action)>/) || [])[1];
149
+ const label = keyText ? keyText.trim().replace(/\s+/g, ' ').slice(0, 80) : `unnamed ${type}`;
150
+ const required = CHECKPOINT_REQUIRED[type];
151
+ if (!required) {
152
+ errors.push(`Task '${label}' has unknown checkpoint type '${type}'`);
153
+ return { name: label, type };
154
+ }
155
+ for (const el of required) {
156
+ if (!body.includes(`<${el}>`)) errors.push(`Checkpoint '${label}' missing <${el}>`);
157
+ }
158
+ if (type !== 'checkpoint:decision') return { name: label, type };
159
+
160
+ const optionIds = [...body.matchAll(/<option\b[^>]*\bid=["']([^"']+)["']/g)].map(m => m[1]);
161
+ if (optionIds.length === 0) errors.push(`Decision checkpoint '${label}' has no <option id="...">`);
162
+ const autoMatch = attrs.match(/\bauto_select=["']([^"']*)["']/);
163
+ if (!autoMatch) {
164
+ warnings.push(`Decision checkpoint '${label}' has no auto_select: auto mode stops here for a human`);
165
+ } else if (!optionIds.includes(autoMatch[1])) {
166
+ errors.push(`Decision checkpoint '${label}' auto_select="${autoMatch[1]}" names no option (options: ${optionIds.join(', ') || 'none'})`);
167
+ }
168
+ return { name: label, type, auto_select: autoMatch ? autoMatch[1] : null };
169
+ }
170
+
119
171
  /**
120
172
  * Validate a plan.md structure: required frontmatter, task elements, and consistency.
121
173
  * @param {string} cwd - Working directory path
@@ -139,14 +191,24 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
139
191
  if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
140
192
  }
141
193
 
142
- // Parse XML <task> elements and validate each has required sub-elements:
143
- // <name> (required), <action> (required), <verify> (recommended),
144
- // <done> (recommended), <files> (recommended)
145
- const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
194
+ // Parse XML <task> elements. An auto task needs <name> and <action> and should
195
+ // have <verify>, <done> and <files>. A checkpoint task has its own shape
196
+ // (references/checkpoints.md) and is checked against that instead: until
197
+ // 2026-10-03 every checkpoint failed the auto-task checks, and the `<task[^>]*>`
198
+ // pattern also matched the `<tasks>` wrapper, so a decision's first option name
199
+ // was read as the task's name. `\b` keeps the wrapper out.
200
+ const taskPattern = /<task\b([^>]*)>([\s\S]*?)<\/task>/g;
146
201
  const tasks = [];
147
202
  let taskMatch;
148
203
  while ((taskMatch = taskPattern.exec(content)) !== null) {
149
- const taskContent = taskMatch[1];
204
+ const attrs = taskMatch[1];
205
+ const taskContent = taskMatch[2];
206
+ const typeMatch = attrs.match(/\btype=["']?([\w:-]+)/);
207
+ const taskType = typeMatch ? typeMatch[1] : 'auto';
208
+ if (taskType.startsWith('checkpoint:')) {
209
+ tasks.push(checkCheckpointTask(taskType, attrs, taskContent, errors, warnings));
210
+ continue;
211
+ }
150
212
  const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
151
213
  const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
152
214
  const hasFiles = /<files>/.test(taskContent);
@@ -156,11 +218,19 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
156
218
 
157
219
  if (!nameMatch) errors.push('Task missing <name> element');
158
220
  if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
221
+ // Decisions, not code (market-ideas M32): an <action> names signatures, values,
222
+ // assertions and the verify command. A long fenced block there is an
223
+ // implementation body the planner wrote for the executor.
224
+ const actionText = (taskContent.match(/<action>([\s\S]*?)<\/action>/) || [])[1] || '';
225
+ const longest = longestFencedBlock(actionText);
226
+ if (longest > MAX_ACTION_CODE_LINES) {
227
+ warnings.push(`Task '${taskName}' embeds a ${longest}-line code block in <action>: a plan records decisions (signatures, values, assertions, the verify command), not implementation`);
228
+ }
159
229
  if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
160
230
  if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
161
231
  if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
162
232
 
163
- tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
233
+ tasks.push({ name: taskName, type: taskType, hasFiles, hasAction, hasVerify, hasDone });
164
234
  }
165
235
 
166
236
  if (tasks.length === 0) warnings.push('No <task> elements found');
@@ -370,11 +440,10 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
370
440
  // artifacts: [], key_links: [] }` (pan-wizard-core/templates/phase-prompt.md,
371
441
  // template.cjs generatePlanTemplate), so an empty block is the documented default
372
442
  // state of a scaffolded plan; presence of the block is enforced by
373
- // `verify plan-structure`, not here. (2) parseMustHavesBlock currently matches the
374
- // block header at 4-space indent while every shipped template and agent emits it at
375
- // 2 — so this branch is reached for *every* plan authored in PAN's own format, and
376
- // exiting non-zero would fail every real `verify artifacts` call. Reclassifying
377
- // 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.
378
447
  output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
379
448
  return;
380
449
  }
@@ -439,8 +508,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
439
508
  const r = checkKeyLinks(cwd, content);
440
509
  if (r.total === 0) {
441
510
  // EXIT_OK: same reasoning as cmdVerifyArtifacts above — an empty key_links block is
442
- // the shipped template default, and the parseMustHavesBlock indent mismatch means
443
- // 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.
444
512
  output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
445
513
  return;
446
514
  }
@@ -481,10 +549,19 @@ function reconcilePhase(cwd, phaseNum) {
481
549
  const sm = verRaw.match(/^status:\s*([A-Za-z_-]+)/m);
482
550
  const status = sm ? sm[1].toLowerCase() : 'unknown';
483
551
  const claimsPass = /^(pass|passed|verified|complete|verified_pass)$/.test(status);
484
- const planFile = files.find(f => isPlanFile(f));
485
- const planContent = planFile ? (safeReadFile(path.join(dir, planFile)) || '') : '';
486
- const artifacts = checkArtifacts(cwd, planContent);
487
- 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;
488
565
  const signals = artifacts.total + keyLinks.total;
489
566
  const contradictions = [];
490
567
  if (claimsPass) {
@@ -856,6 +933,15 @@ function checkConfigFile(cwd, addIssue, repairs) {
856
933
  if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
857
934
  addIssue('warning', 'W004', `${CONFIG_FILE}: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
858
935
  }
936
+ // I006 (market-ideas M26): keys PAN does not read. Info, not a warning: the key
937
+ // may be the user's own note or meant for a newer PAN, but a typo must not pass
938
+ // as a setting that took effect.
939
+ const unknown = unknownConfigKeys(parsed);
940
+ if (unknown.length) {
941
+ const hints = unknown.map((k) => { const n = nearestConfigKey(k); return n ? `${k} → ${n}?` : k; });
942
+ addIssue('info', 'I006', `${CONFIG_FILE}: PAN does not read ${unknown.length === 1 ? 'this key' : 'these keys'}: ${unknown.join(', ')}`,
943
+ `Check the spelling (${hints.join('; ')}), or remove the key. pan-tools config-set names an unknown key when you set it.`);
944
+ }
859
945
  } catch (err) {
860
946
  // JSON parse failed -- config is corrupt and should be reset
861
947
  addIssue('error', 'E005', `${CONFIG_FILE}: JSON parse error - ${err.message}`, 'Run /pan:health --repair to reset to defaults', true);
@@ -1158,11 +1244,10 @@ function syncRequirementCheckboxes(cwd) {
1158
1244
  // For each completed phase, find linked requirement IDs and check their boxes
1159
1245
  let fixed = 0;
1160
1246
  for (const phaseNum of completedPhases) {
1161
- const reqMatch = roadmapContent.match(
1162
- new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
1163
- );
1164
- if (!reqMatch) continue;
1165
- 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);
1166
1251
  for (const reqId of reqIds) {
1167
1252
  const escaped = escapeRegex(reqId);
1168
1253
  const re = new RegExp(`(- \\[) (\\]\\s*\\*\\*${escaped}\\*\\*)`, 'gi');
@@ -1383,7 +1468,6 @@ function cmdValidateHealth(cwd, options, raw) {
1383
1468
  // Check 10 (optional): full validation — run tests and build
1384
1469
  let testStatus;
1385
1470
  let buildStatus;
1386
- let memoryBudget;
1387
1471
  if (options.full) {
1388
1472
  testStatus = runFullTestCheck(cwd);
1389
1473
  buildStatus = runFullBuildCheck(cwd);
@@ -1393,14 +1477,9 @@ function cmdValidateHealth(cwd, options, raw) {
1393
1477
  if (buildStatus.pass === false) {
1394
1478
  addIssue('error', 'BUILD_FAIL', `Build failed (exit code ${buildStatus.exitCode})`, 'Fix build errors');
1395
1479
  }
1396
- // Memory-load budget (ADR-0036 acceptance signal): keep per-agent memory
1397
- // injection bounded as logs grow. Read-only, non-blocking.
1398
- memoryBudget = require('./memory.cjs').memoryLoadBudget(cwd);
1399
- if (memoryBudget.status === 'critical') {
1400
- addIssue('warning', 'MEM_BUDGET', memoryBudget.advisory, "Run 'pan-tools memory compact <agent>' or scope injection with 'memory select'");
1401
- } else if (memoryBudget.status === 'warning') {
1402
- addIssue('info', 'MEM_BUDGET', memoryBudget.advisory, "Run 'pan-tools memory compact <agent>' or scope injection with 'memory select'");
1403
- }
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.
1404
1483
  }
1405
1484
 
1406
1485
  // Check 11 (optional): drift analysis
@@ -1461,7 +1540,6 @@ function cmdValidateHealth(cwd, options, raw) {
1461
1540
  if (options.full) {
1462
1541
  result.test_status = testStatus;
1463
1542
  result.build_status = buildStatus;
1464
- result.memory_budget = memoryBudget;
1465
1543
  }
1466
1544
  if (options.drift) {
1467
1545
  result.drift_status = driftResult;
@@ -1471,8 +1549,10 @@ function cmdValidateHealth(cwd, options, raw) {
1471
1549
  }
1472
1550
 
1473
1551
  // Explicit verdict exit: `broken` → 1; `degraded` and `healthy` → 0 (warnings are
1474
- // not failures). Computed AFTER --repair ran, so the code reflects the post-repair
1475
- // 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.
1476
1556
  output(result, raw, undefined, status === HEALTH_STATUS.BROKEN ? 1 : 0);
1477
1557
  }
1478
1558
 
@@ -1558,6 +1638,8 @@ module.exports = {
1558
1638
  cmdVerifyStubs,
1559
1639
  scopePhase,
1560
1640
  cmdVerifyScope,
1641
+ verificationFreshness,
1642
+ cmdVerifyStale,
1561
1643
  cmdValidateConsistency,
1562
1644
  cmdValidateHealth,
1563
1645
  cmdPreflight,
@@ -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).
@@ -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');
@@ -333,7 +362,7 @@ async function main() {
333
362
  + ' --planning-dir <path> act on an arbitrary project-relative planning tree\n'
334
363
  + ' --all-tracks (hygiene) act on the root tree AND every discovered track\n'
335
364
  + ' 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';
365
+ + '\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
366
 
338
367
  if (!command) {
339
368
  error(USAGE);
@@ -538,9 +567,16 @@ async function main() {
538
567
  const force = args.includes('--force');
539
568
  const failOnError = args.includes('--fail-on-error');
540
569
  const message = args[1] && !args[1].startsWith('--') ? args[1] : null;
541
- // Parse --files flag (collect args after --files, stopping at other flags)
570
+ // Parse --files flag: collect args after --files, stopping at the next flag. A
571
+ // filter here took every later bare argument, so `--files f --type docs` staged `docs`.
542
572
  const filesIndex = args.indexOf('--files');
543
- const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
573
+ const files = [];
574
+ if (filesIndex !== -1) {
575
+ for (const a of args.slice(filesIndex + 1)) {
576
+ if (a.startsWith('--')) break;
577
+ files.push(a);
578
+ }
579
+ }
544
580
  const commitType = getArgValue(args, '--type');
545
581
  commands.cmdCommit(cwd, message, files, raw, amend, { type: commitType, force, failOnError });
546
582
  break;
@@ -611,12 +647,19 @@ async function main() {
611
647
  verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
612
648
  } else if (subcommand === 'scope') {
613
649
  verify.cmdVerifyScope(cwd, args[2], raw);
650
+ } else if (subcommand === 'stale') {
651
+ verify.cmdVerifyStale(cwd, args[2], raw);
614
652
  } else {
615
- error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope');
653
+ error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale');
616
654
  }
617
655
  break;
618
656
  }
619
657
 
658
+ case 'version': {
659
+ commands.cmdVersion(cwd, { check: args.includes('--check') }, raw);
660
+ break;
661
+ }
662
+
620
663
  case 'generate-slug': {
621
664
  commands.cmdGenerateSlug(args[1], raw);
622
665
  break;
@@ -682,8 +725,16 @@ async function main() {
682
725
  } else if (subcommand === 'update-plan-progress') {
683
726
  if (!args[2]) error('roadmap update-plan-progress requires a phase number');
684
727
  roadmap.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw);
728
+ } else if (subcommand === 'slice') {
729
+ if (!args[2]) error('roadmap slice requires a phase number');
730
+ roadmap.cmdRoadmapSlice(cwd, args[2], { write: args.includes('--write') }, raw);
731
+ } else if (subcommand === 'compact') {
732
+ const keepIdx = args.indexOf('--keep');
733
+ const keep = keepIdx !== -1 ? Number(args[keepIdx + 1]) : undefined;
734
+ if (keepIdx !== -1 && !(Number.isInteger(keep) && keep >= 0)) error('roadmap compact --keep needs a whole number of phases (0 or more)');
735
+ require('./lib/roadmap-compact.cjs').cmdRoadmapCompact(cwd, { apply: args.includes('--apply'), keep }, raw);
685
736
  } else {
686
- error('Unknown roadmap subcommand. Available: get-phase, analyze, update-plan-progress');
737
+ error('Unknown roadmap subcommand. Available: get-phase, analyze, update-plan-progress, slice, compact');
687
738
  }
688
739
  break;
689
740
  }
@@ -844,7 +895,8 @@ async function main() {
844
895
  }
845
896
 
846
897
  case 'phase-plan-index': {
847
- phase.cmdPhasePlanIndex(cwd, args[1], raw);
898
+ const failedIdx = args.indexOf('--failed');
899
+ phase.cmdPhasePlanIndex(cwd, args[1], raw, { failed: failedIdx !== -1 ? args[failedIdx + 1] : null });
848
900
  break;
849
901
  }
850
902
 
@@ -1030,7 +1082,12 @@ async function main() {
1030
1082
  if (subcommand === 'read') {
1031
1083
  memory.cmdMemoryRead(cwd, args[2], raw);
1032
1084
  } else if (subcommand === 'append') {
1033
- memory.cmdMemoryAppend(cwd, args[2], args.slice(3).join(' '), raw);
1085
+ // `--cites <path[#symbol]>,...` is taken out before the entry words are joined.
1086
+ const rest = args.slice(3);
1087
+ const ci = rest.indexOf('--cites');
1088
+ const cites = ci === -1 ? undefined : rest[ci + 1];
1089
+ if (ci !== -1) rest.splice(ci, 2);
1090
+ memory.cmdMemoryAppend(cwd, args[2], rest.join(' '), raw, { cites });
1034
1091
  } else if (subcommand === 'list') {
1035
1092
  memory.cmdMemoryList(cwd, raw);
1036
1093
  } else if (subcommand === 'compact') {
@@ -1040,7 +1097,22 @@ async function main() {
1040
1097
  cue: getArgValue(args, '--cue'),
1041
1098
  tokenBudget: getArgValue(args, '--token-budget'),
1042
1099
  recencyFloor: getArgValue(args, '--recency-floor'),
1100
+ all: args.includes('--all'),
1101
+ markUsed: args.includes('--mark-used'),
1102
+ expireDays: getArgValue(args, '--days'),
1103
+ }, raw);
1104
+ } else if (subcommand === 'record') {
1105
+ memory.cmdMemoryRecord(cwd, args[2] && !args[2].startsWith('--') ? args[2] : null, {
1106
+ lesson: getArgValue(args, '--lesson'),
1107
+ finding: getArgValue(args, '--finding'),
1108
+ trace: getArgValue(args, '--trace'),
1109
+ cites: getArgValue(args, '--cites'),
1043
1110
  }, raw);
1111
+ } else if (subcommand === 'prune') {
1112
+ const daysArg = getArgValue(args, '--days');
1113
+ if (daysArg !== null && !/^\d+$/.test(daysArg)) error('memory prune --days needs a whole number of days (0 turns expiry off)');
1114
+ const target = args[2] && !args[2].startsWith('--') ? args[2] : null;
1115
+ memory.cmdMemoryPrune(cwd, target, { apply: args.includes('--apply'), days: daysArg === null ? undefined : Number(daysArg) }, raw);
1044
1116
  } else if (subcommand === 'budget') {
1045
1117
  memory.cmdMemoryBudget(cwd, raw);
1046
1118
  } else if (subcommand === 'optimize') {
@@ -1054,7 +1126,7 @@ async function main() {
1054
1126
  apply: args.includes('--apply'),
1055
1127
  }, raw);
1056
1128
  } else {
1057
- error('Unknown memory subcommand. Available: read, append, list, compact, select, budget, optimize, rebuild');
1129
+ error('Unknown memory subcommand. Available: read, append, list, compact, select, record, prune, budget, optimize, rebuild');
1058
1130
  }
1059
1131
  break;
1060
1132
  }
@@ -1109,7 +1181,14 @@ async function main() {
1109
1181
  case 'knowledge': {
1110
1182
  const subcommand = args[1];
1111
1183
  if (subcommand === 'ask') {
1112
- const question = args.slice(2).filter(a => !a.startsWith('--')).join(' ');
1184
+ // The question is every word after `ask` that is neither a flag nor a flag's
1185
+ // value: `--recall-cue "session tokens" --max-sources 3` used to join it.
1186
+ const words = [];
1187
+ for (let i = 2; i < args.length; i++) {
1188
+ if (args[i] === '--max-sources' || args[i] === '--recall-cue') { i++; continue; }
1189
+ if (!args[i].startsWith('--')) words.push(args[i]);
1190
+ }
1191
+ const question = words.join(' ');
1113
1192
  const maxSources = getArgValue(args, '--max-sources');
1114
1193
  knowledge.cmdKnowledgeAsk(cwd, question, {
1115
1194
  max_sources: maxSources ? Number(maxSources) : undefined,
@@ -1241,7 +1320,8 @@ async function main() {
1241
1320
 
1242
1321
  case 'cost': {
1243
1322
  const subcommand = args[1];
1244
- if (subcommand === 'report' || !subcommand) {
1323
+ // `report` is the default, so a leading flag (`cost --format chart`) means report, as for `links`.
1324
+ if (subcommand === 'report' || !subcommand || subcommand.startsWith('--')) {
1245
1325
  const format = getArgValue(args, '--format', 'json');
1246
1326
  const since = getArgValue(args, '--since');
1247
1327
  const until = getArgValue(args, '--until');
@@ -1268,8 +1348,10 @@ async function main() {
1268
1348
  mainThread: !args.includes('--no-main-thread'),
1269
1349
  claudeDir: getArgValue(args, '--claude-dir'),
1270
1350
  }, raw);
1351
+ } else if (subcommand === 'limits') {
1352
+ cost.cmdCostLimits(cwd, raw);
1271
1353
  } else {
1272
- error('Unknown cost subcommand. Available: report, append, clear, rebuild');
1354
+ error('Unknown cost subcommand. Available: report, append, clear, rebuild, limits');
1273
1355
  }
1274
1356
  break;
1275
1357
  }
@@ -1322,7 +1404,7 @@ async function main() {
1322
1404
  campaign.cmdCampaignSchedule(cwd, {
1323
1405
  goal: getArgValue(args, '--goal'),
1324
1406
  source: getArgValue(args, '--source'),
1325
- cadence: getArgValue(args, '--cadence', 'daily'),
1407
+ cadence: getArgValue(args, '--cadence'),
1326
1408
  daily_budget: budget != null ? Number(budget) : undefined,
1327
1409
  enabled: args.includes('--disable') ? false : undefined,
1328
1410
  paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
@@ -1531,9 +1613,11 @@ async function main() {
1531
1613
  ? sourceExpsCsv.split(',').map(s => s.trim()).filter(Boolean)
1532
1614
  : [];
1533
1615
  const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1616
+ const citesCsv = getArgValue(args, '--cites') || '';
1617
+ const cites = citesCsv ? citesCsv.split(',').map(s => s.trim()).filter(Boolean) : [];
1534
1618
 
1535
1619
  const result = optimize.promotePattern(
1536
- { id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments },
1620
+ { id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments, ...(cites.length ? { cites } : {}) },
1537
1621
  { scope, topic, sourceRoot }
1538
1622
  );
1539
1623
  output(result, raw);
@@ -1581,9 +1665,13 @@ async function main() {
1581
1665
  if (!agent) { error('learn topics-for requires --agent <name>'); }
1582
1666
  const minRelevance = getArgValue(args, '--min-relevance', 'medium');
1583
1667
  const tokenBudget = parseInt(getArgValue(args, '--token-budget', '5000'), 10);
1584
- const result = learnIndex.cmdTopicsFor(sourceRoot, { agent, minRelevance, tokenBudget });
1668
+ const cue = getArgValue(args, '--cue') || undefined;
1669
+ const result = learnIndex.cmdTopicsFor(sourceRoot, { agent, minRelevance, tokenBudget, cue });
1585
1670
  if (raw) {
1586
- const lines = [`Topics for "${agent}" (min ${minRelevance}, budget ${tokenBudget}):`, ``];
1671
+ const head = result.mode === 'cue'
1672
+ ? `Topics for "${agent}" matching the cue (budget ${tokenBudget}):`
1673
+ : `Topics for "${agent}" (min ${minRelevance}, budget ${tokenBudget})${cue ? ' — nothing matched the cue, so by role' : ''}:`;
1674
+ const lines = [head, ``];
1587
1675
  for (const t of result.selected) {
1588
1676
  lines.push(` [${t.relevance.padEnd(6)}] ${t.scope}/${t.name.padEnd(22)} ${t.tokens.toString().padStart(5)}t ${t.patterns.join(', ')}`);
1589
1677
  }
@@ -1638,18 +1726,24 @@ async function main() {
1638
1726
  // and to the suggestion index that is now parsed from those strings.
1639
1727
  // Publishing it fixes the error AND feeds the suggester, with no second
1640
1728
  // list to maintain.
1641
- if (subcommand) {
1729
+ //
1730
+ // A flag is not a subcommand: `learn --session <id>` and `learn --sessions N`
1731
+ // take the alias with `optimize learn`'s flags (the guard used to refuse both).
1732
+ if (subcommand && !subcommand.startsWith('--')) {
1642
1733
  error('Unknown learn subcommand. Available: promote, unpromote, list-promoted, build-index, topics-for, lint');
1643
1734
  }
1644
1735
  optimize.cmdOptimizeLearn(cwd, {
1645
1736
  sessionId: getArgValue(args, '--session'),
1737
+ sessions: getArgValue(args, '--sessions'),
1646
1738
  }, raw);
1647
1739
  break;
1648
1740
  }
1649
1741
 
1650
1742
  case 'links': {
1651
1743
  const subcommand = args[1];
1652
- if (subcommand === 'validate' || !subcommand) {
1744
+ // `validate` is the default, flags and all: `links --strict` used to be refused
1745
+ // as an unknown subcommand while bare `links` validated.
1746
+ if (subcommand === 'validate' || !subcommand || subcommand.startsWith('--')) {
1653
1747
  const collectMulti = (flag) => {
1654
1748
  const vals = [];
1655
1749
  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
  }
@@ -50,6 +50,15 @@ const META_SERVER_INFO_KEY = 'io.modelcontextprotocol/serverInfo';
50
50
  // claim to speak a version we don't. Newest first (the `server/discover` order).
51
51
  const SUPPORTED_VERSIONS_LIST = [MODERN_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05'];
52
52
  const SUPPORTED_PROTOCOL_VERSIONS = new Set(SUPPORTED_VERSIONS_LIST);
53
+ // The `CacheableResult` fields a modern result MUST carry where its type extends
54
+ // that interface: DiscoverResult, ListToolsResult, ListResourcesResult and
55
+ // ReadResourceResult (schema/2026-07-28/schema.ts). Claude Code 2.1.288 drops a
56
+ // list without them, so the server showed as connected with no tools and no
57
+ // resources (live check H2, 2026-10-03). The registry is fixed for the life of the
58
+ // process, so its lists are public and long-lived; a resource is the project's
59
+ // state at the moment it is read, so a read is private and stale at once.
60
+ const REGISTRY_CACHE = Object.freeze({ ttlMs: 3600000, cacheScope: 'public' });
61
+ const READ_CACHE = Object.freeze({ ttlMs: 0, cacheScope: 'private' });
53
62
  /**
54
63
  * The version the server reports in `initialize` / `server/discover`. Read from the
55
64
  * package.json two levels up: the repository root in the source tree, the runtime
@@ -320,12 +329,13 @@ function createServer(opts = {}) {
320
329
  { supported: SUPPORTED_VERSIONS_LIST, requested: requestedVersion });
321
330
  }
322
331
 
323
- // Modern results MUST carry a `resultType`; legacy results MUST NOT change
324
- // shape (clients treat an absent resultType as "complete"). Stamp it only on
332
+ // Modern results MUST carry a `resultType`, a cacheable one its caching fields,
333
+ // and each SHOULD name the server in `_meta`; legacy results MUST NOT change
334
+ // shape (clients treat an absent resultType as "complete"). Stamp them only on
325
335
  // the modern path.
326
- const reply = (result) => rpcResult(id,
336
+ const reply = (result, cache) => rpcResult(id,
327
337
  isModern && result && typeof result === 'object' && result.resultType === undefined
328
- ? { resultType: 'complete', ...result }
338
+ ? { resultType: 'complete', ...cache, ...result, _meta: { ...result._meta, [META_SERVER_INFO_KEY]: SERVER_INFO } }
329
339
  : result);
330
340
 
331
341
  switch (method) {
@@ -339,8 +349,7 @@ function createServer(opts = {}) {
339
349
  supportedVersions: SUPPORTED_VERSIONS_LIST,
340
350
  capabilities: { tools: {}, resources: {} },
341
351
  instructions: 'PAN Wizard engine bridge: planning, verification, and orchestration tools backed by the pan-tools CLI. All tools are read-only except the gated pan_confirm_merge.',
342
- ttlMs: 3600000,
343
- cacheScope: 'public',
352
+ ...REGISTRY_CACHE,
344
353
  _meta: { [META_SERVER_INFO_KEY]: SERVER_INFO },
345
354
  });
346
355
  case 'initialize': {
@@ -354,9 +363,9 @@ function createServer(opts = {}) {
354
363
  case 'ping':
355
364
  return reply({});
356
365
  case 'tools/list':
357
- return reply({ tools: reg.TOOLS.map(toMcpTool) });
366
+ return reply({ tools: reg.TOOLS.map(toMcpTool) }, REGISTRY_CACHE);
358
367
  case 'resources/list':
359
- return reply({ resources: reg.RESOURCES.map(toMcpResource) });
368
+ return reply({ resources: reg.RESOURCES.map(toMcpResource) }, REGISTRY_CACHE);
360
369
  case 'tools/call': {
361
370
  const out = callTool(params && params.name, params && params.arguments);
362
371
  return out.error ? rpcError(id, out.error.code, out.error.message) : reply(out.result);
@@ -364,7 +373,7 @@ function createServer(opts = {}) {
364
373
  case 'resources/read': {
365
374
  const out = readResource(params && params.uri);
366
375
  if (out.unknown) return rpcError(id, -32602, `Unknown resource: ${params && params.uri}`);
367
- return out.error ? rpcError(id, out.error.code, out.error.message) : reply(out.result);
376
+ return out.error ? rpcError(id, out.error.code, out.error.message) : reply(out.result, READ_CACHE);
368
377
  }
369
378
  default:
370
379
  return rpcError(id, -32601, `Method not found: ${method}`);