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
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { loadConfig, getMilestoneInfo, escapeRegex, safeReadFile, output, error } = require('./core.cjs');
7
+ const { loadConfig, getMilestoneInfo, escapeRegex, escapeTableCell, safeReadFile, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
9
9
  const { withFileLock, writeFileAtomic } = require('./lock.cjs');
10
10
  const {
@@ -294,21 +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
- const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
304
-
305
- if (tableBody.trim() === '' || tableBody.includes('None yet')) {
306
- tableBody = newRow;
307
- } else {
308
- tableBody = tableBody + '\n' + newRow;
309
- }
310
-
311
- content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
305
+ const cell = escapeTableCell;
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);
311
+
312
+ content = content.replace(metricsPattern, (_match, header) => `${header}${rows.join('\n')}\n`);
312
313
  writeStateMd(statePath, content, cwd);
313
314
  output({ recorded: true, phase, plan, duration }, raw, 'true');
314
315
  } else {
@@ -431,7 +432,8 @@ function cmdStateAddBlocker(cwd, text, raw) {
431
432
 
432
433
  if (match) {
433
434
  let sectionBody = match[2];
434
- 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, '');
435
437
  sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
436
438
  content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
437
439
  writeStateMd(statePath, content, cwd);
@@ -537,14 +539,23 @@ function cmdStateRecordSession(cwd, options, raw) {
537
539
  // --- Snapshot Parsers --------------------------------------------------------
538
540
 
539
541
  /**
540
- * Parse the Decisions Made table from state.md content.
541
- * Extracts rows from a markdown table under the "## Decisions Made" heading,
542
- * 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.
543
546
  * @param {string} content - Full state.md content
544
547
  * @returns {Array<{phase: string, summary: string, rationale: string}>}
545
548
  */
546
549
  function parseDecisionsFromState(content) {
547
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;
548
559
  // Match the decisions table body after header row and separator row
549
560
  const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
550
561
  if (decisionsMatch) {
@@ -566,14 +577,15 @@ function parseDecisionsFromState(content) {
566
577
 
567
578
  /**
568
579
  * Parse the Blockers section from state.md content.
569
- * Extracts bullet-point items (lines starting with "- ") from under
570
- * 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.
571
583
  * @param {string} content - Full state.md content
572
584
  * @returns {string[]} Array of blocker text strings
573
585
  */
574
586
  function parseBlockersFromState(content) {
575
587
  const blockers = [];
576
- 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);
577
589
  if (blockersMatch) {
578
590
  const blockersSection = blockersMatch[1];
579
591
  const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
@@ -584,6 +596,18 @@ function parseBlockersFromState(content) {
584
596
  return blockers;
585
597
  }
586
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
+
587
611
  /**
588
612
  * Parse the Session section from state.md content.
589
613
  * Extracts **Last Date:**, **Stopped At:**, and **Resume File:** bold-field
@@ -1028,17 +1052,8 @@ function cmdDashboard(cwd, raw) {
1028
1052
  lastActivity = stateExtractField(stateContent, 'Last Activity');
1029
1053
  lastActivityDesc = stateExtractField(stateContent, 'Last Activity Description');
1030
1054
 
1031
- // Parse blockers
1032
- const blockersMatch = stateContent.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
1033
- if (blockersMatch) {
1034
- const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
1035
- for (const item of items) {
1036
- const text = item.replace(/^-\s+/, '').trim();
1037
- if (text && !/^none$/i.test(text)) {
1038
- activeBlockers.push(text);
1039
- }
1040
- }
1041
- }
1055
+ // Parse blockers (the template's `### Blockers/Concerns`, or a legacy `## Blockers`)
1056
+ activeBlockers.push(...activeBlockersFromState(stateContent));
1042
1057
  blockerCount = activeBlockers.length;
1043
1058
  }
1044
1059
 
@@ -1110,6 +1125,7 @@ function cmdDashboard(cwd, raw) {
1110
1125
 
1111
1126
  module.exports = {
1112
1127
  readStateSafe: safeReadFile,
1128
+ activeBlockersFromState,
1113
1129
  stateExtractField,
1114
1130
  stateReplaceField,
1115
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);
@@ -40,7 +40,9 @@ const KNOWN_VERDICTS = Object.freeze({
40
40
  'pan-design-checker': Object.freeze({ PASS: 'pass', GAPS: 'fail' }),
41
41
  });
42
42
 
43
- const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40 });
43
+ const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80 });
44
+ // What the verifier's test gate can report (verify-phase.md, run_test_suite).
45
+ const TEST_GATE_VALUES = Object.freeze(['passed', 'failed', 'skipped']);
44
46
  const AGENT_RE = /^[a-z][a-z0-9-]{0,63}$/;
45
47
  const OPEN_FENCE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*pan-verdict[ \t]*$/;
46
48
  const CLOSE_FENCE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
@@ -195,7 +197,32 @@ function validateVerdict(obj, opts = {}) {
195
197
  findings.push({ class: cls, severity, where, summary });
196
198
  });
197
199
 
198
- return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings }, warnings };
200
+ const notChecked = normaliseNotChecked(obj.not_checked, warnings);
201
+ return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked }, warnings };
202
+ }
203
+
204
+ /**
205
+ * The checks a judge could not run, each `{check, reason}` (market-ideas M23).
206
+ * A verdict that carries them says what it did not see: a `pass` with tests that
207
+ * never ran is recorded as exactly that, never as a plain pass. Accepts bare
208
+ * strings as checks with no reason; drops entries with no check.
209
+ */
210
+ function normaliseNotChecked(value, warnings) {
211
+ if (value == null) return [];
212
+ if (!Array.isArray(value)) {
213
+ warnings.push('not_checked_not_an_array');
214
+ return [];
215
+ }
216
+ const out = [];
217
+ for (const entry of value.slice(0, LIMITS.not_checked)) {
218
+ const obj = typeof entry === 'string' ? { check: entry } : (entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null);
219
+ const check = obj ? truncate(cleanText(obj.check ?? obj.gate ?? ''), LIMITS.check) : '';
220
+ if (!check) continue;
221
+ const reason = obj.reason == null || cleanText(obj.reason) === '' ? null : truncate(cleanText(obj.reason), LIMITS.summary);
222
+ out.push({ check, reason });
223
+ }
224
+ if (value.length > LIMITS.not_checked) warnings.push(`not_checked_truncated: ${value.length} reported, ${LIMITS.not_checked} kept`);
225
+ return out;
199
226
  }
200
227
 
201
228
  // ─── verification.md frontmatter adapter ─────────────────────────────────────
@@ -354,7 +381,23 @@ function verdictFromVerificationFrontmatter(text, agent = 'pan-verifier') {
354
381
  const summary = what || (path ? `unrequested change to ${path}` : '');
355
382
  if (summary) findings.push({ class: 'unrequested', severity: 'low', where: path, summary });
356
383
  }
357
- return { contract: VERDICT_CONTRACT, agent, verdict: status, outcome, findings };
384
+ // The test gate (M23). `not_checked` lists what the verifier could not run; a
385
+ // `test_gate: skipped` with no tests entry there still counts as one, so a model
386
+ // that records the gate but forgets the list cannot turn it into a plain pass.
387
+ let notChecked = listOfMappings(yaml, 'not_checked').map((n) => ({ check: n.check || n.gate || '', reason: n.reason || null }));
388
+ const testGateRaw = frontmatterScalar(yaml, 'test_gate');
389
+ const testGate = testGateRaw ? String(testGateRaw).toLowerCase() : null;
390
+ const isTests = (n) => /test/i.test(String(n.check));
391
+ let contradicted = false;
392
+ if (testGate === 'skipped' && !notChecked.some(isTests)) {
393
+ notChecked.push({ check: 'tests', reason: frontmatterScalar(yaml, 'test_gate_reason') || 'the test gate was skipped' });
394
+ } else if ((testGate === 'passed' || testGate === 'failed') && notChecked.some(isTests)) {
395
+ // The gate ran, so a `tests` entry is the template's example copied over: the
396
+ // recorded gate is the measurement, and the entry goes.
397
+ notChecked = notChecked.filter((n) => !isTests(n));
398
+ contradicted = true;
399
+ }
400
+ return { contract: VERDICT_CONTRACT, agent, verdict: status, outcome, findings, not_checked: notChecked, test_gate: testGate, not_checked_contradicted: contradicted };
358
401
  }
359
402
 
360
403
  /**
@@ -383,7 +426,14 @@ function parseVerdictText(text, opts = {}) {
383
426
  return { ok: false, error: 'unknown_status', reason: `verification status "${adapted.verdict}" is not passed, gaps_found or human_needed` };
384
427
  }
385
428
  const r = validateVerdict(adapted, opts);
386
- return r.ok ? { ...r, source_kind: 'frontmatter' } : r;
429
+ if (!r.ok) return r;
430
+ // Say when the verification does not record its test gate, or records a failed
431
+ // gate under a pass: both make "passed" claim more than the verifier checked.
432
+ if (!adapted.test_gate) r.warnings.push('test_gate_unrecorded: the verification does not say whether the test suite ran');
433
+ else if (!TEST_GATE_VALUES.includes(adapted.test_gate)) r.warnings.push(`test_gate_unknown: "${adapted.test_gate}" is not passed, failed or skipped`);
434
+ else if (adapted.test_gate === 'failed' && adapted.outcome === 'pass') r.warnings.push('test_gate_failed_under_pass: the test gate failed but the status is passed');
435
+ if (adapted.not_checked_contradicted) r.warnings.push(`not_checked_contradicted: test_gate is ${adapted.test_gate}, so the not_checked tests entry was dropped`);
436
+ return { ...r, source_kind: 'frontmatter' };
387
437
  }
388
438
  return { ok: false, error: 'no_verdict', reason: 'no pan-verdict block and no verification frontmatter status' };
389
439
  }
@@ -13,14 +13,24 @@ const {
13
13
  const { planningPath } = require('./utils.cjs');
14
14
 
15
15
  /**
16
- * Parse convention rules from CONVENTIONS.md markdown content.
16
+ * The codebase map's conventions document. map-codebase writes `conventions.md`;
17
+ * a map from an older PAN, or one written by hand, may be `CONVENTIONS.md`. Only a
18
+ * case-sensitive filesystem tells the two apart, which is why reading the uppercase
19
+ * name alone found nothing on Linux (prompt audit, market-ideas M34).
20
+ */
21
+ function readConventions(cwd) {
22
+ const dir = path.join(planningPath(cwd), 'codebase');
23
+ return safeReadFile(path.join(dir, 'conventions.md')) || safeReadFile(path.join(dir, 'CONVENTIONS.md'));
24
+ }
25
+
26
+ /**
27
+ * Parse convention rules from the conventions document's markdown content.
17
28
  * Extracts anti-pattern rules from prose containing "instead of", "not", "never".
18
29
  * Run drift check internally and return result object (no output).
19
30
  * Used by cmdValidateHealth --drift.
20
31
  */
21
32
  function runDriftCheck(cwd) {
22
- const conventionsPath = path.join(planningPath(cwd), 'codebase', 'CONVENTIONS.md');
23
- const conventionsContent = safeReadFile(conventionsPath);
33
+ const conventionsContent = readConventions(cwd);
24
34
  const claudeMdContent = safeReadFile(path.join(cwd, 'CLAUDE.md'));
25
35
  const combined = [conventionsContent, claudeMdContent].filter(Boolean).join('\n');
26
36
  const rules = parseConventionRules(combined || null);
@@ -198,8 +208,7 @@ function cmdDriftCheck(cwd, raw, args) {
198
208
  }
199
209
 
200
210
  // Load convention rules
201
- const conventionsPath = path.join(planningPath(cwd), 'codebase', 'CONVENTIONS.md');
202
- const conventionsContent = safeReadFile(conventionsPath);
211
+ const conventionsContent = readConventions(cwd);
203
212
  const claudeMdContent = safeReadFile(path.join(cwd, 'CLAUDE.md'));
204
213
  const combined = [conventionsContent, claudeMdContent].filter(Boolean).join('\n');
205
214
  const rules = parseConventionRules(combined || null);
@@ -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');
@@ -18,7 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { safeReadFile, execGit, findPhaseInternal, toPosix, output, escapeRegex } = require('./core.cjs');
20
20
  const { extractFrontmatter } = require('./frontmatter.cjs');
21
- const { isPlanFile, isSummaryFile } = require('./constants.cjs');
21
+ const { isPlanFile, isSummaryFile, isVerificationFile } = require('./constants.cjs');
22
22
  const { planningRel } = require('./utils.cjs');
23
23
 
24
24
  const LOCKFILES = new Set([
@@ -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;
@@ -153,4 +156,71 @@ function cmdVerifyScope(cwd, phase, raw) {
153
156
  output(r, raw, r.error ? undefined : String(r.candidates.length));
154
157
  }
155
158
 
156
- module.exports = { scopePhase, cmdVerifyScope, LOCKFILES };
159
+ // ─── Stale verification (market-ideas M27) ───────────────────────────────────
160
+ // A verification describes the code at one commit. Until 2026-10-03 nothing tied it
161
+ // to that commit, so a phase edited after it passed still read as verified. The
162
+ // verifier now records `verified_commit`; any file the phase covers that changed
163
+ // since then, committed or not, makes the verification stale.
164
+
165
+ const SHA_RE = /^[0-9a-f]{7,40}$/i;
166
+
167
+ /**
168
+ * The files a phase's verification covered: the plans' declared `files_modified` and
169
+ * the summaries' `key-files`, minus the planning tree, lockfiles and PAN's runtime
170
+ * directories. No git log scan, so `progress` can afford it for every phase.
171
+ */
172
+ function coveredFiles(cwd, info) {
173
+ const dir = path.join(cwd, info.directory);
174
+ const files = new Set();
175
+ for (const f of info.plans.filter(isPlanFile)) {
176
+ const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
177
+ for (const p of asList(fm.files_modified || fm['files-modified'])) files.add(normalisePath(p));
178
+ }
179
+ for (const f of info.summaries.filter(isSummaryFile)) {
180
+ const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
181
+ const kf = fm['key-files'] || fm.key_files || {};
182
+ for (const p of [...asList(kf.created), ...asList(kf.modified)]) files.add(normalisePath(p));
183
+ }
184
+ const planningPrefix = normalisePath(planningRel());
185
+ return [...files].filter((f) => f && !exclusionReason(f, planningPrefix)).sort();
186
+ }
187
+
188
+ /**
189
+ * Whether a phase's verification still describes the code.
190
+ * @param {string} cwd
191
+ * @param {string} phase
192
+ * @param {Object} [info] - findPhaseInternal's result, when the caller already has it
193
+ * @returns {Object} `{phase, state, verified_commit, verification, changed_since, reason?}` or `{error, reason}`.
194
+ * state: `fresh` · `stale` (covered files changed since the verified commit) ·
195
+ * `unverified` (no verification file) · `unknown` (no usable verified_commit, or no git)
196
+ */
197
+ function verificationFreshness(cwd, phase, info) {
198
+ info = info || findPhaseInternal(cwd, phase);
199
+ if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
200
+ const dir = path.join(cwd, info.directory);
201
+ let names = [];
202
+ try { names = fs.readdirSync(dir); } catch { /* unreadable: no verification */ }
203
+ const verification = names.filter(isVerificationFile).sort().pop() || null;
204
+ const base = { phase: info.phase_number, state: 'unverified', verified_commit: null, verification: verification ? toPosix(path.join(info.directory, verification)) : null, changed_since: [] };
205
+ if (!verification) return base;
206
+ const fm = extractFrontmatter(safeReadFile(path.join(dir, verification)) || '');
207
+ const sha = String(fm.verified_commit || '').trim();
208
+ if (!SHA_RE.test(sha)) return { ...base, state: 'unknown', reason: 'the verification records no verified_commit' };
209
+ const known = execGit(cwd, ['cat-file', '-e', `${sha}^{commit}`]);
210
+ if (known.exitCode !== 0) return { ...base, state: 'unknown', verified_commit: sha, reason: 'the verified commit is not in this repository (rebased, squashed, or no git)' };
211
+ const covered = coveredFiles(cwd, info);
212
+ if (!covered.length) return { ...base, state: 'unknown', verified_commit: sha, reason: 'the phase declares no files to compare' };
213
+ // Against the working tree, not HEAD: an uncommitted edit makes the pass stale too.
214
+ const diff = execGit(cwd, ['-c', 'core.quotepath=off', 'diff', '--name-only', '--relative', sha, '--', ...covered]);
215
+ if (diff.exitCode !== 0) return { ...base, state: 'unknown', verified_commit: sha, reason: 'git diff failed' };
216
+ const changed = diff.stdout.split('\n').map(normalisePath).filter(Boolean).sort();
217
+ return { ...base, state: changed.length ? 'stale' : 'fresh', verified_commit: sha, changed_since: changed };
218
+ }
219
+
220
+ function cmdVerifyStale(cwd, phase, raw) {
221
+ if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify stale <phase>' }, raw); return; }
222
+ const r = verificationFreshness(cwd, phase);
223
+ output(r, raw, r.error ? undefined : r.state);
224
+ }
225
+
226
+ module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, LOCKFILES };