pan-wizard 3.36.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 (314) hide show
  1. package/CHANGELOG.md +3645 -0
  2. package/README.md +36 -48
  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 +1 -2
  8. package/agents/pan-document_code.md +1 -2
  9. package/agents/pan-hardener.md +2 -3
  10. package/agents/pan-integration-checker.md +1 -2
  11. package/agents/pan-knowledge.md +1 -2
  12. package/agents/pan-meta-reviewer.md +3 -3
  13. package/agents/pan-optimizer.md +4 -4
  14. package/agents/pan-phase-researcher.md +79 -488
  15. package/agents/pan-plan-checker.md +12 -2
  16. package/agents/pan-planner.md +10 -6
  17. package/agents/pan-previewer.md +1 -2
  18. package/agents/pan-project-researcher.md +70 -499
  19. package/agents/pan-release.md +1 -1
  20. package/agents/pan-reviewer.md +9 -3
  21. package/agents/pan-roadmapper.md +12 -7
  22. package/agents/pan-verifier.md +7 -2
  23. package/bin/install-lib.cjs +134 -14
  24. package/bin/install.js +59 -12
  25. package/commands/pan/army.md +1 -1
  26. package/commands/pan/autodev.md +76 -0
  27. package/commands/pan/exec-phase.md +3 -4
  28. package/commands/pan/focus-auto.md +14 -7
  29. package/commands/pan/focus-design.md +4 -5
  30. package/commands/pan/focus-docs.md +24 -0
  31. package/commands/pan/learn.md +13 -6
  32. package/commands/pan/milestone-audit.md +8 -1
  33. package/commands/pan/milestone-done.md +10 -5
  34. package/commands/pan/milestone-new.md +2 -2
  35. package/commands/pan/new-project.md +15 -12
  36. package/commands/pan/plan-phase.md +15 -9
  37. package/commands/pan/quick.md +1 -1
  38. package/commands/pan/review-deep.md +11 -5
  39. package/commands/pan/settings.md +6 -0
  40. package/commands/pan/verify-phase.md +6 -2
  41. package/docs/AGENTS.md +917 -0
  42. package/docs/ARCHITECTURE.md +1054 -0
  43. package/docs/CLI-REFERENCE.md +4582 -0
  44. package/docs/COMPARISON.md +116 -0
  45. package/docs/DEVELOPMENT.md +490 -0
  46. package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
  47. package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
  48. package/docs/EXAMPLES.md +584 -0
  49. package/docs/FAQ.md +254 -0
  50. package/docs/FIELD-HARVEST-2026-07.md +88 -0
  51. package/docs/FIELD-REPORT-army-2026-06.md +121 -0
  52. package/docs/HOOKS.md +638 -0
  53. package/docs/IMPROVEMENT-TODO.md +246 -0
  54. package/docs/INTERNALS.md +485 -0
  55. package/docs/MIGRATION-v2-to-v3.md +258 -0
  56. package/docs/SECURITY-HARDENING.md +230 -0
  57. package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
  58. package/docs/SKILLS-FULL-TEXT.md +9685 -0
  59. package/docs/SKILLS-REFERENCE.md +1037 -0
  60. package/docs/TROUBLESHOOTING.md +1084 -0
  61. package/docs/USER-GUIDE.md +1437 -0
  62. package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
  63. package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
  64. package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
  65. package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
  66. package/docs/audits/OUTSTANDING-2026-08.md +275 -0
  67. package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
  68. package/docs/branding/README-brand-draft.md +119 -0
  69. package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
  70. package/docs/branding/handoff/README-brand-draft.md +119 -0
  71. package/docs/branding/handoff/image-prompts.md +46 -0
  72. package/docs/branding/image-prompts.md +46 -0
  73. package/docs/branding/nano-banana-prompts.txt +60 -0
  74. package/docs/context-monitor.md +13 -0
  75. package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
  76. package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
  77. package/docs/decisions/ADR-0003-smart-execution.md +84 -0
  78. package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
  79. package/docs/decisions/ADR-0005-command-naming.md +80 -0
  80. package/docs/decisions/ADR-0006-focus-commands.md +101 -0
  81. package/docs/decisions/ADR-0007-ship-v1.md +73 -0
  82. package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
  83. package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
  84. package/docs/decisions/ADR-0010-standards-integration.md +79 -0
  85. package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
  86. package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
  87. package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
  88. package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
  89. package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
  90. package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
  91. package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
  92. package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
  93. package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
  94. package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
  95. package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
  96. package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
  97. package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
  98. package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
  99. package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
  100. package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
  101. package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
  102. package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
  103. package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
  104. package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
  105. package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
  106. package/docs/decisions/ADR-0032-squad-model.md +64 -0
  107. package/docs/decisions/ADR-0033-army-campaign.md +40 -0
  108. package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
  109. package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
  110. package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
  111. package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
  112. package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
  113. package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
  114. package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
  115. package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
  116. package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
  117. package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
  118. package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
  119. package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
  120. package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
  121. package/docs/decisions/ADR-0047-pan-harness.md +76 -0
  122. package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
  123. package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
  124. package/docs/specs/ai_drift_prevention_featureai.md +286 -0
  125. package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
  126. package/docs/specs/command_naming_optimization_featureai.md +930 -0
  127. package/docs/specs/comp_analysis_markdown_v1.md +149 -0
  128. package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
  129. package/docs/specs/contributors_attribution_index_featureai.md +476 -0
  130. package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
  131. package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
  132. package/docs/specs/custom-skills-assessment.md +461 -0
  133. package/docs/specs/deployment_audit_findings_featureai.md +243 -0
  134. package/docs/specs/doc_code_link_graph_featureai.md +253 -0
  135. package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
  136. package/docs/specs/e2e_visual_testing_featureai.md +615 -0
  137. package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
  138. package/docs/specs/evidence-loop-plan.md +76 -0
  139. package/docs/specs/evidence_loop_featureai.md +497 -0
  140. package/docs/specs/focus-auto-optimize-category.md +449 -0
  141. package/docs/specs/focus-auto-optimize-superplan.md +188 -0
  142. package/docs/specs/focus_auto_runner_featureai.md +1102 -0
  143. package/docs/specs/focus_design_v2_featureai.md +197 -0
  144. package/docs/specs/focus_sync_v2_featureai.md +202 -0
  145. package/docs/specs/git_integration_overhaul_featureai.md +326 -0
  146. package/docs/specs/glm_zai_provider_featureai.md +200 -0
  147. package/docs/specs/googlecli_adoption_featureai.md +661 -0
  148. package/docs/specs/help_commands_rewrite_featureai.md +860 -0
  149. package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
  150. package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
  151. package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
  152. package/docs/specs/investigations/README.md +31 -0
  153. package/docs/specs/investigations/digests/2026-09-28.md +47 -0
  154. package/docs/specs/investigations/digests/2026-10-08.md +62 -0
  155. package/docs/specs/investigations/digests/2026-10-10.md +61 -0
  156. package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
  157. package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
  158. package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
  159. package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
  160. package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
  161. package/docs/specs/lifecycle_completeness_featureai.md +754 -0
  162. package/docs/specs/local-first-ci.md +42 -0
  163. package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
  164. package/docs/specs/market-delta-2026-09-superplan.md +310 -0
  165. package/docs/specs/market-ideas-2026-09.md +131 -0
  166. package/docs/specs/market-ideas-2026-10.md +428 -0
  167. package/docs/specs/market-ideas-ledger.md +177 -0
  168. package/docs/specs/memory-optimization-2026-10.md +285 -0
  169. package/docs/specs/multi-track-planning-roots.md +143 -0
  170. package/docs/specs/multi_model_routing_featureai.md +687 -0
  171. package/docs/specs/multi_model_routing_superplan.md +197 -0
  172. package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
  173. package/docs/specs/opus_47_extended_features_featureai.md +363 -0
  174. package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
  175. package/docs/specs/pan_focus_commands_featureai.md +2336 -0
  176. package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
  177. package/docs/specs/product-excellence-superplan.md +315 -0
  178. package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
  179. package/docs/specs/production_deployment_readiness_featureai.md +433 -0
  180. package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
  181. package/docs/specs/real_test_enforcement_featureai.md +213 -0
  182. package/docs/specs/reality-check-2026-09-superplan.md +362 -0
  183. package/docs/specs/right-sized-ceremony.md +74 -0
  184. package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
  185. package/docs/specs/self_improvement_loop_featureai.md +875 -0
  186. package/docs/specs/skill-aligned-decomposition.md +155 -0
  187. package/docs/specs/standards_integration_featureai.md +833 -0
  188. package/docs/specs/testing-system-redesign-2026-09.md +267 -0
  189. package/docs/specs/user_reality_testing_system_featureai.md +788 -0
  190. package/docs/specs/watch-triggers.json +22 -0
  191. package/docs/specs/workflow-integration-redesign.md +742 -0
  192. package/docs/specs/workflow_state_consistency_featureai.md +183 -0
  193. package/hooks/dist/pan-context-monitor.js +9 -2
  194. package/hooks/dist/pan-cost-logger.js +5 -0
  195. package/hooks/dist/pan-guard.js +28 -5
  196. package/hooks/dist/pan-stop-guard.js +9 -9
  197. package/hooks/dist/pan-trace-logger.js +5 -0
  198. package/package.json +10 -2
  199. package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
  200. package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
  201. package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
  202. package/pan-wizard-core/bin/lib/ci.cjs +143 -0
  203. package/pan-wizard-core/bin/lib/commands.cjs +1 -1
  204. package/pan-wizard-core/bin/lib/config.cjs +31 -2
  205. package/pan-wizard-core/bin/lib/constants.cjs +27 -1
  206. package/pan-wizard-core/bin/lib/core.cjs +22 -10
  207. package/pan-wizard-core/bin/lib/cost.cjs +14 -4
  208. package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
  209. package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
  210. package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
  211. package/pan-wizard-core/bin/lib/findings.cjs +34 -4
  212. package/pan-wizard-core/bin/lib/focus.cjs +1 -1
  213. package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
  214. package/pan-wizard-core/bin/lib/init.cjs +7 -2
  215. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
  216. package/pan-wizard-core/bin/lib/merge.cjs +50 -0
  217. package/pan-wizard-core/bin/lib/mutate.cjs +16 -5
  218. package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
  219. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  220. package/pan-wizard-core/bin/lib/phase.cjs +48 -4
  221. package/pan-wizard-core/bin/lib/plan-lint.cjs +150 -1
  222. package/pan-wizard-core/bin/lib/preview.cjs +23 -4
  223. package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
  224. package/pan-wizard-core/bin/lib/research-lint.cjs +150 -3
  225. package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
  226. package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
  227. package/pan-wizard-core/bin/lib/runner.cjs +2 -16
  228. package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
  229. package/pan-wizard-core/bin/lib/squads.cjs +1 -1
  230. package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
  231. package/pan-wizard-core/bin/lib/uat.cjs +149 -0
  232. package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
  233. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
  234. package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
  235. package/pan-wizard-core/bin/lib/verify-red.cjs +37 -1
  236. package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
  237. package/pan-wizard-core/bin/lib/verify.cjs +59 -4
  238. package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
  239. package/pan-wizard-core/bin/pan-tools.cjs +168 -27
  240. package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
  241. package/pan-wizard-core/references/continuation-format.md +3 -3
  242. package/pan-wizard-core/references/guardrails.md +1 -1
  243. package/pan-wizard-core/references/model-profiles.md +2 -3
  244. package/pan-wizard-core/references/questioning.md +24 -0
  245. package/pan-wizard-core/references/ui-brand.md +4 -6
  246. package/pan-wizard-core/references/verdict-contract.md +2 -1
  247. package/pan-wizard-core/templates/config.json +5 -1
  248. package/pan-wizard-core/templates/debug.md +1 -0
  249. package/pan-wizard-core/templates/discovery.md +2 -2
  250. package/pan-wizard-core/templates/requirements.md +9 -0
  251. package/pan-wizard-core/workflows/add-phase.md +4 -6
  252. package/pan-wizard-core/workflows/assumptions.md +19 -1
  253. package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
  254. package/pan-wizard-core/workflows/discuss-phase.md +4 -2
  255. package/pan-wizard-core/workflows/exec-lean.md +69 -0
  256. package/pan-wizard-core/workflows/exec-phase.md +36 -8
  257. package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
  258. package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
  259. package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
  260. package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
  261. package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
  262. package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
  263. package/pan-wizard-core/workflows/health.md +6 -0
  264. package/pan-wizard-core/workflows/help.md +31 -53
  265. package/pan-wizard-core/workflows/insert-phase.md +4 -6
  266. package/pan-wizard-core/workflows/learn.md +3 -3
  267. package/pan-wizard-core/workflows/milestone-audit.md +2 -2
  268. package/pan-wizard-core/workflows/milestone-new.md +38 -95
  269. package/pan-wizard-core/workflows/new-project.md +96 -258
  270. package/pan-wizard-core/workflows/optimize.md +107 -5
  271. package/pan-wizard-core/workflows/phase-tests.md +10 -0
  272. package/pan-wizard-core/workflows/plan-phase.md +86 -43
  273. package/pan-wizard-core/workflows/profile.md +50 -0
  274. package/pan-wizard-core/workflows/progress.md +10 -0
  275. package/pan-wizard-core/workflows/quick.md +34 -43
  276. package/pan-wizard-core/workflows/research-phase.md +118 -40
  277. package/pan-wizard-core/workflows/resume-project.md +18 -2
  278. package/pan-wizard-core/workflows/settings.md +21 -24
  279. package/pan-wizard-core/workflows/todo-check.md +6 -2
  280. package/pan-wizard-core/workflows/transition.md +22 -4
  281. package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
  282. package/pan-wizard-core/workflows/update.md +1 -1
  283. package/pan-wizard-core/workflows/verify-phase.md +2 -2
  284. package/scripts/build-agent-plugin.js +35 -15
  285. package/scripts/build-plugin.js +10 -1
  286. package/scripts/deprecate-old-versions.js +70 -3
  287. package/scripts/generate-skills-docs.py +1 -1
  288. package/scripts/git-hooks/pre-push +27 -0
  289. package/scripts/install-git-hooks.js +7 -5
  290. package/scripts/install-pre-push.cjs +38 -0
  291. package/scripts/land.cjs +151 -0
  292. package/scripts/local-ci.cjs +501 -0
  293. package/scripts/local-schedule.cjs +277 -0
  294. package/scripts/prompt-lint.cjs +32 -0
  295. package/scripts/prompt-rules.cjs +156 -0
  296. package/scripts/run-tests.cjs +7 -1
  297. package/scripts/test-quality-lint.cjs +3 -3
  298. package/scripts/test-surface.cjs +9 -2
  299. package/agents/pan-research-synthesizer.md +0 -246
  300. package/commands/pan/audit-deployment.md +0 -386
  301. package/commands/pan/dashboard.md +0 -22
  302. package/commands/pan/discord.md +0 -19
  303. package/commands/pan/mcp-bridge.md +0 -143
  304. package/commands/pan/milestone-cleanup.md +0 -19
  305. package/commands/pan/milestone-gaps.md +0 -35
  306. package/commands/pan/optimize.md +0 -115
  307. package/commands/pan/profile.md +0 -76
  308. package/commands/pan/research-phase.md +0 -189
  309. package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
  310. package/pan-wizard-core/templates/research-project/architecture.md +0 -210
  311. package/pan-wizard-core/templates/research-project/features.md +0 -153
  312. package/pan-wizard-core/templates/research-project/pitfalls.md +0 -206
  313. package/pan-wizard-core/templates/research-project/stack.md +0 -126
  314. package/pan-wizard-core/templates/research-project/summary.md +0 -170
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+ /**
3
+ * One next-action router (program L7): `pan-tools next-action`.
4
+ *
5
+ * progress, resume and transition each kept their own routing table over the same
6
+ * facts. They now ask this verb, which reads the disk (roadmap.md and the phase
7
+ * directories) into the snapshot the MCP orchestrator's state machine
8
+ * (mcp/orchestrator.cjs `nextAction`) keys on, and maps its decision to the command
9
+ * that carries it out.
10
+ *
11
+ * Run facts on disk: a phase is `verified` when it has a verification file, and
12
+ * finished (`merged` in the state machine's words) when its roadmap checkbox is ticked,
13
+ * which `phase complete` does. Two interactive routes come before the machine: diagnosed
14
+ * UAT gaps are planned first, and a project between milestones (roadmap but no
15
+ * requirements.md) starts the next one.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { output } = require('./core.cjs');
21
+ const { planningPath } = require('./utils.cjs');
22
+ const { enumerateRoadmapPhases, enrichPhaseWithDiskStatus } = require('./roadmap.cjs');
23
+ const { nextAction } = require('../../mcp/orchestrator.cjs');
24
+
25
+ function escapeRegex(s) {
26
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
+ }
28
+
29
+ function phaseDir(cwd, number) {
30
+ const root = path.join(planningPath(cwd), 'phases');
31
+ const num = String(number);
32
+ const padded = num.includes('.') ? num : num.padStart(2, '0');
33
+ try {
34
+ const d = fs.readdirSync(root, { withFileTypes: true })
35
+ .find((e) => e.isDirectory() && (e.name === padded || e.name.startsWith(`${padded}-`) || e.name.startsWith(`${num}-`)));
36
+ return d ? path.join(root, d.name) : null;
37
+ } catch { return null; }
38
+ }
39
+
40
+ function listDir(dir) {
41
+ try { return fs.readdirSync(dir); } catch { return []; }
42
+ }
43
+
44
+ /**
45
+ * The state machine's snapshot, read from disk.
46
+ * @returns {{phases: Array<{number, status, verified, merged, uat_gaps}>, roadmap: boolean, requirements: boolean}}
47
+ */
48
+ function diskSnapshot(cwd) {
49
+ const planning = planningPath(cwd);
50
+ let content = null;
51
+ try { content = fs.readFileSync(path.join(planning, 'roadmap.md'), 'utf8').replace(/\r\n/g, '\n'); } catch {}
52
+ const requirements = fs.existsSync(path.join(planning, 'requirements.md'));
53
+ if (content === null) return { phases: [], roadmap: false, requirements };
54
+ const phases = enumerateRoadmapPhases(content).map((raw) => {
55
+ const p = enrichPhaseWithDiskStatus(cwd, raw);
56
+ const dir = phaseDir(cwd, p.number);
57
+ const files = dir ? listDir(dir) : [];
58
+ const ticked = new RegExp(`-\\s*\\[x\\]\\s*.*Phase\\s+${escapeRegex(p.number)}\\b`, 'i').test(content);
59
+ const uatGaps = files.filter((f) => /uat\.md$/i.test(f)).some((f) => {
60
+ try { return /^status:\s*diagnosed\s*$/m.test(fs.readFileSync(path.join(dir, f), 'utf8')); } catch { return false; }
61
+ });
62
+ return {
63
+ number: p.number,
64
+ status: p.disk_status === 'no_directory' ? 'empty' : p.disk_status,
65
+ verified: files.some((f) => f === 'verification.md' || f.endsWith('-verification.md')),
66
+ merged: ticked,
67
+ uat_gaps: uatGaps,
68
+ };
69
+ });
70
+ return { phases, roadmap: true, requirements };
71
+ }
72
+
73
+ const COMMANDS = {
74
+ plan: (n) => `/pan:plan-phase ${n}`,
75
+ execute: (n) => `/pan:exec-phase ${n}`,
76
+ verify: (n) => `/pan:verify-phase ${n}`,
77
+ complete_phase: (n) => `pan-tools phase complete ${n}`,
78
+ plan_gaps: (n) => `/pan:plan-phase ${n} --gaps`,
79
+ audit_milestone: () => '/pan:milestone-audit',
80
+ new_milestone: () => '/pan:milestone-new',
81
+ new_project: () => '/pan:new-project',
82
+ };
83
+
84
+ /**
85
+ * Decide the next step for the project at `cwd`.
86
+ * @returns {{action: string, phase: string|null, reason: string, command: string|null, done: boolean}}
87
+ */
88
+ function routeNextAction(cwd) {
89
+ const snap = diskSnapshot(cwd);
90
+ const result = (action, phase, reason, done = false) => ({
91
+ action, phase: phase == null ? null : String(phase), reason,
92
+ command: COMMANDS[action] ? COMMANDS[action](phase) : null, done,
93
+ });
94
+ if (!snap.roadmap) return result('new_project', null, 'no_roadmap');
95
+ if (!snap.requirements) return result('new_milestone', null, 'between_milestones');
96
+
97
+ const open = snap.phases.find((p) => !(p.verified && p.merged));
98
+ if (open && open.uat_gaps) return result('plan_gaps', open.number, 'uat_gaps_diagnosed');
99
+
100
+ const d = nextAction({ phases: snap.phases });
101
+ if (d.action === 'request_merge') return result('complete_phase', d.args.phase, d.reason);
102
+ if (d.action === 'stop' && d.reason === 'all_complete') return result('audit_milestone', null, 'all_phases_complete');
103
+ if (d.action === 'stop') return result('stop', null, d.reason, d.done);
104
+ return result(d.action, d.args && d.args.phase, d.reason);
105
+ }
106
+
107
+ function cmdNextAction(cwd, raw) {
108
+ const r = routeNextAction(cwd);
109
+ output(r, raw, r.command || r.action);
110
+ }
111
+
112
+ module.exports = { diskSnapshot, routeNextAction, cmdNextAction };
@@ -2,7 +2,7 @@
2
2
  // optimize.cjs — Circular optimization loop: trace → learn → apply → repeat.
3
3
  //
4
4
  // Every agent spawn is logged to a trace session. After a build, /pan:learn
5
- // invokes pan-optimizer to analyze the trace. /pan:optimize apply writes memory
5
+ // invokes pan-optimizer to analyze the trace. /pan:learn --optimize apply writes memory
6
6
  // entries, config notes, and prompt suggestions back into the project, making
7
7
  // the next run smarter.
8
8
 
@@ -1044,7 +1044,7 @@ function revertApply(cwd, which) {
1044
1044
  const LESSON_TARGET = "the project's instructions (CLAUDE.md or AGENTS.md, outside PAN's section)";
1045
1045
 
1046
1046
  // Derive suggestion notes from a raw JSON analysis when no optimizer agent
1047
- // report is available (fallback for /pan:optimize apply on a JSON file).
1047
+ // report is available (fallback for /pan:learn --optimize apply on a JSON file).
1048
1048
  function deriveActionsFromAnalysis(analysis) {
1049
1049
  const actions = [];
1050
1050
  const { memory_miss_patterns, gap_patterns, summary } = analysis;
@@ -278,9 +278,9 @@ function cmdPhasePlanIndex(cwd, phase, raw, opts = {}) {
278
278
  const planFiles = filterPlanFiles(phaseFiles);
279
279
  const summaryFiles = filterSummaryFiles(phaseFiles);
280
280
 
281
- const { plans, waves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
281
+ const { plans, waves, subWaves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
282
282
 
283
- const result = { phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints };
283
+ const result = { phase: normalized, plans, waves, sub_waves: subWaves, incomplete, has_checkpoints: hasCheckpoints };
284
284
  // `--failed <ids>` (market-ideas M28): the plans that must not run after those
285
285
  // failed, because a dependency, direct or transitive, is among them.
286
286
  if (opts.failed) {
@@ -336,6 +336,35 @@ function blockedPlans(plans, failedIds) {
336
336
  * @param {string[]} summaryFiles - List of summary filenames
337
337
  * @returns {{plans: Array, waves: Object, incomplete: string[], hasCheckpoints: boolean}}
338
338
  */
339
+ /**
340
+ * Split one wave's plans into sub-waves so no two plans in a sub-wave modify the same
341
+ * file (program A3). Greedy, in plan order: each plan joins the first sub-wave it does
342
+ * not overlap. A plan that declares no files may touch anything, so it runs alone.
343
+ * @param {Array<{id: string, files_modified?: string[]}>} plans
344
+ * @returns {string[][]} plan ids per sub-wave
345
+ */
346
+ function partitionWave(plans) {
347
+ const subs = [];
348
+ const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '');
349
+ for (const p of plans) {
350
+ const files = (p.files_modified || []).map(norm);
351
+ const solo = files.length === 0;
352
+ let placed = false;
353
+ if (!solo) {
354
+ for (const sub of subs) {
355
+ if (sub.solo) continue;
356
+ if (files.some((x) => sub.files.has(x))) continue;
357
+ sub.ids.push(p.id);
358
+ for (const x of files) sub.files.add(x);
359
+ placed = true;
360
+ break;
361
+ }
362
+ }
363
+ if (!placed) subs.push({ ids: [p.id], files: new Set(files), solo });
364
+ }
365
+ return subs.map((x) => x.ids);
366
+ }
367
+
339
368
  function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
340
369
  const completedPlanIds = new Set(
341
370
  summaryFiles.map(summaryFile => summaryFile.replace('-summary.md', '').replace('summary.md', ''))
@@ -401,7 +430,14 @@ function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
401
430
  waves[waveKey].push(planId);
402
431
  }
403
432
 
404
- return { plans, waves, incomplete, hasCheckpoints };
433
+ // A3: within a wave, plans that share a file run in different sub-waves.
434
+ const subWaves = {};
435
+ for (const [w, ids] of Object.entries(waves)) {
436
+ subWaves[w] = partitionWave(ids.map((id) => plans.find((p) => p.id === id)));
437
+ subWaves[w].forEach((sub, i) => { for (const id of sub) plans.find((p) => p.id === id).sub_wave = i; });
438
+ }
439
+
440
+ return { plans, waves, subWaves, incomplete, hasCheckpoints };
405
441
  }
406
442
 
407
443
  /**
@@ -741,8 +777,14 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
741
777
  const reqIds = roadmapRequirementIds(roadmapContent, phaseNum);
742
778
  if (reqIds.length === 0) return;
743
779
  let reqContent = reqReadContent;
780
+ // V8: a requirement with EARS criteria is ticked only when its mapped tests pass;
781
+ // a legacy one (no criteria) keeps the old tick. Held ones are reported.
782
+ const decisions = require('./requirements-lint.cjs').tickDecisions(cwd, reqIds);
783
+ const held = [];
744
784
 
745
785
  for (const reqId of reqIds) {
786
+ const d = decisions.get(reqId);
787
+ if (d && !d.allowed) { held.push({ id: reqId, status: d.status }); continue; }
746
788
  const reqEscaped = escapeRegex(reqId);
747
789
  reqContent = reqContent.replace(
748
790
  new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
@@ -759,7 +801,7 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
759
801
  } catch (e) {
760
802
  return { updated: false, error: 'Failed to write requirements.md: ' + e.message };
761
803
  }
762
- return { updated: true };
804
+ return { updated: true, held };
763
805
  } catch {
764
806
  // requirements.md not found or unreadable — skip
765
807
  return { updated: false };
@@ -1027,6 +1069,7 @@ function classifyPlanTier(frontmatter, config) {
1027
1069
  }
1028
1070
 
1029
1071
  module.exports = {
1072
+ partitionWave,
1030
1073
  cmdPhasesList,
1031
1074
  cmdPhaseNextDecimal,
1032
1075
  cmdFindPhase,
@@ -1044,6 +1087,7 @@ module.exports = {
1044
1087
  renumberIntegerPhases,
1045
1088
  updateRoadmapAfterRemoval,
1046
1089
  markPhaseCompleteInRoadmap,
1090
+ markRequirementsCompleteForPhase,
1047
1091
  updateStateAfterPhaseComplete,
1048
1092
  classifyPlanTier,
1049
1093
  };
@@ -144,6 +144,152 @@ function checkDecisions(cwd, phase) {
144
144
 
145
145
  // ─── CLI ─────────────────────────────────────────────────────────────────────
146
146
 
147
+ // ─── Gate baselines (M48) ─────────────────────────────────────────────────────
148
+ //
149
+ // A plan's `<verify>` often names a grep or count gate. One that already passes
150
+ // before any work proves nothing about the task; one whose pattern cannot compile
151
+ // can never match (gsd-core v1.18.0 "gate baselines"). The simple grep forms are
152
+ // interpreted here, in Node, never through a shell: the same on every platform,
153
+ // and nothing a plan writes is executed. Anything else is listed as not checkable.
154
+
155
+ const GATE_TOOLS = [['git', 'grep'], ['grep'], ['rg']];
156
+ const GATE_FLAGS = new Set(['r', 'R', 'i', 'c', 'E', 'F', 'w', 'q', 'l', 'n', 's', 'H', 'h']);
157
+ const REGRESSION_GUARD = /regression guard|already passes|must still pass/i;
158
+
159
+ /** Split a command line on spaces, honouring quotes. Null on shell syntax. */
160
+ function shellWords(cmd) {
161
+ const words = [];
162
+ let cur = '';
163
+ let quote = null;
164
+ let started = false;
165
+ for (const ch of cmd) {
166
+ if (quote) {
167
+ if (ch === quote) quote = null; else cur += ch;
168
+ continue;
169
+ }
170
+ if (ch === '"' || ch === "'") { quote = ch; started = true; continue; }
171
+ if (/[|;&<>`$()\\]/.test(ch)) return null;
172
+ if (/\s/.test(ch)) { if (started) { words.push(cur); cur = ''; started = false; } continue; }
173
+ cur += ch; started = true;
174
+ }
175
+ if (quote) return null;
176
+ if (started) words.push(cur);
177
+ return words;
178
+ }
179
+
180
+ /**
181
+ * Parse a simple grep gate: `grep|rg|git grep [flags] PATTERN [paths...]`.
182
+ * @returns {{tool, flags, pattern, paths}|null} null when the command is not one
183
+ */
184
+ function parseGateCommand(cmd) {
185
+ const words = shellWords(String(cmd || '').trim());
186
+ if (!words || !words.length) return null;
187
+ const tool = GATE_TOOLS.find((t) => t.every((w, i) => words[i] === w));
188
+ if (!tool) return null;
189
+ const flags = {};
190
+ let pattern = null;
191
+ const paths = [];
192
+ let flagsDone = false;
193
+ for (const w of words.slice(tool.length)) {
194
+ if (!flagsDone && w === '--') { flagsDone = true; continue; }
195
+ if (!flagsDone && /^-[A-Za-z]+$/.test(w)) {
196
+ for (const f of w.slice(1)) { if (!GATE_FLAGS.has(f)) return null; flags[f] = true; }
197
+ continue;
198
+ }
199
+ if (!flagsDone && w.startsWith('-')) return null;
200
+ if (pattern === null) pattern = w; else paths.push(w);
201
+ }
202
+ if (pattern === null) return null;
203
+ return { tool: tool.join(' '), flags, pattern, paths };
204
+ }
205
+
206
+ /** A JS RegExp for a grep pattern: -F literal, -E extended, else basic (BRE). Null when it cannot compile. */
207
+ function gateRegex(g) {
208
+ let src = g.pattern;
209
+ if (g.flags.F) src = src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
210
+ else if (!g.flags.E && g.tool !== 'rg') {
211
+ // BRE: \( \) \{ \} \| \+ \? are the operators; their bare forms are literals.
212
+ src = src.replace(/\\([(){}|+?])|([(){}|+?])/g, (m, esc, bare) => (esc ? esc : '\\' + bare));
213
+ }
214
+ if (g.flags.w) src = `\\b(?:${src})\\b`;
215
+ try { return new RegExp(src, g.flags.i ? 'i' : ''); } catch { return null; }
216
+ }
217
+
218
+ function filesUnder(root, rel, recursive) {
219
+ const abs = path.join(root, rel);
220
+ let st;
221
+ try { st = fs.statSync(abs); } catch { return null; }
222
+ if (st.isFile()) return [abs];
223
+ if (!st.isDirectory() || !recursive) return [];
224
+ const out = [];
225
+ const walk = (d) => {
226
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
227
+ if (e.name === '.git' || e.name === 'node_modules') continue;
228
+ const p = path.join(d, e.name);
229
+ if (e.isDirectory()) walk(p); else if (e.isFile()) out.push(p);
230
+ }
231
+ };
232
+ walk(abs);
233
+ return out;
234
+ }
235
+
236
+ /** Run a parsed gate against the tree as it is: passes_now | fails_now | target_missing | invalid. */
237
+ function runGate(cwd, g) {
238
+ const re = gateRegex(g);
239
+ if (!re) return 'invalid';
240
+ const recursive = g.tool !== 'grep' || g.flags.r || g.flags.R;
241
+ const targets = g.paths.length ? g.paths : ['.'];
242
+ let anyTarget = false;
243
+ for (const t of targets) {
244
+ const files = filesUnder(cwd, t, recursive);
245
+ if (files === null) continue;
246
+ anyTarget = true;
247
+ for (const f of files) {
248
+ let text;
249
+ try { text = toLf(fs.readFileSync(f, 'utf-8')); } catch { continue; }
250
+ if (text.split('\n').some((line) => re.test(line))) return 'passes_now';
251
+ }
252
+ }
253
+ return anyTarget ? 'fails_now' : 'target_missing';
254
+ }
255
+
256
+ /** The gate commands in a plan: backticked spans inside each task's <verify>. */
257
+ function gatesOf(text) {
258
+ const out = [];
259
+ const taskRe = /<task\b[^>]*>([\s\S]*?)<\/task>/g;
260
+ let t;
261
+ while ((t = taskRe.exec(text)) !== null) {
262
+ const name = ((t[1].match(/<name>([\s\S]*?)<\/name>/) || [])[1] || 'unnamed').trim();
263
+ const verify = (t[1].match(/<verify>([\s\S]*?)<\/verify>/) || [])[1] || '';
264
+ for (const m of verify.matchAll(/`([^`]+)`/g)) out.push({ task: name, command: m[1].trim(), guard: REGRESSION_GUARD.test(verify) });
265
+ }
266
+ return out;
267
+ }
268
+
269
+ function checkGates(cwd, phase) {
270
+ const pp = phasePlans(cwd, phase);
271
+ if (pp.error) return pp;
272
+ const gates = [];
273
+ const errors = [];
274
+ for (const p of pp.plans) {
275
+ for (const g of gatesOf(p.text)) {
276
+ const parsed = parseGateCommand(g.command);
277
+ const status = parsed ? runGate(cwd, parsed) : 'not_checkable';
278
+ gates.push({ plan: p.plan, task: g.task, command: g.command, status, ...(g.guard ? { regression_guard: true } : {}) });
279
+ if (status === 'passes_now' && !g.guard) errors.push({ code: 'gate_vacuous', plan: p.plan, task: g.task, command: g.command, detail: 'passes before any work, so it cannot show the task did anything; check for what the task adds, or mark it a regression guard' });
280
+ if (status === 'invalid') errors.push({ code: 'gate_invalid', plan: p.plan, task: g.task, command: g.command, detail: 'the pattern does not compile, so the gate can never match as written' });
281
+ }
282
+ }
283
+ return { phase: pp.pf.info.phase_number, ok: errors.length === 0, gates, errors };
284
+ }
285
+
286
+ function cmdVerifyGates(cwd, phase, raw) {
287
+ if (!phase) error('Usage: verify gates <phase>');
288
+ const r = checkGates(cwd, phase);
289
+ if (r.error) { output(r, raw); return; }
290
+ output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
291
+ }
292
+
147
293
  function cmdVerifyReviewFocus(cwd, phase, raw) {
148
294
  if (!phase) error('Usage: verify review-focus <phase>');
149
295
  const r = checkReviewFocus(cwd, phase);
@@ -158,4 +304,7 @@ function cmdVerifyDecisions(cwd, phase, raw) {
158
304
  output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
159
305
  }
160
306
 
161
- module.exports = { REVIEW_FOCUS_MAX, checkReviewFocus, checkDecisions, recordedDecisions, cmdVerifyReviewFocus, cmdVerifyDecisions };
307
+ module.exports = {
308
+ REVIEW_FOCUS_MAX, checkReviewFocus, checkDecisions, recordedDecisions, cmdVerifyReviewFocus, cmdVerifyDecisions,
309
+ parseGateCommand, checkGates, cmdVerifyGates,
310
+ };
@@ -371,7 +371,7 @@ function buildMilestoneETA(cwd) {
371
371
  const remaining = phaseList.filter(p => !p.completed);
372
372
 
373
373
  // Sample phase durations from summary frontmatter or commit metadata.
374
- const durations = sampleCompletedPhaseDurations(cwd, completed);
374
+ const { durations, sources } = sampleCompletedPhaseDurations(cwd, completed, { withSources: true });
375
375
  const avg = durations.length > 0
376
376
  ? durations.reduce((a, b) => a + b, 0) / durations.length
377
377
  : 5.0; // default 5 days per phase when no history
@@ -408,6 +408,9 @@ function buildMilestoneETA(cwd) {
408
408
  sample_size: durations.length,
409
409
  eta_date: etaDate,
410
410
  confidence_pct: confidence,
411
+ // L6b: where the average came from — dated summaries, the phases' plan commits, or
412
+ // the 5-day default when there is no history at all.
413
+ eta_source: durations.length === 0 ? 'default' : (sources.every((s) => s === 'git') ? 'git' : (sources.every((s) => s === 'summary') ? 'summary' : 'mixed')),
411
414
  bottleneck,
412
415
  };
413
416
  }
@@ -420,8 +423,9 @@ function findPhaseDir(dirs, num) {
420
423
  return dirs.find(d => d.startsWith(`${num}-`) || d.startsWith(`${padded}-`));
421
424
  }
422
425
 
423
- function sampleCompletedPhaseDurations(cwd, completedPhases) {
426
+ function sampleCompletedPhaseDurations(cwd, completedPhases, opts = {}) {
424
427
  const durations = [];
428
+ const sources = [];
425
429
  const phasesRoot = phasesPath(cwd);
426
430
  let dirs = [];
427
431
  try { dirs = fs.readdirSync(phasesRoot); } catch { return durations; }
@@ -441,10 +445,25 @@ function sampleCompletedPhaseDurations(cwd, completedPhases) {
441
445
  const completed_at = fm.completed || fm.finished || fm.done;
442
446
  if (started && completed_at) {
443
447
  const d = daysBetween(started, completed_at);
444
- if (d && d > 0 && d < 60) durations.push(d);
448
+ if (d && d > 0 && d < 60) { durations.push(d); sources.push('summary'); continue; }
445
449
  }
450
+ // No dated summary (most have none): time the phase by its plan commits, oldest to newest.
451
+ const g = phaseCommitSpanDays(cwd, p.num);
452
+ if (g && g > 0 && g < 60) { durations.push(g); sources.push('git'); }
446
453
  }
447
- return durations;
454
+ return opts.withSources ? { durations, sources } : durations;
455
+ }
456
+
457
+ /** Days between a phase's oldest and newest plan commit ({type}({phase}-{plan}): …), or null. */
458
+ function phaseCommitSpanDays(cwd, phaseNum) {
459
+ const n = String(phaseNum);
460
+ const padded = n.includes('.') ? n.split('.').map((s, i) => (i === 0 ? s.padStart(2, '0') : s)).join('.') : n.padStart(2, '0');
461
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
462
+ const r = require('./core.cjs').execGit(cwd, ['log', '--format=%ct', '-E', `--grep=^[a-z]+\\((${esc(padded)}|${esc(n)})-[0-9]+\\):`]);
463
+ if (!r || r.exitCode !== 0 || !r.stdout) return null;
464
+ const times = r.stdout.split('\n').map(Number).filter(Number.isFinite);
465
+ if (times.length < 2) return null;
466
+ return Math.round(((Math.max(...times) - Math.min(...times)) / 86400) * 10) / 10;
448
467
  }
449
468
 
450
469
  function findBottleneckCandidate(cwd, remainingPhases) {
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+ /**
3
+ * Requirements with EARS criteria, a requirements lint and a truth→test trace matrix
4
+ * (program V8).
5
+ *
6
+ * A v1 requirement (a checkbox line `- [ ] **ID**: …`) carries its acceptance criteria
7
+ * as indented sub-bullets in an EARS form, and the tests that check them as `Test:`
8
+ * sub-bullets:
9
+ *
10
+ * - [ ] **AUTH-01**: User can sign in
11
+ * - WHEN a user submits a correct email and password THE SYSTEM SHALL start a session
12
+ * - Test: tests/auth.test.js
13
+ *
14
+ * `verify requirements` fails a v1 requirement with no criterion (`req_no_scenario`).
15
+ * `verify traceability` maps each requirement to its tests and runs them: `passed`, `failing`,
16
+ * or `not_checked` when a truth has no test (an untested truth is never read as met).
17
+ * `phase complete` ticks a requirement that has criteria only when its trace passes; a
18
+ * legacy requirement with no criteria keeps the old tick, so existing projects work.
19
+ */
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { output, toLf } = require('./core.cjs');
23
+ const { planningPath } = require('./utils.cjs');
24
+ const { REQUIREMENTS_FILE } = require('./constants.cjs');
25
+ const TG = require('./test-gate.cjs');
26
+
27
+ // EARS forms: ubiquitous (THE <system> SHALL), event (WHEN … SHALL), state (WHILE …),
28
+ // optional feature (WHERE …), unwanted behaviour (IF … THEN … SHALL).
29
+ const EARS_RE = /^(?:(?:WHEN|WHILE|WHERE)\b[\s\S]*\bSHALL\b|IF\b[\s\S]*\bTHEN\b[\s\S]*\bSHALL\b|THE\s+\S[\s\S]*\bSHALL\b)/i;
30
+ const REQ_RE = /^- (\[[ xX]\] )?\*\*([A-Z][A-Z0-9]*-\d+)\*\*:?\s*(.*)$/;
31
+ const DEFAULT_TEST_TIMEOUT_MS = 3 * 60 * 1000;
32
+
33
+ const isEars = (s) => EARS_RE.test(String(s || '').trim());
34
+
35
+ /** The requirements in a requirements.md, with criteria and mapped tests. */
36
+ function parseRequirements(raw) {
37
+ const lines = toLf(String(raw || '')).split('\n');
38
+ const out = [];
39
+ let cur = null;
40
+ for (const line of lines) {
41
+ const m = REQ_RE.exec(line);
42
+ if (m) {
43
+ cur = { id: m[2], v1: !!m[1], checked: !!m[1] && /x/i.test(m[1]), text: m[3].trim(), criteria: [], tests: [] };
44
+ out.push(cur);
45
+ continue;
46
+ }
47
+ if (!cur) continue;
48
+ const sub = /^\s{2,}- (.*)$/.exec(line);
49
+ if (!sub) { if (line.trim() !== '') cur = null; continue; }
50
+ const body = sub[1].trim();
51
+ const t = /^tests?:\s*(.+)$/i.exec(body);
52
+ if (t) { for (const p of t[1].split(',')) { const file = p.trim().replace(/`/g, '').split('#')[0].trim(); if (file) cur.tests.push(file); } continue; }
53
+ if (isEars(body)) cur.criteria.push(body);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function readRequirements(cwd) {
59
+ try { return fs.readFileSync(path.join(planningPath(cwd), REQUIREMENTS_FILE), 'utf-8'); } catch { return null; }
60
+ }
61
+
62
+ function lintRequirements(cwd) {
63
+ const text = readRequirements(cwd);
64
+ if (text === null) return { error: 'requirements_not_found', reason: '.planning/requirements.md is missing' };
65
+ const reqs = parseRequirements(text).filter((r) => r.v1);
66
+ const errors = reqs.filter((r) => r.criteria.length === 0).map((r) => ({ code: 'req_no_scenario', id: r.id, detail: 'no acceptance criterion in an EARS form (WHEN/WHILE/WHERE/IF … THE SYSTEM SHALL …)' }));
67
+ return { ok: errors.length === 0, requirements: reqs.map((r) => ({ id: r.id, criteria: r.criteria.length, tests: r.tests.length })), errors };
68
+ }
69
+
70
+ /** Run a requirement's mapped tests: passed | failing | not_checked | legacy. Pure over the cache. */
71
+ function traceOf(cwd, req, cache, opts = {}) {
72
+ if (req.criteria.length === 0) return { id: req.id, status: 'legacy', tests: req.tests };
73
+ if (req.tests.length === 0) return { id: req.id, status: 'not_checked', tests: [], detail: 'no test is mapped to this requirement' };
74
+ const results = [];
75
+ for (const file of req.tests) {
76
+ if (!cache.has(file)) {
77
+ if (!fs.existsSync(path.join(cwd, file))) cache.set(file, { file, result: 'missing' });
78
+ else {
79
+ const fc = TG.fileCommand(cwd, file);
80
+ if (!fc.command) cache.set(file, { file, result: 'not_checked', detail: fc.reason });
81
+ else cache.set(file, { file, result: TG.runCommand(cwd, fc.command, opts.timeoutMs || DEFAULT_TEST_TIMEOUT_MS).exit === 0 ? 'passed' : 'failing' });
82
+ }
83
+ }
84
+ results.push(cache.get(file));
85
+ }
86
+ const status = results.some((r) => r.result === 'failing' || r.result === 'missing') ? 'failing'
87
+ : results.every((r) => r.result === 'passed') ? 'passed' : 'not_checked';
88
+ return { id: req.id, status, tests: results };
89
+ }
90
+
91
+ function traceMatrix(cwd, opts = {}) {
92
+ const text = readRequirements(cwd);
93
+ if (text === null) return { error: 'requirements_not_found', reason: '.planning/requirements.md is missing' };
94
+ let reqs = parseRequirements(text).filter((r) => r.v1);
95
+ if (opts.ids && opts.ids.length) reqs = reqs.filter((r) => opts.ids.includes(r.id));
96
+ const cache = new Map();
97
+ const requirements = reqs.map((r) => traceOf(cwd, r, cache, opts));
98
+ const count = (s) => requirements.filter((r) => r.status === s).length;
99
+ return { requirements, passed: count('passed'), failing: count('failing'), not_checked: count('not_checked'), legacy: count('legacy') };
100
+ }
101
+
102
+ /** Whether `phase complete` may tick each id. Legacy requirements may; the rest need a passing trace. */
103
+ function tickDecisions(cwd, ids) {
104
+ const text = readRequirements(cwd);
105
+ if (text === null) return new Map(ids.map((id) => [id, { allowed: true, status: 'unknown' }]));
106
+ const byId = new Map(parseRequirements(text).map((r) => [r.id, r]));
107
+ const cache = new Map();
108
+ const out = new Map();
109
+ for (const id of ids) {
110
+ const r = byId.get(id);
111
+ if (!r) { out.set(id, { allowed: true, status: 'unknown' }); continue; }
112
+ const t = traceOf(cwd, r, cache);
113
+ out.set(id, { allowed: t.status === 'legacy' || t.status === 'passed', status: t.status });
114
+ }
115
+ return out;
116
+ }
117
+
118
+ function cmdVerifyRequirements(cwd, raw) {
119
+ const r = lintRequirements(cwd);
120
+ if (r.error) { output(r, raw, 'error', 1); return; }
121
+ output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
122
+ }
123
+
124
+ function cmdVerifyTrace(cwd, ids, raw) {
125
+ const r = traceMatrix(cwd, { ids });
126
+ if (r.error) { output(r, raw, 'error', 1); return; }
127
+ output(r, raw, r.failing ? 'failing' : (r.not_checked ? 'not_checked' : 'passed'), r.failing ? 1 : 0);
128
+ }
129
+
130
+ module.exports = { isEars, parseRequirements, lintRequirements, traceMatrix, tickDecisions, cmdVerifyRequirements, cmdVerifyTrace };