pan-wizard 3.33.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 (197) hide show
  1. package/README.md +23 -20
  2. package/agents/pan-conductor.md +17 -16
  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 +14 -14
  9. package/agents/pan-executor.md +12 -8
  10. package/agents/pan-experiment-runner.md +8 -7
  11. package/agents/pan-integration-checker.md +2 -2
  12. package/agents/pan-knowledge.md +1 -1
  13. package/agents/pan-meta-reviewer.md +3 -3
  14. package/agents/pan-optimizer.md +39 -43
  15. package/agents/pan-phase-researcher.md +8 -8
  16. package/agents/pan-plan-checker.md +30 -28
  17. package/agents/pan-planner.md +54 -43
  18. package/agents/pan-previewer.md +1 -1
  19. package/agents/pan-project-researcher.md +11 -11
  20. package/agents/pan-release.md +1 -1
  21. package/agents/pan-research-synthesizer.md +2 -2
  22. package/agents/pan-reviewer.md +24 -2
  23. package/agents/pan-roadmapper.md +15 -14
  24. package/agents/pan-verifier.md +32 -24
  25. package/bin/install-lib.cjs +58 -23
  26. package/bin/install.js +47 -1
  27. package/commands/pan/army.md +15 -11
  28. package/commands/pan/assumptions.md +2 -2
  29. package/commands/pan/audit-deployment.md +11 -12
  30. package/commands/pan/cost.md +8 -4
  31. package/commands/pan/debug.md +10 -7
  32. package/commands/pan/design-phase.md +3 -3
  33. package/commands/pan/discuss-phase.md +2 -0
  34. package/commands/pan/exec-phase.md +21 -37
  35. package/commands/pan/experiment.md +9 -8
  36. package/commands/pan/focus-auto.md +22 -20
  37. package/commands/pan/focus-design.md +7 -6
  38. package/commands/pan/focus-doc-audit.md +4 -3
  39. package/commands/pan/focus-drift-walking.md +4 -3
  40. package/commands/pan/focus-exec.md +6 -6
  41. package/commands/pan/focus-plan.md +12 -11
  42. package/commands/pan/focus-scan.md +5 -3
  43. package/commands/pan/focus-sync.md +3 -2
  44. package/commands/pan/git.md +8 -8
  45. package/commands/pan/health.md +2 -2
  46. package/commands/pan/hud.md +3 -2
  47. package/commands/pan/hygiene.md +14 -7
  48. package/commands/pan/knowledge.md +9 -9
  49. package/commands/pan/learn.md +10 -7
  50. package/commands/pan/links.md +4 -1
  51. package/commands/pan/map-codebase.md +1 -1
  52. package/commands/pan/mcp-bridge.md +18 -18
  53. package/commands/pan/milestone-done.md +6 -3
  54. package/commands/pan/milestone-new.md +1 -1
  55. package/commands/pan/new-project.md +10 -10
  56. package/commands/pan/optimize.md +16 -14
  57. package/commands/pan/patches.md +2 -6
  58. package/commands/pan/pause.md +1 -1
  59. package/commands/pan/phase-tests.md +2 -1
  60. package/commands/pan/plan-phase.md +9 -19
  61. package/commands/pan/preview.md +1 -1
  62. package/commands/pan/profile.md +2 -2
  63. package/commands/pan/quick.md +1 -1
  64. package/commands/pan/remove-phase.md +1 -1
  65. package/commands/pan/report.md +2 -2
  66. package/commands/pan/research-phase.md +12 -11
  67. package/commands/pan/resume.md +9 -11
  68. package/commands/pan/retro.md +5 -3
  69. package/commands/pan/review-deep.md +7 -7
  70. package/commands/pan/settings.md +1 -1
  71. package/commands/pan/verify-phase.md +0 -11
  72. package/commands/pan/what-if.md +2 -2
  73. package/hooks/dist/pan-context-monitor.js +343 -44
  74. package/hooks/dist/pan-cost-logger.js +30 -0
  75. package/hooks/dist/pan-statusline.js +68 -22
  76. package/hooks/dist/pan-stop-guard.js +7 -2
  77. package/package.json +1 -1
  78. package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
  79. package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
  80. package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
  81. package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
  82. package/pan-wizard-core/bin/lib/commands.cjs +33 -4
  83. package/pan-wizard-core/bin/lib/config.cjs +2 -1
  84. package/pan-wizard-core/bin/lib/constants.cjs +18 -6
  85. package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
  86. package/pan-wizard-core/bin/lib/core.cjs +53 -19
  87. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
  88. package/pan-wizard-core/bin/lib/cost.cjs +142 -13
  89. package/pan-wizard-core/bin/lib/distill.cjs +6 -3
  90. package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
  91. package/pan-wizard-core/bin/lib/findings.cjs +9 -2
  92. package/pan-wizard-core/bin/lib/focus.cjs +13 -4
  93. package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
  94. package/pan-wizard-core/bin/lib/hygiene.cjs +178 -17
  95. package/pan-wizard-core/bin/lib/init.cjs +7 -4
  96. package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
  97. package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
  98. package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
  99. package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
  100. package/pan-wizard-core/bin/lib/memory.cjs +508 -54
  101. package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
  102. package/pan-wizard-core/bin/lib/optimize.cjs +84 -16
  103. package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
  104. package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
  105. package/pan-wizard-core/bin/lib/phase.cjs +15 -13
  106. package/pan-wizard-core/bin/lib/preview.cjs +3 -1
  107. package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
  108. package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
  109. package/pan-wizard-core/bin/lib/runner.cjs +2 -1
  110. package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
  111. package/pan-wizard-core/bin/lib/state.cjs +43 -28
  112. package/pan-wizard-core/bin/lib/template.cjs +11 -4
  113. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
  114. package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
  115. package/pan-wizard-core/bin/lib/verify-scope.cjs +5 -2
  116. package/pan-wizard-core/bin/lib/verify.cjs +30 -28
  117. package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
  118. package/pan-wizard-core/bin/pan-tools.cjs +101 -15
  119. package/pan-wizard-core/learnings/index.json +5 -5
  120. package/pan-wizard-core/opencode/pan-wizard.js +111 -0
  121. package/pan-wizard-core/references/checkpoints.md +3 -3
  122. package/pan-wizard-core/references/continuation-format.md +3 -3
  123. package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
  124. package/pan-wizard-core/references/design-methodology.md +1 -1
  125. package/pan-wizard-core/references/git-integration.md +4 -6
  126. package/pan-wizard-core/references/git-planning-commit.md +2 -2
  127. package/pan-wizard-core/references/guardrails.md +2 -2
  128. package/pan-wizard-core/references/handoff-decisions.md +3 -3
  129. package/pan-wizard-core/references/model-profile-resolution.md +5 -5
  130. package/pan-wizard-core/references/model-profiles.md +14 -9
  131. package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
  132. package/pan-wizard-core/references/planning-config.md +4 -4
  133. package/pan-wizard-core/references/tdd.md +10 -1
  134. package/pan-wizard-core/references/verification-patterns.md +5 -5
  135. package/pan-wizard-core/templates/codebase/architecture.md +2 -2
  136. package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
  137. package/pan-wizard-core/templates/codebase/concerns.md +3 -3
  138. package/pan-wizard-core/templates/codebase/integrations.md +6 -6
  139. package/pan-wizard-core/templates/codebase/relationships.md +2 -2
  140. package/pan-wizard-core/templates/codebase/stack.md +2 -2
  141. package/pan-wizard-core/templates/codebase/structure.md +4 -4
  142. package/pan-wizard-core/templates/codebase/testing.md +4 -4
  143. package/pan-wizard-core/templates/continue-here.md +1 -1
  144. package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
  145. package/pan-wizard-core/templates/debug.md +15 -1
  146. package/pan-wizard-core/templates/milestone-archive.md +6 -6
  147. package/pan-wizard-core/templates/milestone.md +1 -1
  148. package/pan-wizard-core/templates/phase-prompt.md +29 -17
  149. package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
  150. package/pan-wizard-core/templates/research-project/architecture.md +3 -3
  151. package/pan-wizard-core/templates/research-project/features.md +1 -1
  152. package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
  153. package/pan-wizard-core/templates/research-project/stack.md +3 -3
  154. package/pan-wizard-core/templates/research.md +5 -5
  155. package/pan-wizard-core/templates/retrospective.md +1 -1
  156. package/pan-wizard-core/templates/roadmap.md +15 -15
  157. package/pan-wizard-core/templates/summary-complex.md +1 -0
  158. package/pan-wizard-core/templates/summary-minimal.md +1 -0
  159. package/pan-wizard-core/templates/summary-standard.md +2 -1
  160. package/pan-wizard-core/templates/summary.md +18 -2
  161. package/pan-wizard-core/templates/uat.md +3 -3
  162. package/pan-wizard-core/templates/user-setup.md +8 -8
  163. package/pan-wizard-core/templates/verification-report.md +53 -8
  164. package/pan-wizard-core/workflows/add-phase.md +1 -1
  165. package/pan-wizard-core/workflows/assumptions.md +1 -1
  166. package/pan-wizard-core/workflows/discuss-phase.md +9 -8
  167. package/pan-wizard-core/workflows/exec-phase.md +26 -57
  168. package/pan-wizard-core/workflows/execute-plan.md +8 -7
  169. package/pan-wizard-core/workflows/health.md +13 -12
  170. package/pan-wizard-core/workflows/help.md +8 -9
  171. package/pan-wizard-core/workflows/learn.md +13 -11
  172. package/pan-wizard-core/workflows/map-codebase.md +10 -10
  173. package/pan-wizard-core/workflows/milestone-audit.md +10 -10
  174. package/pan-wizard-core/workflows/milestone-gaps.md +17 -24
  175. package/pan-wizard-core/workflows/milestone-new.md +7 -6
  176. package/pan-wizard-core/workflows/new-project.md +9 -9
  177. package/pan-wizard-core/workflows/optimize.md +11 -9
  178. package/pan-wizard-core/workflows/pause.md +6 -1
  179. package/pan-wizard-core/workflows/phase-tests.md +4 -4
  180. package/pan-wizard-core/workflows/plan-phase.md +31 -34
  181. package/pan-wizard-core/workflows/profile.md +2 -2
  182. package/pan-wizard-core/workflows/progress.md +5 -7
  183. package/pan-wizard-core/workflows/quick.md +5 -3
  184. package/pan-wizard-core/workflows/remove-phase.md +1 -1
  185. package/pan-wizard-core/workflows/research-phase.md +9 -5
  186. package/pan-wizard-core/workflows/resume-project.md +4 -6
  187. package/pan-wizard-core/workflows/retro.md +3 -1
  188. package/pan-wizard-core/workflows/settings.md +16 -24
  189. package/pan-wizard-core/workflows/todo-check.md +1 -1
  190. package/pan-wizard-core/workflows/transition.md +12 -12
  191. package/pan-wizard-core/workflows/update.md +25 -17
  192. package/pan-wizard-core/workflows/verify-phase.md +17 -15
  193. package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
  194. package/pan-zcode/README.md +7 -7
  195. package/pan-zcode/bin/install-zcode.js +1 -1
  196. package/scripts/build-plugin.js +3 -1
  197. package/scripts/generate-skills-docs.py +13 -4
@@ -294,22 +294,22 @@ function cmdStateRecordMetric(cwd, options, raw) {
294
294
  return;
295
295
  }
296
296
 
297
- // Find Performance Metrics section and its table
298
- const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
297
+ // The Performance Metrics section's first table: its rows only, then an optional
298
+ // "None yet" line. Taking the rest of the section as the table body put every row
299
+ // below the template's "Recent Trend" and footer lines, outside the table. The
300
+ // separator is matched on one line: `[-|\s]+` ran on into a `| - | - |` row.
301
+ const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|: \t]+\r?\n)((?:\|[^\n]*(?:\n|$))*)((?:[^\S\n]*None yet\.?[^\S\n]*(?:\n|$))?)/i;
299
302
  const metricsMatch = content.match(metricsPattern);
300
303
 
301
304
  if (metricsMatch) {
302
- let tableBody = metricsMatch[2].trimEnd();
303
305
  const cell = escapeTableCell;
304
306
  const newRow = `| Phase ${cell(phase)} P${cell(plan)} | ${cell(duration)} | ${cell(tasks || '-')} tasks | ${cell(files || '-')} files |`;
307
+ // A template placeholder row (`| - | - | - | - |`, `| None yet | | | |`) gives way.
308
+ const isPlaceholder = (row) => /None yet/i.test(row) || row.split('|').slice(1, -1).every((c) => /^\s*-?\s*$/.test(c));
309
+ const rows = metricsMatch[2].split('\n').map((r) => r.replace(/\r$/, '')).filter((r) => r.trim() && !isPlaceholder(r));
310
+ rows.push(newRow);
305
311
 
306
- if (tableBody.trim() === '' || tableBody.includes('None yet')) {
307
- tableBody = newRow;
308
- } else {
309
- tableBody = tableBody + '\n' + newRow;
310
- }
311
-
312
- content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
312
+ content = content.replace(metricsPattern, (_match, header) => `${header}${rows.join('\n')}\n`);
313
313
  writeStateMd(statePath, content, cwd);
314
314
  output({ recorded: true, phase, plan, duration }, raw, 'true');
315
315
  } else {
@@ -432,7 +432,8 @@ function cmdStateAddBlocker(cwd, text, raw) {
432
432
 
433
433
  if (match) {
434
434
  let sectionBody = match[2];
435
- sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
435
+ // "None yet" first: stripping "None" first left a stray "yet." on the template.
436
+ sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/None\.?\s*\n?/gi, '');
436
437
  sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
437
438
  content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
438
439
  writeStateMd(statePath, content, cwd);
@@ -538,14 +539,23 @@ function cmdStateRecordSession(cwd, options, raw) {
538
539
  // --- Snapshot Parsers --------------------------------------------------------
539
540
 
540
541
  /**
541
- * Parse the Decisions Made table from state.md content.
542
- * Extracts rows from a markdown table under the "## Decisions Made" heading,
543
- * splitting each row into phase, summary, and rationale cells.
542
+ * Parse the decisions from state.md content: the `- [Phase N]: summary — rationale`
543
+ * bullets `state add-decision` appends under `### Decisions` (the template's
544
+ * heading), or else an older "## Decisions Made" table. Reading only the table
545
+ * left `decisions` empty on every state.md PAN itself writes.
544
546
  * @param {string} content - Full state.md content
545
547
  * @returns {Array<{phase: string, summary: string, rationale: string}>}
546
548
  */
547
549
  function parseDecisionsFromState(content) {
548
550
  const decisions = [];
551
+ const section = content.match(/###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
552
+ for (const line of section ? section[1].split('\n') : []) {
553
+ const m = line.match(/^-\s+\[Phase\s+([^\]]*)\]:\s*(.+?)\s*$/i);
554
+ if (!m) continue;
555
+ const [summary, ...rest] = m[2].split(' — ');
556
+ decisions.push({ phase: m[1].trim(), summary: summary.trim(), rationale: rest.join(' — ').trim() });
557
+ }
558
+ if (decisions.length > 0) return decisions;
549
559
  // Match the decisions table body after header row and separator row
550
560
  const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
551
561
  if (decisionsMatch) {
@@ -567,14 +577,15 @@ function parseDecisionsFromState(content) {
567
577
 
568
578
  /**
569
579
  * Parse the Blockers section from state.md content.
570
- * Extracts bullet-point items (lines starting with "- ") from under
571
- * the "## Blockers" heading.
580
+ * Extracts bullet-point items (lines starting with "- ") from under the heading
581
+ * `state add-blocker` writes to: `### Blockers/Concerns` in the template, or
582
+ * `Blockers` / `Concerns`. Matching only "## Blockers" left the list empty.
572
583
  * @param {string} content - Full state.md content
573
584
  * @returns {string[]} Array of blocker text strings
574
585
  */
575
586
  function parseBlockersFromState(content) {
576
587
  const blockers = [];
577
- const blockersMatch = content.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
588
+ const blockersMatch = content.match(/###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
578
589
  if (blockersMatch) {
579
590
  const blockersSection = blockersMatch[1];
580
591
  const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
@@ -585,6 +596,18 @@ function parseBlockersFromState(content) {
585
596
  return blockers;
586
597
  }
587
598
 
599
+ /**
600
+ * The blockers that are still open: the section's bullets minus a "None" placeholder.
601
+ * `preflight` and `dashboard` read a `## Blockers` heading of their own, which the
602
+ * state template never writes (it writes `### Blockers/Concerns`, where
603
+ * `state add-blocker` puts its bullets), so both reported no blockers with one open.
604
+ * @param {string} content - Full state.md content
605
+ * @returns {string[]}
606
+ */
607
+ function activeBlockersFromState(content) {
608
+ return parseBlockersFromState(content).filter((b) => b && !/^none(\s+yet)?\.?$/i.test(b));
609
+ }
610
+
588
611
  /**
589
612
  * Parse the Session section from state.md content.
590
613
  * Extracts **Last Date:**, **Stopped At:**, and **Resume File:** bold-field
@@ -1029,17 +1052,8 @@ function cmdDashboard(cwd, raw) {
1029
1052
  lastActivity = stateExtractField(stateContent, 'Last Activity');
1030
1053
  lastActivityDesc = stateExtractField(stateContent, 'Last Activity Description');
1031
1054
 
1032
- // Parse blockers
1033
- const blockersMatch = stateContent.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
1034
- if (blockersMatch) {
1035
- const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
1036
- for (const item of items) {
1037
- const text = item.replace(/^-\s+/, '').trim();
1038
- if (text && !/^none$/i.test(text)) {
1039
- activeBlockers.push(text);
1040
- }
1041
- }
1042
- }
1055
+ // Parse blockers (the template's `### Blockers/Concerns`, or a legacy `## Blockers`)
1056
+ activeBlockers.push(...activeBlockersFromState(stateContent));
1043
1057
  blockerCount = activeBlockers.length;
1044
1058
  }
1045
1059
 
@@ -1111,6 +1125,7 @@ function cmdDashboard(cwd, raw) {
1111
1125
 
1112
1126
  module.exports = {
1113
1127
  readStateSafe: safeReadFile,
1128
+ activeBlockersFromState,
1114
1129
  stateExtractField,
1115
1130
  stateReplaceField,
1116
1131
  writeStateMd,
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const {
8
- PHASES_DIR, PLAN_SUFFIX, SUMMARY_SUFFIX, VERIFICATION_SUFFIX,
8
+ PHASES_DIR, PLAN_SUFFIX, SUMMARY_SUFFIX, VERIFICATION_SUFFIX, ROADMAP_SLICE_SUFFIX,
9
9
  SIMPLE_TASK_THRESHOLD, SIMPLE_FILE_THRESHOLD, COMPLEX_TASK_THRESHOLD, COMPLEX_FILE_THRESHOLD,
10
10
  } = require('./constants.cjs');
11
11
  const { planningPath, phasesPath } = require('./utils.cjs');
@@ -86,7 +86,7 @@ function cmdTemplateSelect(cwd, planPath, raw) {
86
86
  * @param {string} phaseName - Human-readable phase name
87
87
  * @param {string} phaseSlug - Phase slug for directory naming
88
88
  * @param {string} phaseNum - Raw phase number from options
89
- * @param {Object} options - Additional options (type, wave, fields)
89
+ * @param {Object} options - Additional options (type, wave, fields, slicePath)
90
90
  * @returns {{frontmatter: Object, body: string, fileName: string}}
91
91
  */
92
92
  function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, options) {
@@ -94,6 +94,10 @@ function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, o
94
94
  const wave = parseInt(options.wave, 10) || 1;
95
95
  const fields = options.fields || {};
96
96
  const padded = phaseId.split('-')[0]; // e.g., "01" from "01-setup-auth"
97
+ // The <context> names the phase's roadmap slice, as templates/phase-prompt.md
98
+ // does (O2): `@.planning/roadmap.md` handed every executor the whole roadmap.
99
+ // cmdTemplateFill passes the phase's real directory.
100
+ const slicePath = options.slicePath || `.planning/phases/${phaseId}/${padded}${ROADMAP_SLICE_SUFFIX}`;
97
101
 
98
102
  const frontmatter = {
99
103
  phase: phaseId,
@@ -121,7 +125,7 @@ function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, o
121
125
  '',
122
126
  '## Context',
123
127
  '@.planning/project.md',
124
- '@.planning/roadmap.md',
128
+ `@${slicePath}`,
125
129
  '@.planning/state.md',
126
130
  '',
127
131
  '## Tasks',
@@ -286,7 +290,10 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
286
290
  generated = generateSummaryTemplate(phaseId, planNum, phaseName, options.phase, fields);
287
291
  break;
288
292
  case 'plan':
289
- generated = generatePlanTemplate(phaseId, planNum, phaseName, phaseSlug, options.phase, options);
293
+ generated = generatePlanTemplate(phaseId, planNum, phaseName, phaseSlug, options.phase, {
294
+ ...options,
295
+ slicePath: toPosix(path.join(phaseInfo.directory, `${padded}${ROADMAP_SLICE_SUFFIX}`)),
296
+ });
290
297
  break;
291
298
  case 'verification':
292
299
  generated = generateVerificationTemplate(phaseId, phaseName, options.phase, fields);
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { safeReadFile, execGit, findPhaseInternal, output, normalizePhaseName } = require('./core.cjs');
10
- const { readStateSafe } = require('./state.cjs');
10
+ const { readStateSafe, activeBlockersFromState } = require('./state.cjs');
11
11
  const {
12
12
  STATE_FILE, ROADMAP_FILE, CONFIG_FILE, PATTERNS_FILE, PHASE_DIR_RE,
13
13
  } = require('./constants.cjs');
@@ -44,19 +44,10 @@ function cmdPreflight(cwd, target, raw) {
44
44
  blockers.push('state.md not found — run /pan:new-project');
45
45
  }
46
46
 
47
- // Check 3: no unresolved blockers in state.md
47
+ // Check 3: no unresolved blockers in state.md (the template's `### Blockers/Concerns`,
48
+ // where `state add-blocker` writes, or a legacy `## Blockers`)
48
49
  if (stateContent) {
49
- const blockersMatch = stateContent.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
50
- const activeBlockers = [];
51
- if (blockersMatch) {
52
- const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
53
- for (const item of items) {
54
- const text = item.replace(/^-\s+/, '').trim();
55
- if (text && !/^none$/i.test(text)) {
56
- activeBlockers.push(text);
57
- }
58
- }
59
- }
50
+ const activeBlockers = activeBlockersFromState(stateContent);
60
51
  if (activeBlockers.length === 0) {
61
52
  checks.push({ name: 'no_blockers', passed: true });
62
53
  } else {
@@ -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;
@@ -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).
@@ -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;
@@ -689,8 +725,16 @@ async function main() {
689
725
  } else if (subcommand === 'update-plan-progress') {
690
726
  if (!args[2]) error('roadmap update-plan-progress requires a phase number');
691
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);
692
736
  } else {
693
- 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');
694
738
  }
695
739
  break;
696
740
  }
@@ -1038,7 +1082,12 @@ async function main() {
1038
1082
  if (subcommand === 'read') {
1039
1083
  memory.cmdMemoryRead(cwd, args[2], raw);
1040
1084
  } else if (subcommand === 'append') {
1041
- 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 });
1042
1091
  } else if (subcommand === 'list') {
1043
1092
  memory.cmdMemoryList(cwd, raw);
1044
1093
  } else if (subcommand === 'compact') {
@@ -1048,7 +1097,22 @@ async function main() {
1048
1097
  cue: getArgValue(args, '--cue'),
1049
1098
  tokenBudget: getArgValue(args, '--token-budget'),
1050
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'),
1051
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);
1052
1116
  } else if (subcommand === 'budget') {
1053
1117
  memory.cmdMemoryBudget(cwd, raw);
1054
1118
  } else if (subcommand === 'optimize') {
@@ -1062,7 +1126,7 @@ async function main() {
1062
1126
  apply: args.includes('--apply'),
1063
1127
  }, raw);
1064
1128
  } else {
1065
- 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');
1066
1130
  }
1067
1131
  break;
1068
1132
  }
@@ -1117,7 +1181,14 @@ async function main() {
1117
1181
  case 'knowledge': {
1118
1182
  const subcommand = args[1];
1119
1183
  if (subcommand === 'ask') {
1120
- 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(' ');
1121
1192
  const maxSources = getArgValue(args, '--max-sources');
1122
1193
  knowledge.cmdKnowledgeAsk(cwd, question, {
1123
1194
  max_sources: maxSources ? Number(maxSources) : undefined,
@@ -1249,7 +1320,8 @@ async function main() {
1249
1320
 
1250
1321
  case 'cost': {
1251
1322
  const subcommand = args[1];
1252
- 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('--')) {
1253
1325
  const format = getArgValue(args, '--format', 'json');
1254
1326
  const since = getArgValue(args, '--since');
1255
1327
  const until = getArgValue(args, '--until');
@@ -1276,8 +1348,10 @@ async function main() {
1276
1348
  mainThread: !args.includes('--no-main-thread'),
1277
1349
  claudeDir: getArgValue(args, '--claude-dir'),
1278
1350
  }, raw);
1351
+ } else if (subcommand === 'limits') {
1352
+ cost.cmdCostLimits(cwd, raw);
1279
1353
  } else {
1280
- error('Unknown cost subcommand. Available: report, append, clear, rebuild');
1354
+ error('Unknown cost subcommand. Available: report, append, clear, rebuild, limits');
1281
1355
  }
1282
1356
  break;
1283
1357
  }
@@ -1330,7 +1404,7 @@ async function main() {
1330
1404
  campaign.cmdCampaignSchedule(cwd, {
1331
1405
  goal: getArgValue(args, '--goal'),
1332
1406
  source: getArgValue(args, '--source'),
1333
- cadence: getArgValue(args, '--cadence', 'daily'),
1407
+ cadence: getArgValue(args, '--cadence'),
1334
1408
  daily_budget: budget != null ? Number(budget) : undefined,
1335
1409
  enabled: args.includes('--disable') ? false : undefined,
1336
1410
  paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
@@ -1539,9 +1613,11 @@ async function main() {
1539
1613
  ? sourceExpsCsv.split(',').map(s => s.trim()).filter(Boolean)
1540
1614
  : [];
1541
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) : [];
1542
1618
 
1543
1619
  const result = optimize.promotePattern(
1544
- { 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 } : {}) },
1545
1621
  { scope, topic, sourceRoot }
1546
1622
  );
1547
1623
  output(result, raw);
@@ -1589,9 +1665,13 @@ async function main() {
1589
1665
  if (!agent) { error('learn topics-for requires --agent <name>'); }
1590
1666
  const minRelevance = getArgValue(args, '--min-relevance', 'medium');
1591
1667
  const tokenBudget = parseInt(getArgValue(args, '--token-budget', '5000'), 10);
1592
- 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 });
1593
1670
  if (raw) {
1594
- 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, ``];
1595
1675
  for (const t of result.selected) {
1596
1676
  lines.push(` [${t.relevance.padEnd(6)}] ${t.scope}/${t.name.padEnd(22)} ${t.tokens.toString().padStart(5)}t ${t.patterns.join(', ')}`);
1597
1677
  }
@@ -1646,18 +1726,24 @@ async function main() {
1646
1726
  // and to the suggestion index that is now parsed from those strings.
1647
1727
  // Publishing it fixes the error AND feeds the suggester, with no second
1648
1728
  // list to maintain.
1649
- 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('--')) {
1650
1733
  error('Unknown learn subcommand. Available: promote, unpromote, list-promoted, build-index, topics-for, lint');
1651
1734
  }
1652
1735
  optimize.cmdOptimizeLearn(cwd, {
1653
1736
  sessionId: getArgValue(args, '--session'),
1737
+ sessions: getArgValue(args, '--sessions'),
1654
1738
  }, raw);
1655
1739
  break;
1656
1740
  }
1657
1741
 
1658
1742
  case 'links': {
1659
1743
  const subcommand = args[1];
1660
- 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('--')) {
1661
1747
  const collectMulti = (flag) => {
1662
1748
  const vals = [];
1663
1749
  for (let i = 0; i < args.length; i++) {