pan-wizard 3.35.0 → 3.37.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 (339) hide show
  1. package/CHANGELOG.md +3645 -0
  2. package/README.md +46 -52
  3. package/agents/pan-conductor.md +2 -3
  4. package/agents/pan-counterfactual.md +1 -2
  5. package/agents/pan-debugger.md +14 -3
  6. package/agents/pan-design-checker.md +1 -2
  7. package/agents/pan-designer.md +2 -3
  8. package/agents/pan-document_code.md +1 -2
  9. package/agents/pan-executor.md +25 -0
  10. package/agents/pan-hardener.md +4 -3
  11. package/agents/pan-integration-checker.md +1 -2
  12. package/agents/pan-knowledge.md +1 -2
  13. package/agents/pan-meta-reviewer.md +3 -3
  14. package/agents/pan-optimizer.md +4 -4
  15. package/agents/pan-phase-researcher.md +80 -475
  16. package/agents/pan-plan-checker.md +40 -3
  17. package/agents/pan-planner.md +21 -7
  18. package/agents/pan-previewer.md +3 -2
  19. package/agents/pan-project-researcher.md +72 -493
  20. package/agents/pan-release.md +1 -1
  21. package/agents/pan-reviewer.md +14 -3
  22. package/agents/pan-roadmapper.md +12 -7
  23. package/agents/pan-verifier.md +9 -4
  24. package/bin/install-lib.cjs +134 -14
  25. package/bin/install.js +59 -12
  26. package/commands/pan/army.md +1 -1
  27. package/commands/pan/autodev.md +76 -0
  28. package/commands/pan/cost.md +21 -1
  29. package/commands/pan/exec-phase.md +3 -4
  30. package/commands/pan/focus-auto.md +14 -7
  31. package/commands/pan/focus-design.md +4 -5
  32. package/commands/pan/focus-docs.md +24 -0
  33. package/commands/pan/learn.md +13 -6
  34. package/commands/pan/milestone-audit.md +8 -1
  35. package/commands/pan/milestone-done.md +10 -5
  36. package/commands/pan/milestone-new.md +2 -2
  37. package/commands/pan/new-project.md +15 -12
  38. package/commands/pan/plan-phase.md +15 -9
  39. package/commands/pan/preview.md +1 -1
  40. package/commands/pan/quick.md +1 -1
  41. package/commands/pan/review-deep.md +11 -5
  42. package/commands/pan/settings.md +6 -0
  43. package/commands/pan/verify-phase.md +6 -2
  44. package/docs/AGENTS.md +917 -0
  45. package/docs/ARCHITECTURE.md +1054 -0
  46. package/docs/CLI-REFERENCE.md +4582 -0
  47. package/docs/COMPARISON.md +116 -0
  48. package/docs/DEVELOPMENT.md +490 -0
  49. package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
  50. package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
  51. package/docs/EXAMPLES.md +584 -0
  52. package/docs/FAQ.md +254 -0
  53. package/docs/FIELD-HARVEST-2026-07.md +88 -0
  54. package/docs/FIELD-REPORT-army-2026-06.md +121 -0
  55. package/docs/HOOKS.md +638 -0
  56. package/docs/IMPROVEMENT-TODO.md +246 -0
  57. package/docs/INTERNALS.md +485 -0
  58. package/docs/MIGRATION-v2-to-v3.md +258 -0
  59. package/docs/SECURITY-HARDENING.md +230 -0
  60. package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
  61. package/docs/SKILLS-FULL-TEXT.md +9685 -0
  62. package/docs/SKILLS-REFERENCE.md +1037 -0
  63. package/docs/TROUBLESHOOTING.md +1084 -0
  64. package/docs/USER-GUIDE.md +1437 -0
  65. package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
  66. package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
  67. package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
  68. package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
  69. package/docs/audits/OUTSTANDING-2026-08.md +275 -0
  70. package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
  71. package/docs/branding/README-brand-draft.md +119 -0
  72. package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
  73. package/docs/branding/handoff/README-brand-draft.md +119 -0
  74. package/docs/branding/handoff/image-prompts.md +46 -0
  75. package/docs/branding/image-prompts.md +46 -0
  76. package/docs/branding/nano-banana-prompts.txt +60 -0
  77. package/docs/context-monitor.md +13 -0
  78. package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
  79. package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
  80. package/docs/decisions/ADR-0003-smart-execution.md +84 -0
  81. package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
  82. package/docs/decisions/ADR-0005-command-naming.md +80 -0
  83. package/docs/decisions/ADR-0006-focus-commands.md +101 -0
  84. package/docs/decisions/ADR-0007-ship-v1.md +73 -0
  85. package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
  86. package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
  87. package/docs/decisions/ADR-0010-standards-integration.md +79 -0
  88. package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
  89. package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
  90. package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
  91. package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
  92. package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
  93. package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
  94. package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
  95. package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
  96. package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
  97. package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
  98. package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
  99. package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
  100. package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
  101. package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
  102. package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
  103. package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
  104. package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
  105. package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
  106. package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
  107. package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
  108. package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
  109. package/docs/decisions/ADR-0032-squad-model.md +64 -0
  110. package/docs/decisions/ADR-0033-army-campaign.md +40 -0
  111. package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
  112. package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
  113. package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
  114. package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
  115. package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
  116. package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
  117. package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
  118. package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
  119. package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
  120. package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
  121. package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
  122. package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
  123. package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
  124. package/docs/decisions/ADR-0047-pan-harness.md +76 -0
  125. package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
  126. package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
  127. package/docs/specs/ai_drift_prevention_featureai.md +286 -0
  128. package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
  129. package/docs/specs/command_naming_optimization_featureai.md +930 -0
  130. package/docs/specs/comp_analysis_markdown_v1.md +149 -0
  131. package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
  132. package/docs/specs/contributors_attribution_index_featureai.md +476 -0
  133. package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
  134. package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
  135. package/docs/specs/custom-skills-assessment.md +461 -0
  136. package/docs/specs/deployment_audit_findings_featureai.md +243 -0
  137. package/docs/specs/doc_code_link_graph_featureai.md +253 -0
  138. package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
  139. package/docs/specs/e2e_visual_testing_featureai.md +615 -0
  140. package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
  141. package/docs/specs/evidence-loop-plan.md +76 -0
  142. package/docs/specs/evidence_loop_featureai.md +497 -0
  143. package/docs/specs/focus-auto-optimize-category.md +449 -0
  144. package/docs/specs/focus-auto-optimize-superplan.md +188 -0
  145. package/docs/specs/focus_auto_runner_featureai.md +1102 -0
  146. package/docs/specs/focus_design_v2_featureai.md +197 -0
  147. package/docs/specs/focus_sync_v2_featureai.md +202 -0
  148. package/docs/specs/git_integration_overhaul_featureai.md +326 -0
  149. package/docs/specs/glm_zai_provider_featureai.md +200 -0
  150. package/docs/specs/googlecli_adoption_featureai.md +661 -0
  151. package/docs/specs/help_commands_rewrite_featureai.md +860 -0
  152. package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
  153. package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
  154. package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
  155. package/docs/specs/investigations/README.md +31 -0
  156. package/docs/specs/investigations/digests/2026-09-28.md +47 -0
  157. package/docs/specs/investigations/digests/2026-10-08.md +62 -0
  158. package/docs/specs/investigations/digests/2026-10-10.md +61 -0
  159. package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
  160. package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
  161. package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
  162. package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
  163. package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
  164. package/docs/specs/lifecycle_completeness_featureai.md +754 -0
  165. package/docs/specs/local-first-ci.md +42 -0
  166. package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
  167. package/docs/specs/market-delta-2026-09-superplan.md +310 -0
  168. package/docs/specs/market-ideas-2026-09.md +131 -0
  169. package/docs/specs/market-ideas-2026-10.md +428 -0
  170. package/docs/specs/market-ideas-ledger.md +177 -0
  171. package/docs/specs/memory-optimization-2026-10.md +285 -0
  172. package/docs/specs/multi-track-planning-roots.md +143 -0
  173. package/docs/specs/multi_model_routing_featureai.md +687 -0
  174. package/docs/specs/multi_model_routing_superplan.md +197 -0
  175. package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
  176. package/docs/specs/opus_47_extended_features_featureai.md +363 -0
  177. package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
  178. package/docs/specs/pan_focus_commands_featureai.md +2336 -0
  179. package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
  180. package/docs/specs/product-excellence-superplan.md +315 -0
  181. package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
  182. package/docs/specs/production_deployment_readiness_featureai.md +433 -0
  183. package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
  184. package/docs/specs/real_test_enforcement_featureai.md +213 -0
  185. package/docs/specs/reality-check-2026-09-superplan.md +362 -0
  186. package/docs/specs/right-sized-ceremony.md +74 -0
  187. package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
  188. package/docs/specs/self_improvement_loop_featureai.md +875 -0
  189. package/docs/specs/skill-aligned-decomposition.md +155 -0
  190. package/docs/specs/standards_integration_featureai.md +833 -0
  191. package/docs/specs/testing-system-redesign-2026-09.md +267 -0
  192. package/docs/specs/user_reality_testing_system_featureai.md +788 -0
  193. package/docs/specs/watch-triggers.json +22 -0
  194. package/docs/specs/workflow-integration-redesign.md +742 -0
  195. package/docs/specs/workflow_state_consistency_featureai.md +183 -0
  196. package/hooks/dist/pan-context-monitor.js +9 -2
  197. package/hooks/dist/pan-cost-logger.js +5 -0
  198. package/hooks/dist/pan-guard.js +28 -5
  199. package/hooks/dist/pan-stop-guard.js +9 -9
  200. package/hooks/dist/pan-trace-logger.js +5 -0
  201. package/package.json +10 -2
  202. package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
  203. package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
  204. package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
  205. package/pan-wizard-core/bin/lib/ci.cjs +143 -0
  206. package/pan-wizard-core/bin/lib/commands.cjs +1 -1
  207. package/pan-wizard-core/bin/lib/config.cjs +41 -5
  208. package/pan-wizard-core/bin/lib/constants.cjs +27 -1
  209. package/pan-wizard-core/bin/lib/core.cjs +31 -14
  210. package/pan-wizard-core/bin/lib/cost.cjs +160 -4
  211. package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
  212. package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
  213. package/pan-wizard-core/bin/lib/doc-lint.cjs +5 -3
  214. package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
  215. package/pan-wizard-core/bin/lib/findings.cjs +148 -9
  216. package/pan-wizard-core/bin/lib/focus.cjs +1 -1
  217. package/pan-wizard-core/bin/lib/git.cjs +1 -1
  218. package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
  219. package/pan-wizard-core/bin/lib/init.cjs +7 -2
  220. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
  221. package/pan-wizard-core/bin/lib/merge.cjs +50 -0
  222. package/pan-wizard-core/bin/lib/mutate.cjs +268 -0
  223. package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
  224. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  225. package/pan-wizard-core/bin/lib/phase-gate.cjs +15 -9
  226. package/pan-wizard-core/bin/lib/phase-remove.cjs +91 -11
  227. package/pan-wizard-core/bin/lib/phase.cjs +85 -13
  228. package/pan-wizard-core/bin/lib/plan-lint.cjs +310 -0
  229. package/pan-wizard-core/bin/lib/preview.cjs +26 -4
  230. package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
  231. package/pan-wizard-core/bin/lib/research-lint.cjs +268 -0
  232. package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
  233. package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
  234. package/pan-wizard-core/bin/lib/roadmap.cjs +1 -0
  235. package/pan-wizard-core/bin/lib/runner.cjs +2 -16
  236. package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
  237. package/pan-wizard-core/bin/lib/squads.cjs +1 -1
  238. package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
  239. package/pan-wizard-core/bin/lib/test-gate.cjs +433 -0
  240. package/pan-wizard-core/bin/lib/uat.cjs +149 -0
  241. package/pan-wizard-core/bin/lib/usage.cjs +104 -0
  242. package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
  243. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
  244. package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
  245. package/pan-wizard-core/bin/lib/verify-red.cjs +174 -0
  246. package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
  247. package/pan-wizard-core/bin/lib/verify-scope.cjs +143 -2
  248. package/pan-wizard-core/bin/lib/verify.cjs +86 -31
  249. package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
  250. package/pan-wizard-core/bin/pan-tools.cjs +257 -28
  251. package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
  252. package/pan-wizard-core/mcp/orchestrator.cjs +4 -1
  253. package/pan-wizard-core/references/continuation-format.md +3 -3
  254. package/pan-wizard-core/references/design-methodology.md +1 -1
  255. package/pan-wizard-core/references/guardrails.md +1 -1
  256. package/pan-wizard-core/references/handoff-decisions.md +1 -1
  257. package/pan-wizard-core/references/model-profiles.md +2 -3
  258. package/pan-wizard-core/references/questioning.md +24 -0
  259. package/pan-wizard-core/references/ui-brand.md +4 -6
  260. package/pan-wizard-core/references/verdict-contract.md +2 -1
  261. package/pan-wizard-core/templates/config.json +10 -2
  262. package/pan-wizard-core/templates/debug.md +1 -0
  263. package/pan-wizard-core/templates/design.md +5 -4
  264. package/pan-wizard-core/templates/discovery.md +2 -2
  265. package/pan-wizard-core/templates/phase-prompt.md +7 -0
  266. package/pan-wizard-core/templates/requirements.md +9 -0
  267. package/pan-wizard-core/templates/research.md +10 -0
  268. package/pan-wizard-core/templates/summary-complex.md +5 -1
  269. package/pan-wizard-core/templates/summary-minimal.md +4 -0
  270. package/pan-wizard-core/templates/summary-standard.md +5 -1
  271. package/pan-wizard-core/templates/summary.md +8 -1
  272. package/pan-wizard-core/workflows/add-phase.md +4 -6
  273. package/pan-wizard-core/workflows/assumptions.md +19 -1
  274. package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
  275. package/pan-wizard-core/workflows/discuss-phase.md +4 -2
  276. package/pan-wizard-core/workflows/exec-lean.md +69 -0
  277. package/pan-wizard-core/workflows/exec-phase.md +79 -15
  278. package/pan-wizard-core/workflows/execute-plan.md +3 -1
  279. package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
  280. package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
  281. package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
  282. package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
  283. package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
  284. package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
  285. package/pan-wizard-core/workflows/health.md +6 -0
  286. package/pan-wizard-core/workflows/help.md +114 -77
  287. package/pan-wizard-core/workflows/insert-phase.md +4 -6
  288. package/pan-wizard-core/workflows/learn.md +3 -3
  289. package/pan-wizard-core/workflows/milestone-audit.md +2 -2
  290. package/pan-wizard-core/workflows/milestone-new.md +38 -95
  291. package/pan-wizard-core/workflows/new-project.md +96 -258
  292. package/pan-wizard-core/workflows/optimize.md +107 -5
  293. package/pan-wizard-core/workflows/phase-tests.md +10 -0
  294. package/pan-wizard-core/workflows/plan-phase.md +106 -45
  295. package/pan-wizard-core/workflows/profile.md +50 -0
  296. package/pan-wizard-core/workflows/progress.md +10 -0
  297. package/pan-wizard-core/workflows/quick.md +34 -43
  298. package/pan-wizard-core/workflows/remove-phase.md +11 -1
  299. package/pan-wizard-core/workflows/research-phase.md +118 -40
  300. package/pan-wizard-core/workflows/resume-project.md +18 -2
  301. package/pan-wizard-core/workflows/settings.md +33 -19
  302. package/pan-wizard-core/workflows/todo-check.md +6 -2
  303. package/pan-wizard-core/workflows/transition.md +22 -4
  304. package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
  305. package/pan-wizard-core/workflows/update.md +1 -1
  306. package/pan-wizard-core/workflows/verify-phase.md +39 -46
  307. package/scripts/build-agent-plugin.js +35 -15
  308. package/scripts/build-help.cjs +106 -0
  309. package/scripts/build-plugin.js +10 -1
  310. package/scripts/deprecate-old-versions.js +70 -3
  311. package/scripts/generate-skills-docs.py +1 -1
  312. package/scripts/git-hooks/pre-push +27 -0
  313. package/scripts/install-git-hooks.js +7 -5
  314. package/scripts/install-pre-push.cjs +38 -0
  315. package/scripts/land.cjs +151 -0
  316. package/scripts/local-ci.cjs +501 -0
  317. package/scripts/local-schedule.cjs +277 -0
  318. package/scripts/mutation-probe.cjs +7 -80
  319. package/scripts/prompt-lint.cjs +32 -0
  320. package/scripts/prompt-rules.cjs +156 -0
  321. package/scripts/run-tests.cjs +7 -1
  322. package/scripts/test-quality-lint.cjs +3 -3
  323. package/scripts/test-surface.cjs +175 -6
  324. package/agents/pan-research-synthesizer.md +0 -246
  325. package/commands/pan/audit-deployment.md +0 -386
  326. package/commands/pan/dashboard.md +0 -22
  327. package/commands/pan/discord.md +0 -19
  328. package/commands/pan/mcp-bridge.md +0 -143
  329. package/commands/pan/milestone-cleanup.md +0 -19
  330. package/commands/pan/milestone-gaps.md +0 -35
  331. package/commands/pan/optimize.md +0 -115
  332. package/commands/pan/profile.md +0 -76
  333. package/commands/pan/research-phase.md +0 -189
  334. package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
  335. package/pan-wizard-core/templates/research-project/architecture.md +0 -204
  336. package/pan-wizard-core/templates/research-project/features.md +0 -147
  337. package/pan-wizard-core/templates/research-project/pitfalls.md +0 -200
  338. package/pan-wizard-core/templates/research-project/stack.md +0 -120
  339. package/pan-wizard-core/templates/research-project/summary.md +0 -170
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+ /**
3
+ * Local usage census (program L9): `pan-tools usage`.
4
+ *
5
+ * Which of the installed commands and agents this project has used, from its own
6
+ * files only: the cost ledger (`.planning/metrics/tokens.jsonl`: `command` and `agent`
7
+ * per row) and the trace sessions (`.planning/optimization/traces/<sess>/session.json`:
8
+ * `command`, `ended_at`). It reports what was used (calls, last use), what was never
9
+ * used, and commands that were started but never finished: every trace session for
10
+ * the command stopped without ending, the newest more than UNFINISHED_AFTER_MS ago.
11
+ * Nothing leaves the machine.
12
+ */
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { output } = require('./core.cjs');
16
+ const { readRecords } = require('./cost.cjs');
17
+ const { planningPath } = require('./utils.cjs');
18
+
19
+ // A trace session still open after this long is not "running", it stopped.
20
+ const UNFINISHED_AFTER_MS = 6 * 60 * 60 * 1000;
21
+ // The install this engine ships in: <runtime dir>/pan-wizard-core/bin/lib → <runtime dir>.
22
+ const DEFAULT_INVENTORY = path.resolve(__dirname, '..', '..', '..');
23
+
24
+ /** A command name as the inventory spells it: `/pan:quick`, `pan-quick`, `pan:quick` → `quick`. */
25
+ function commandName(s) {
26
+ return String(s || '').trim().replace(/^\//, '').replace(/^pan[:-]/, '');
27
+ }
28
+
29
+ /** The installed commands and agents: commands/pan/*.md or skills/pan-*, and agents/pan-*. */
30
+ function readInventory(root) {
31
+ const list = (d, re) => { try { return fs.readdirSync(d).filter((f) => re.test(f)); } catch { return []; } };
32
+ const commands = new Set(list(path.join(root, 'commands', 'pan'), /\.md$/).map((f) => f.replace(/\.md$/, '')));
33
+ for (const d of list(path.join(root, 'skills'), /^pan-/)) commands.add(commandName(d));
34
+ const agents = new Set(list(path.join(root, 'agents'), /^pan-.*\.(md|toml)$/).map((f) => f.replace(/\.(md|toml)$/, '')));
35
+ return { commands: [...commands].sort(), agents: [...agents].sort() };
36
+ }
37
+
38
+ function readSessions(cwd) {
39
+ const dir = planningPath(cwd, 'optimization', 'traces');
40
+ let names = [];
41
+ try { names = fs.readdirSync(dir).filter((n) => n.startsWith('sess_')); } catch { return []; }
42
+ const out = [];
43
+ for (const n of names) {
44
+ try { out.push(JSON.parse(fs.readFileSync(path.join(dir, n, 'session.json'), 'utf-8'))); } catch { /* unreadable session */ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * @param {string} cwd
51
+ * @param {{inventory?: string, now?: string}} [opts]
52
+ */
53
+ function usageCensus(cwd, opts = {}) {
54
+ const inventory = readInventory(opts.inventory || DEFAULT_INVENTORY);
55
+ const now = opts.now ? Date.parse(opts.now) : Date.now();
56
+ const cmds = new Map(inventory.commands.map((n) => [n, { name: n, calls: 0, last_used: null, sessions: 0, finished: 0, _lastStart: null }]));
57
+ const agents = new Map(inventory.agents.map((n) => [n, { name: n, calls: 0, last_used: null }]));
58
+ const later = (a, b) => (!a || (b && b > a) ? b : a);
59
+
60
+ const rows = readRecords(cwd);
61
+ for (const r of rows) {
62
+ const c = cmds.get(commandName(r.command));
63
+ if (c) { c.calls++; c.last_used = later(c.last_used, r.ts || null); }
64
+ const a = agents.get(String(r.agent || ''));
65
+ if (a) { a.calls++; a.last_used = later(a.last_used, r.ts || null); }
66
+ }
67
+ const sessions = readSessions(cwd);
68
+ for (const s of sessions) {
69
+ const c = cmds.get(commandName(s.command));
70
+ if (!c) continue;
71
+ c.sessions++;
72
+ if (s.ended_at) c.finished++;
73
+ c._lastStart = later(c._lastStart, s.started_at || null);
74
+ c.last_used = later(c.last_used, s.ended_at || s.started_at || null);
75
+ }
76
+ const byUse = (a, b) => b.calls - a.calls || String(b.last_used).localeCompare(String(a.last_used)) || a.name.localeCompare(b.name);
77
+ const pub = ({ _lastStart, ...rest }) => rest;
78
+ const cmdList = [...cmds.values()];
79
+ const used = cmdList.filter((c) => c.calls > 0 || c.sessions > 0);
80
+ const unfinished = used.filter((c) => c.sessions > 0 && c.finished === 0 && c._lastStart && now - Date.parse(c._lastStart) > UNFINISHED_AFTER_MS);
81
+ const agentList = [...agents.values()];
82
+ return {
83
+ local_only: true,
84
+ commands: {
85
+ used: used.sort(byUse).map(pub),
86
+ never_used: cmdList.filter((c) => c.calls === 0 && c.sessions === 0).map((c) => c.name).sort(),
87
+ unfinished: unfinished.sort(byUse).map(pub),
88
+ installed: inventory.commands.length,
89
+ },
90
+ agents: {
91
+ used: agentList.filter((a) => a.calls > 0).sort(byUse),
92
+ never_used: agentList.filter((a) => a.calls === 0).map((a) => a.name).sort(),
93
+ installed: inventory.agents.length,
94
+ },
95
+ sources: { cost_rows: rows.length, trace_sessions: sessions.length },
96
+ };
97
+ }
98
+
99
+ function cmdUsage(cwd, raw) {
100
+ const r = usageCensus(cwd);
101
+ output(r, raw, `${r.commands.used.length} of ${r.commands.installed} commands used, ${r.commands.unfinished.length} unfinished; ${r.agents.used.length} of ${r.agents.installed} agents used`);
102
+ }
103
+
104
+ module.exports = { usageCensus, readInventory, commandName, cmdUsage, UNFINISHED_AFTER_MS };
@@ -36,7 +36,7 @@ const KNOWN_VERDICTS = Object.freeze({
36
36
  'pan-design-checker': Object.freeze({ PASS: 'pass', GAPS: 'fail' }),
37
37
  });
38
38
 
39
- const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80 });
39
+ const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80, declined: 20 });
40
40
  // What the verifier's test gate can report (verify-phase.md, run_test_suite).
41
41
  const TEST_GATE_VALUES = Object.freeze(['passed', 'failed', 'skipped']);
42
42
  const RESOLUTION_STATUSES = Object.freeze(['resolved', 'still_present', 'cannot_tell']);
@@ -192,12 +192,15 @@ function validateVerdict(obj, opts = {}) {
192
192
  warnings.push(`summary_truncated: #${i}`);
193
193
  }
194
194
  const where = f.where == null || cleanText(f.where) === '' ? null : truncate(cleanText(f.where).replace(/\\/g, '/'), LIMITS.where);
195
- findings.push({ class: cls, severity, where, summary });
195
+ // pre_existing (M52): the finding is in code the change did not touch. It is
196
+ // recorded as deferred debt, never a blocker; findings.cjs files it.
197
+ findings.push({ class: cls, severity, where, summary, ...(f.pre_existing === true ? { pre_existing: true } : {}) });
196
198
  });
197
199
 
198
200
  const notChecked = normaliseNotChecked(obj.not_checked, warnings);
199
201
  const resolutions = normaliseResolutions(obj.resolutions, warnings);
200
- return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked, resolutions }, warnings };
202
+ const declined = normaliseDeclined(obj.declined, warnings);
203
+ return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked, resolutions, ...(declined.length ? { declined } : {}) }, warnings };
201
204
  }
202
205
 
203
206
  /**
@@ -234,6 +237,26 @@ function normaliseResolutions(value, warnings) {
234
237
  return out;
235
238
  }
236
239
 
240
+ /**
241
+ * The behaviours a judge set aside, each `{behaviour, reason}` (MI-118, program R2).
242
+ * A review that names what it declined to judge says where its silence is not a pass.
243
+ * Accepts bare strings; drops entries that name nothing.
244
+ */
245
+ function normaliseDeclined(value, warnings) {
246
+ if (value == null) return [];
247
+ if (!Array.isArray(value)) { warnings.push('declined_not_an_array'); return []; }
248
+ const out = [];
249
+ for (const entry of value.slice(0, LIMITS.declined)) {
250
+ const obj = typeof entry === 'string' ? { behaviour: entry } : (entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null);
251
+ const behaviour = obj ? truncate(cleanText(obj.behaviour ?? obj.behavior ?? ''), LIMITS.summary) : '';
252
+ if (!behaviour) continue;
253
+ const reason = obj.reason == null || cleanText(obj.reason) === '' ? null : truncate(cleanText(obj.reason), LIMITS.summary);
254
+ out.push({ behaviour, reason });
255
+ }
256
+ if (value.length > LIMITS.declined) warnings.push(`declined_truncated: ${value.length} reported, ${LIMITS.declined} kept`);
257
+ return out;
258
+ }
259
+
237
260
  /**
238
261
  * The checks a judge could not run, each `{check, reason}` (market-ideas M23).
239
262
  * A verdict that carries them says what it did not see: a `pass` with tests that
@@ -176,9 +176,38 @@ function checkPlanSet(cwd, phase) {
176
176
  }
177
177
  }
178
178
 
179
+ // 5. Property tasks (V10): a requirement whose EARS criterion is a validation rule
180
+ // (IF … THEN … SHALL, or a claim over any / every / all inputs, or a "never") is a
181
+ // claim over many inputs, which one example test cannot show. The plan that lists it
182
+ // must carry a property-test task: property="true" on the task, or "property test" /
183
+ // "property-based" in its name or action.
184
+ let reqText = '';
185
+ try { reqText = fs.readFileSync(planningPath(cwd, 'requirements.md'), 'utf-8'); } catch { /* none */ }
186
+ if (reqText && required.length) {
187
+ const RL = require('./requirements-lint.cjs');
188
+ const byId = new Map(RL.parseRequirements(reqText).map((r) => [key(r.id), r]));
189
+ for (const id of required) {
190
+ const r = byId.get(key(id));
191
+ if (!r || !r.criteria.some(isValidationRule)) continue;
192
+ const covering = plans.filter((p) => p.requirements.map(key).includes(key(id)));
193
+ const hasProperty = covering.some((p) => {
194
+ let text = '';
195
+ try { text = toLf(fs.readFileSync(path.join(pf.dir, p.file), 'utf-8')); } catch { /* unreadable */ }
196
+ return /<task\b[^>]*\bproperty=["']?true/i.test(text) || /property[- ]based|property test/i.test(text);
197
+ });
198
+ if (!hasProperty) errors.push({ code: 'missing_property_task', requirement: id, detail: `${id} states a validation rule ("${r.criteria.find(isValidationRule).slice(0, 80)}"): a plan covering it needs a property-test task, which checks the rule over generated inputs with the project's own property library` });
199
+ }
200
+ }
201
+
179
202
  return { phase: phaseNumber, ok: errors.length === 0, errors, warnings, plans: plans.length, plans_sha: plansSha(pf.dir, pf.planFiles) };
180
203
  }
181
204
 
205
+ /** A validation-rule criterion (V10): unwanted-behaviour EARS, or a claim over all inputs. */
206
+ function isValidationRule(criterion) {
207
+ const c = String(criterion || '');
208
+ return /^IF\b[\s\S]*\bTHEN\b/i.test(c) || /\b(any|every|all|each)\s+\w+/i.test(c) || /\bnever\b/i.test(c);
209
+ }
210
+
182
211
  /**
183
212
  * Whether the plans exec-phase is about to run are the plans that passed a plan check.
184
213
  * @returns {{status: 'ok'|'stale'|'failed'|'missing'|'legacy'|'disabled', reason?: string, plans_sha?: string, checked_sha?: string, attempt?: number}}
@@ -227,4 +256,4 @@ function cmdVerifyPlanSet(cwd, phase, raw) {
227
256
  output(result, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
228
257
  }
229
258
 
230
- module.exports = { checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
259
+ module.exports = { isValidationRule, phaseFiles, checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
@@ -103,11 +103,11 @@ function cmdPreflight(cwd, target, raw) {
103
103
  checks.push({ name: 'batch_exists', passed: true, detail: files[files.length - 1] });
104
104
  } else {
105
105
  checks.push({ name: 'batch_exists', passed: false, detail: 'no batch file found' });
106
- blockers.push('No batch file — run /pan:focus-plan first');
106
+ blockers.push('No batch file — run /pan:focus-auto --step plan first');
107
107
  }
108
108
  } catch {
109
109
  checks.push({ name: 'batch_exists', passed: false, detail: 'focus/ directory not found' });
110
- blockers.push('No focus/ directory — run /pan:focus-scan first');
110
+ blockers.push('No focus/ directory — run /pan:focus-auto --step scan first');
111
111
  }
112
112
  } else {
113
113
  // target is a phase number — check the phase directory exists
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+ /**
3
+ * RED and catch proof (program V4): does each test this phase wrote actually test what
4
+ * the phase built?
5
+ *
6
+ * A test that passes before the phase's code exists cannot have caught anything: it is
7
+ * tautological, or it tests something the phase did not change. So, independent of the
8
+ * order the commits landed in (Meta's "weak catch"):
9
+ *
10
+ * base the parent of the phase's oldest plan commit (`{type}({phase}-{plan}):`)
11
+ * tests the test files the phase's plan commits added or changed, as they are now
12
+ * red in a throwaway worktree at the base, with the test file copied in from HEAD,
13
+ * the test must FAIL (a crash on a missing module counts: the code is not there)
14
+ * green at HEAD it must PASS
15
+ *
16
+ * caught red at the base, green at HEAD
17
+ * not_red passes without the phase's code: it cannot have caught a regression
18
+ * not_green fails at HEAD: the test gate reports that, not this check
19
+ * not_checked no command can run that one file (cargo, Maven, Gradle, make), with why
20
+ *
21
+ * Each file runs on its own (`workflow.test_file_command`, else derived from the suite's
22
+ * runner: test-gate.cjs `fileCommand`). The worktree links the project's node_modules,
23
+ * which git does not track, so a JS runner resolves the same at the base as at HEAD.
24
+ */
25
+ const fs = require('fs');
26
+ const os = require('os');
27
+ const path = require('path');
28
+ const { execGit, findPhaseInternal, output, error } = require('./core.cjs');
29
+ const { committedFiles } = require('./verify-scope.cjs');
30
+ const { isTestFile } = require('./verify-integrity.cjs');
31
+ const TG = require('./test-gate.cjs');
32
+
33
+ const DEFAULT_FILE_TIMEOUT_MS = 5 * 60 * 1000;
34
+
35
+ /**
36
+ * The commit the phase started from: the parent of its oldest plan commit.
37
+ * @returns {{base: string, files: Set<string>, commits: number}|{base: null, reason: string}}
38
+ */
39
+ function phaseBase(cwd, phase) {
40
+ const c = committedFiles(cwd, phase);
41
+ if (!c.git) return { base: null, reason: 'git is unavailable' };
42
+ if (!c.commits) return { base: null, reason: 'the phase has no plan commits (subjects like feat(03-01): …)' };
43
+ const oldest = c.hashes[c.hashes.length - 1];
44
+ const r = execGit(cwd, ['rev-parse', '--verify', '-q', `${oldest}^`]);
45
+ if (r.exitCode !== 0 || !r.stdout) return { base: null, reason: 'the phase starts at the first commit of the repository: there is no base to run against' };
46
+ return { base: r.stdout.trim(), files: c.files, commits: c.commits };
47
+ }
48
+
49
+ /** Remove a directory link (a junction or a symlink) without touching what it points to. */
50
+ function unlinkDir(p) {
51
+ try { fs.unlinkSync(p); } catch { try { fs.rmdirSync(p); } catch { /* already gone */ } }
52
+ }
53
+
54
+ /**
55
+ * Run `fn(dir)` in a detached worktree at `rev`, then remove the worktree. The project's
56
+ * node_modules is linked in (and unlinked before git removes the worktree, so the removal
57
+ * can never reach the real one).
58
+ * @returns {{value?: *, error?: string}}
59
+ */
60
+ function withWorktree(cwd, rev, fn) {
61
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-wt-'));
62
+ const dir = path.join(root, 'wt');
63
+ const add = execGit(cwd, ['worktree', 'add', '--detach', '-q', dir, rev]);
64
+ if (add.exitCode !== 0) {
65
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
66
+ return { error: `git worktree add failed: ${add.stderr || add.stdout}` };
67
+ }
68
+ const link = path.join(dir, 'node_modules');
69
+ let linked = false;
70
+ try {
71
+ const modules = path.join(cwd, 'node_modules');
72
+ if (fs.existsSync(modules) && !fs.existsSync(link)) {
73
+ try { fs.symlinkSync(modules, link, 'junction'); linked = true; } catch { /* runs without it */ }
74
+ }
75
+ return { value: fn(dir) };
76
+ } finally {
77
+ if (linked) unlinkDir(link);
78
+ execGit(cwd, ['worktree', 'remove', '--force', dir]);
79
+ execGit(cwd, ['worktree', 'prune']);
80
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
81
+ }
82
+ }
83
+
84
+ const tail = (s) => TG.stripAnsi(String(s || '')).replace(/\r\n?/g, '\n').trim().split('\n').slice(-6).join('\n');
85
+
86
+ /**
87
+ * The RED/catch proof for one phase.
88
+ * @param {string} cwd
89
+ * @param {string} phase
90
+ * @param {{timeoutMs?: number}} [opts]
91
+ * @returns {Object} `{phase, checked, ok, base, files: [{file, verdict, command, base_exit, head_exit, detail}], reason?}` or `{error, reason}`
92
+ */
93
+ function checkRed(cwd, phase, opts = {}) {
94
+ const info = findPhaseInternal(cwd, phase);
95
+ if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
96
+ const phaseNumber = info.phase_number || String(phase);
97
+ const b = phaseBase(cwd, phaseNumber);
98
+ if (!b.base) return { phase: phaseNumber, checked: false, ok: true, files: [], reason: b.reason };
99
+ const tests = [...b.files].filter((f) => isTestFile(f) && fs.existsSync(path.join(cwd, f))).sort();
100
+ if (!tests.length) return { phase: phaseNumber, checked: false, ok: true, base: b.base, files: [], reason: 'the phase changed no test file' };
101
+ const timeoutMs = opts.timeoutMs || DEFAULT_FILE_TIMEOUT_MS;
102
+ const files = [];
103
+ const wt = withWorktree(cwd, b.base, (dir) => {
104
+ for (const file of tests) {
105
+ const fc = TG.fileCommand(cwd, file);
106
+ if (!fc.command) { files.push({ file, verdict: 'not_checked', detail: fc.reason }); continue; }
107
+ const dest = path.join(dir, file);
108
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
109
+ fs.copyFileSync(path.join(cwd, file), dest);
110
+ const atBase = TG.runCommand(dir, fc.command, timeoutMs);
111
+ const atHead = TG.runCommand(cwd, fc.command, timeoutMs);
112
+ const red = atBase.exit !== 0;
113
+ const green = atHead.exit === 0;
114
+ const verdict = red && green ? 'caught' : (!red ? 'not_red' : 'not_green');
115
+ const detail = verdict === 'not_red'
116
+ ? 'passes without the phase\'s code: it is tautological, or tests something the phase did not change'
117
+ : verdict === 'not_green' ? `fails at HEAD: ${tail(atHead.output)}` : null;
118
+ files.push({ file, verdict, command: fc.command, base_exit: atBase.exit, head_exit: atHead.exit, ...(detail ? { detail } : {}) });
119
+ }
120
+ });
121
+ if (wt.error) return { phase: phaseNumber, checked: false, ok: true, base: b.base, files: [], reason: wt.error };
122
+ const ran = files.filter((f) => f.verdict !== 'not_checked');
123
+ return {
124
+ phase: phaseNumber, checked: ran.length > 0, ok: ran.every((f) => f.verdict === 'caught'), base: b.base,
125
+ caught: ran.filter((f) => f.verdict === 'caught').length, files,
126
+ ...(ran.length ? {} : { reason: 'no changed test file could be run on its own' }),
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Prove a review finding with a catching test (program R5): the inverse of RED. The
132
+ * test shows the defect the phase introduced when it PASSES at the phase's base (the
133
+ * behaviour was right before) and FAILS at HEAD (the phase broke it).
134
+ *
135
+ * confirmed passes at the base, fails at HEAD
136
+ * fails_at_base fails before the phase too: not specific to the change
137
+ * passes_at_head the defect it claims is not there, or the test does not reach it
138
+ * not_checked no base, no command for the file, or the worktree failed (with why)
139
+ *
140
+ * @returns {{status, test, base?, command?, base_exit?, head_exit?, detail?}}
141
+ */
142
+ function proveFinding(cwd, phase, testFile, opts = {}) {
143
+ const test = String(testFile || '').split(path.sep).join('/').split('\\').join('/');
144
+ if (!test || !fs.existsSync(path.join(cwd, test))) return { status: 'not_checked', test, detail: 'the test file does not exist' };
145
+ const info = findPhaseInternal(cwd, phase);
146
+ const b = phaseBase(cwd, (info && info.phase_number) || String(phase));
147
+ if (!b.base) return { status: 'not_checked', test, detail: b.reason };
148
+ const fc = TG.fileCommand(cwd, test);
149
+ if (!fc.command) return { status: 'not_checked', test, detail: fc.reason };
150
+ const timeoutMs = opts.timeoutMs || DEFAULT_FILE_TIMEOUT_MS;
151
+ let atBase = null;
152
+ const wt = withWorktree(cwd, b.base, (dir) => {
153
+ const dest = path.join(dir, test);
154
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
155
+ fs.copyFileSync(path.join(cwd, test), dest);
156
+ atBase = TG.runCommand(dir, fc.command, timeoutMs);
157
+ });
158
+ if (wt.error) return { status: 'not_checked', test, base: b.base, detail: wt.error };
159
+ const atHead = TG.runCommand(cwd, fc.command, timeoutMs);
160
+ const status = atBase.exit !== 0 ? 'fails_at_base' : (atHead.exit === 0 ? 'passes_at_head' : 'confirmed');
161
+ const detail = status === 'fails_at_base' ? `fails before the phase too: ${tail(atBase.output)}`
162
+ : status === 'passes_at_head' ? 'passes at HEAD: it does not show the defect the finding claims' : null;
163
+ return { status, test, base: b.base, command: fc.command, base_exit: atBase.exit, head_exit: atHead.exit, ...(detail ? { detail } : {}) };
164
+ }
165
+
166
+ /** `pan-tools verify red <phase> [--timeout <ms>]`: exit 1 when a test is not_red or not_green. */
167
+ function cmdVerifyRed(cwd, phase, raw, opts = {}) {
168
+ if (!phase) error('Usage: verify red <phase> [--timeout <ms>]');
169
+ const r = checkRed(cwd, phase, { timeoutMs: opts.timeout ? Number(opts.timeout) : undefined });
170
+ if (r.error) error(r.reason);
171
+ output(r, raw, !r.checked ? 'not_checked' : (r.ok ? 'caught' : 'not_caught'), r.ok ? 0 : 1);
172
+ }
173
+
174
+ module.exports = { checkRed, cmdVerifyRed, phaseBase, withWorktree, proveFinding };
@@ -38,9 +38,10 @@ function collectVerificationStats(phasesDir) {
38
38
  // Extract gap descriptions from ## Gaps section
39
39
  const gapsMatch = content.match(/## Gaps[\s\S]*?(?=\n## |$)/);
40
40
  if (gapsMatch) {
41
- const lines = gapsMatch[0].split('\n').filter(l => l.match(/^[-*]\s+/));
41
+ // Bullets and numbered items both (L6b: numbered gap lists were dropped).
42
+ const lines = gapsMatch[0].split('\n').filter(l => l.match(/^(?:[-*]|\d+[.)])\s+/));
42
43
  for (const line of lines) {
43
- const desc = line.replace(/^[-*]\s+/, '').trim();
44
+ const desc = line.replace(/^(?:[-*]|\d+[.)])\s+/, '').trim();
44
45
  if (desc) stats.gap_patterns.push(desc);
45
46
  }
46
47
  }
@@ -17,7 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { safeReadFile, execGit, findPhaseInternal, toPosix, output, escapeRegex } = require('./core.cjs');
20
- const { extractFrontmatter } = require('./frontmatter.cjs');
20
+ const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
21
21
  const { isPlanFile, isSummaryFile, isVerificationFile } = require('./constants.cjs');
22
22
  const { planningRel } = require('./utils.cjs');
23
23
 
@@ -151,6 +151,147 @@ function scopePhase(cwd, phase) {
151
151
  };
152
152
  }
153
153
 
154
+ // ─── Review scope (program R1) ───────────────────────────────────────────────
155
+ // The reviewer used to get the summaries' key-files: what the executor said it touched.
156
+ // A file the executor changed and did not list was never reviewed. The review scope is
157
+ // the diff instead (the phase's plan commits, plus work not yet committed), with the
158
+ // intent the code is judged against: each plan's objective, must_haves and Review Focus,
159
+ // and the text of the requirements the plans claim.
160
+
161
+ /** Files modified, staged or untracked in the working tree, relative to cwd. */
162
+ function workingTreeFiles(cwd) {
163
+ // Porcelain v2 with -z: each entry starts with its type (`1`, `2`, `u`, `?`), never a
164
+ // space, and paths come unquoted. v1 starts an unstaged edit with a space (" M a.js"),
165
+ // which execGit's trim() cut off the first line, and the path lost its first letter.
166
+ const r = execGit(cwd, ['status', '--porcelain=v2', '-z', '--untracked-files=all', '--', '.']);
167
+ if (r.exitCode !== 0) return { git: false, uncommitted: [], untracked: [] };
168
+ const top = execGit(cwd, ['rev-parse', '--show-prefix']);
169
+ const prefix = top.exitCode === 0 ? normalisePath(top.stdout.trim()) : '';
170
+ const rel = (p) => { const n = normalisePath(p); return prefix && n.startsWith(prefix) ? n.slice(prefix.length) : n; };
171
+ const uncommitted = [];
172
+ const untracked = [];
173
+ const entries = r.stdout.split('\0');
174
+ for (let i = 0; i < entries.length; i++) {
175
+ const e = entries[i];
176
+ if (!e) continue;
177
+ const f = e.split(' ');
178
+ if (e[0] === '?') untracked.push(rel(e.slice(2)));
179
+ else if (e[0] === '1') { if (!f[1].includes('D')) uncommitted.push(rel(f.slice(8).join(' '))); }
180
+ else if (e[0] === '2') { if (!f[1].includes('D')) uncommitted.push(rel(f.slice(9).join(' '))); i++; } // the next entry is the old path
181
+ else if (e[0] === 'u') uncommitted.push(rel(f.slice(10).join(' ')));
182
+ }
183
+ return { git: true, uncommitted, untracked };
184
+ }
185
+
186
+ /** A markdown section's body, from its `## <title>` heading to the next heading of the same or higher level. */
187
+ function sectionBody(text, title) {
188
+ const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
189
+ const re = new RegExp(`^(#{1,6})\\s+${escapeRegex(title)}\\s*$`, 'i');
190
+ const at = lines.findIndex((l) => re.test(l.trim()));
191
+ if (at === -1) return null;
192
+ const level = lines[at].trim().match(/^#+/)[0].length;
193
+ const out = [];
194
+ for (let i = at + 1; i < lines.length; i++) {
195
+ const h = lines[i].match(/^(#{1,6})\s/);
196
+ if (h && h[1].length <= level) break;
197
+ out.push(lines[i]);
198
+ }
199
+ const body = out.join('\n').trim();
200
+ return body || null;
201
+ }
202
+
203
+ /** The text between `<tag>` and `</tag>`, trimmed, or null. */
204
+ function tagBody(text, tag) {
205
+ const m = String(text || '').match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
206
+ return m && m[1].trim() ? m[1].trim() : null;
207
+ }
208
+
209
+ /**
210
+ * What a reviewer of one phase reads: the diff and the intent.
211
+ * @returns {Object} `{phase, directory, base, files: [{path, sources, listed}], unlisted, excluded,
212
+ * intent: {plans: [{plan, objective, must_haves, review_focus, requirements}], requirements: [{id, text}], missing_requirements}, git, commits}`
213
+ * or `{error, reason}`. `sources` is any of commit · summary · uncommitted · untracked;
214
+ * `listed` is whether a summary's key-files names it; `unlisted` lists the files that are not.
215
+ */
216
+ function reviewScope(cwd, phase) {
217
+ const info = findPhaseInternal(cwd, phase);
218
+ if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
219
+ const dir = path.join(cwd, info.directory);
220
+ const sources = new Map();
221
+ const note = (file, source) => {
222
+ if (!file) return;
223
+ if (!sources.has(file)) sources.set(file, new Set());
224
+ sources.get(file).add(source);
225
+ };
226
+ const listed = new Set();
227
+ for (const f of info.summaries.filter(isSummaryFile)) {
228
+ const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
229
+ const kf = fm['key-files'] || fm.key_files || {};
230
+ for (const p of [...asList(kf.created), ...asList(kf.modified)]) { const n = normalisePath(p); if (n) { listed.add(n); note(n, 'summary'); } }
231
+ }
232
+ const git = committedFiles(cwd, info.phase_number);
233
+ for (const p of git.files) note(p, 'commit');
234
+ const wt = workingTreeFiles(cwd);
235
+ for (const p of wt.uncommitted) note(p, 'uncommitted');
236
+ for (const p of wt.untracked) note(p, 'untracked');
237
+
238
+ const planningPrefix = normalisePath(planningRel());
239
+ const files = [];
240
+ const excluded = [];
241
+ for (const file of [...sources.keys()].sort()) {
242
+ const why = exclusionReason(file, planningPrefix);
243
+ if (why) { excluded.push({ path: file, reason: why }); continue; }
244
+ // A file deleted by the phase has nothing left to read.
245
+ if (!fs.existsSync(path.join(cwd, file))) continue;
246
+ files.push({ path: file, sources: [...sources.get(file)].sort(), listed: listed.has(file) });
247
+ }
248
+
249
+ const plans = [];
250
+ const reqIds = new Set();
251
+ for (const f of info.plans.filter(isPlanFile).sort()) {
252
+ const text = safeReadFile(path.join(dir, f)) || '';
253
+ const fm = extractFrontmatter(text);
254
+ const requirements = asList(fm.requirements).map((s) => s.trim()).filter(Boolean);
255
+ for (const id of requirements) reqIds.add(id);
256
+ const mh = fm.must_haves && typeof fm.must_haves === 'object' ? fm.must_haves : {};
257
+ plans.push({
258
+ plan: f,
259
+ objective: tagBody(text, 'objective'),
260
+ must_haves: { truths: asList(mh.truths), artifacts: parseMustHavesBlock(text, 'artifacts'), key_links: parseMustHavesBlock(text, 'key_links') },
261
+ review_focus: sectionBody(text, 'Review Focus'),
262
+ requirements,
263
+ });
264
+ }
265
+ const { requirementLines } = require('./roadmap.cjs');
266
+ const reqText = safeReadFile(path.join(cwd, planningRel(), 'requirements.md')) || '';
267
+ const ids = [...reqIds].sort();
268
+ const requirements = ids.map((id) => ({ id, text: requirementLines(reqText, [id]).lines.join('\n').trim() || null }));
269
+
270
+ let base = null;
271
+ if (git.hashes.length) {
272
+ const r = execGit(cwd, ['rev-parse', '--verify', '-q', `${git.hashes[git.hashes.length - 1]}^`]);
273
+ if (r.exitCode === 0 && r.stdout) base = r.stdout.trim();
274
+ }
275
+ return {
276
+ phase: info.phase_number,
277
+ directory: info.directory,
278
+ base,
279
+ files,
280
+ unlisted: files.filter((f) => !f.listed).map((f) => f.path),
281
+ excluded,
282
+ intent: { plans, requirements, missing_requirements: requirements.filter((r) => !r.text).map((r) => r.id) },
283
+ git: git.git,
284
+ commits: git.commits,
285
+ };
286
+ }
287
+
288
+ /** `pan-tools review scope <phase>`: the JSON scope; `--raw` prints the file paths, one per line. */
289
+ function cmdReviewScope(cwd, phase, raw) {
290
+ if (!phase) { output({ error: 'phase_required', reason: 'Usage: review scope <phase>' }, raw); return; }
291
+ const r = reviewScope(cwd, phase);
292
+ output(r, raw, r.error ? undefined : r.files.map((f) => f.path).join('\n'));
293
+ }
294
+
154
295
  function cmdVerifyScope(cwd, phase, raw) {
155
296
  if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify scope <phase>' }, raw); return; }
156
297
  const r = scopePhase(cwd, phase);
@@ -224,4 +365,4 @@ function cmdVerifyStale(cwd, phase, raw) {
224
365
  output(r, raw, r.error ? undefined : r.state);
225
366
  }
226
367
 
227
- module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
368
+ module.exports = { scopePhase, cmdVerifyScope, reviewScope, cmdReviewScope, workingTreeFiles, sectionBody, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };