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,104 @@
1
+ 'use strict';
2
+ /**
3
+ * Debug sessions as evidence and as a case base (program P5).
4
+ *
5
+ * `debug repro <slug>` runs the reproduction a session recorded (its `## Reproduction`
6
+ * `command:`), so a gap or a debug session closes only when that command turns green,
7
+ * not when an agent says it is fixed. The reproduction may name the requirement it
8
+ * stands for (`requirement: REQ-n`): one reproduction per requirement a gap breaks.
9
+ *
10
+ * `debug similar "<symptom>"` ranks the resolved sessions in `.planning/debug/resolved/`
11
+ * by token overlap with the new symptom (their symptoms, errors and root cause), so a
12
+ * recurring bug starts from the root cause and fix that worked before.
13
+ */
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { output, error, toLf } = require('./core.cjs');
17
+ const TG = require('./test-gate.cjs');
18
+
19
+ const DEBUG_DIR = path.join('.planning', 'debug');
20
+ const RESOLVED_DIR = path.join(DEBUG_DIR, 'resolved');
21
+ const SLUG_RE = /^[a-z0-9][a-z0-9._-]*$/i;
22
+ const DEFAULT_REPRO_TIMEOUT_MS = 2 * 60 * 1000;
23
+ const STOP = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'when', 'after', 'before', 'from', 'into', 'are', 'was', 'not', 'but', 'its', 'every', 'right']);
24
+
25
+ /** The `key: value` lines of one `## Heading` section. */
26
+ function sectionFields(text, heading) {
27
+ const m = new RegExp(`^## ${heading}\\s*$([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm').exec(text);
28
+ const out = {};
29
+ if (!m) return out;
30
+ for (const line of m[1].split('\n')) {
31
+ const kv = /^([a-z_]+):\s*(.*)$/.exec(line.trim());
32
+ if (kv) out[kv[1]] = kv[2].trim();
33
+ }
34
+ return out;
35
+ }
36
+
37
+ /** Parse a debug session file. */
38
+ function parseDebugSession(raw) {
39
+ const text = toLf(String(raw || ''));
40
+ const fm = /^---\n([\s\S]*?)\n---/.exec(text);
41
+ const status = fm ? ((/^status:\s*(.+)$/m.exec(fm[1]) || [])[1] || '').trim() : '';
42
+ return {
43
+ status,
44
+ symptoms: sectionFields(text, 'Symptoms'),
45
+ reproduction: sectionFields(text, 'Reproduction'),
46
+ resolution: sectionFields(text, 'Resolution'),
47
+ };
48
+ }
49
+
50
+ /** Run a session's reproduction. green | red | none (no command recorded). */
51
+ function reproCheck(cwd, slug, opts = {}) {
52
+ if (!slug || !SLUG_RE.test(slug) || slug.includes('..')) return { error: 'invalid_slug', reason: 'a debug session slug is letters, digits, dots, dashes and underscores' };
53
+ const candidates = [path.join(cwd, DEBUG_DIR, `${slug}.md`), path.join(cwd, RESOLVED_DIR, `${slug}.md`)];
54
+ const file = candidates.find((f) => fs.existsSync(f));
55
+ if (!file) return { error: 'session_not_found', reason: `no .planning/debug/${slug}.md` };
56
+ const s = parseDebugSession(fs.readFileSync(file, 'utf8'));
57
+ const command = s.reproduction.command || '';
58
+ const requirement = s.reproduction.requirement || null;
59
+ if (!command || /^none\b/i.test(command) || /^\[/.test(command)) {
60
+ return { slug, result: 'none', closable: false, requirement, detail: 'the session records no reproduction command; its closing needs a human check' };
61
+ }
62
+ const r = TG.runCommand(cwd, command, opts.timeoutMs || DEFAULT_REPRO_TIMEOUT_MS);
63
+ const result = r.exit === 0 ? 'green' : 'red';
64
+ return { slug, result, closable: result === 'green', requirement, command, exit: r.exit, ...(r.timed_out ? { timed_out: true } : {}) };
65
+ }
66
+
67
+ function tokens(s) {
68
+ return new Set(String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(' ').filter((w) => w.length > 2 && !STOP.has(w)));
69
+ }
70
+
71
+ /** Rank resolved sessions by overlap with a symptom. */
72
+ function similarCases(cwd, symptom, opts = {}) {
73
+ const want = tokens(symptom);
74
+ const dir = path.join(cwd, RESOLVED_DIR);
75
+ let files = [];
76
+ try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { /* none yet */ }
77
+ const cases = [];
78
+ for (const f of files) {
79
+ const s = parseDebugSession(fs.readFileSync(path.join(dir, f), 'utf8'));
80
+ const have = tokens([s.symptoms.expected, s.symptoms.actual, s.symptoms.errors, s.resolution.root_cause].join(' '));
81
+ let both = 0;
82
+ for (const t of want) if (have.has(t)) both++;
83
+ const score = want.size && have.size ? both / (want.size + have.size - both) : 0;
84
+ if (score <= 0) continue;
85
+ cases.push({ slug: f.replace(/\.md$/, ''), score: Number(score.toFixed(3)), root_cause: s.resolution.root_cause || null, fix: s.resolution.fix || null, reproduction: s.reproduction.command || null });
86
+ }
87
+ cases.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
88
+ return { symptom, cases: cases.slice(0, opts.limit || 5) };
89
+ }
90
+
91
+ function cmdDebugRepro(cwd, slug, raw, opts = {}) {
92
+ if (!slug) error('Usage: debug repro <slug> [--timeout <ms>]');
93
+ const r = reproCheck(cwd, slug, { timeoutMs: opts.timeout ? Number(opts.timeout) : undefined });
94
+ if (r.error) { output(r, raw, 'error', 1); return; }
95
+ output(r, raw, r.result, r.result === 'green' ? 0 : 1);
96
+ }
97
+
98
+ function cmdDebugSimilar(cwd, symptom, raw, opts = {}) {
99
+ if (!symptom) error('Usage: debug similar "<symptom>" [--limit N]');
100
+ const r = similarCases(cwd, symptom, { limit: opts.limit ? Number(opts.limit) : undefined });
101
+ output(r, raw, r.cases.map((c) => c.slug).join('\n'));
102
+ }
103
+
104
+ module.exports = { parseDebugSession, reproCheck, similarCases, cmdDebugRepro, cmdDebugSimilar };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+ /**
3
+ * The failure-class router (program A4).
4
+ *
5
+ * When an autodev unit fails, what to do next depends on why. classifyFailure reads the
6
+ * exit, the output tail and whether it timed out; routeFailure gives the class's policy
7
+ * for this attempt. Breadth before depth (Olausson et al.): a cheap retry with the error
8
+ * in front of the model comes before a tier escalation, and anything a model cannot fix
9
+ * (a missing CLI or login, a merge conflict, the guard's deny, an undeclared file) goes
10
+ * to a human.
11
+ *
12
+ * retry_with_tail rerun the unit with the error tail in the prompt
13
+ * fresh_restart rerun the unit as it was
14
+ * escalate_tier rerun it at the next model tier (core.cjs escalateTier)
15
+ * quarantine record it as a flake and go on
16
+ * ask stop: human_needed
17
+ * stop_budget stop: budget_usd
18
+ */
19
+
20
+ const { escalateTier } = require('./core.cjs');
21
+
22
+ const FAILURE_CLASSES = Object.freeze(['compile', 'assertion', 'timeout', 'flake', 'missing_env', 'merge_conflict', 'integrity_violation', 'scope_violation', 'budget', 'unknown']);
23
+
24
+ // First match wins: the specific, unfixable classes are checked before the generic ones.
25
+ const PATTERNS = [
26
+ ['budget', /max-budget-usd|credit balance|usage limit|rate limit|quota exceeded|budget exceeded/i],
27
+ ['missing_env', /is not recognized as an internal or external command|command not found|ENOENT|invalid api key|not logged in|please run \/login|authenticat(e|ion) (failed|required)|missing (an )?api key/i],
28
+ ['integrity_violation', /PAN guard:|integrity (check|violation)|hash mismatch|tamper/i],
29
+ ['merge_conflict', /CONFLICT \(|merge conflict|Automatic merge failed/i],
30
+ ['scope_violation', /scope violation|undeclared (file|test)|not declared by any plan|outside the plan/i],
31
+ ['timeout', /ETIMEDOUT|timed out|timeout of \d+/i],
32
+ ['compile', /error TS\d+|SyntaxError|ReferenceError|Cannot find (module|name)|compilation failed|failed to compile|cannot compile/i],
33
+ ['assertion', /AssertionError|assert\w*\.\w+|expected .{1,80} (to|but)|\btests? failed\b|\d+ failing\b|^not ok /im],
34
+ ];
35
+
36
+ /**
37
+ * @param {{exit?: number|null, stderr?: string, stdout?: string, timedOut?: boolean, flaky?: boolean}} f
38
+ * @returns {string} one of FAILURE_CLASSES
39
+ */
40
+ function classifyFailure(f = {}) {
41
+ if (f.flaky) return 'flake';
42
+ if (f.timedOut) return 'timeout';
43
+ const text = `${f.stderr || ''}\n${f.stdout || ''}`;
44
+ for (const [cls, re] of PATTERNS) if (re.test(text)) return cls;
45
+ if (f.exit === 127) return 'missing_env';
46
+ return 'unknown';
47
+ }
48
+
49
+ const POLICIES = {
50
+ compile: ['retry_with_tail', 'escalate_tier'],
51
+ assertion: ['retry_with_tail', 'escalate_tier'],
52
+ timeout: ['fresh_restart'],
53
+ flake: ['quarantine'],
54
+ missing_env: [],
55
+ merge_conflict: [],
56
+ integrity_violation: [],
57
+ scope_violation: [],
58
+ budget: ['stop_budget'],
59
+ unknown: ['fresh_restart'],
60
+ };
61
+
62
+ /**
63
+ * The policy for the `attempt`th failure of a unit (1 = its first failure).
64
+ * @returns {{action: string, tier?: string}}
65
+ */
66
+ function routeFailure(cls, attempt = 1) {
67
+ const ladder = POLICIES[cls] || POLICIES.unknown;
68
+ if (cls === 'flake') return { action: 'quarantine' };
69
+ if (cls === 'budget') return { action: 'stop_budget' };
70
+ const action = ladder[attempt - 1] || 'ask';
71
+ if (action === 'escalate_tier') return { action, tier: escalateTier('mid', attempt).tier };
72
+ return { action };
73
+ }
74
+
75
+ /** The last lines of a failure's output, for the retry prompt (one line, shell-safe). */
76
+ function errorTail(f = {}, max = 300) {
77
+ const text = `${f.stderr || ''}\n${f.stdout || ''}`.trim().split(/\r?\n/).filter(Boolean).slice(-4).join(' / ');
78
+ return text.replace(/[\r\n&|<>%^"]/g, ' ').replace(/\s+/g, ' ').slice(-max).trim();
79
+ }
80
+
81
+ module.exports = { FAILURE_CLASSES, classifyFailure, routeFailure, errorTail, POLICIES };
@@ -92,6 +92,8 @@ function foldFindings(rows) {
92
92
  severity: r.severity, where: r.where || null, summary: r.summary, first_seen: r.ts,
93
93
  first_verdict_id: r.verdict_id || null, reports: 0, last_reported: null, _reportedSeq: -1,
94
94
  disposition: null, reason: null, disposed_at: null, auto: false, file_sha: null, _disposedSeq: -1, _returnedSeq: -1,
95
+ ...(r.pre_existing === true ? { pre_existing: true } : {}),
96
+ proof: null,
95
97
  });
96
98
  } else if (r.kind === 'verdict') {
97
99
  verdicts.push({ ...r, _seq: seq });
@@ -107,6 +109,10 @@ function foldFindings(rows) {
107
109
  const f = findings.get(id);
108
110
  if (f) f._returnedSeq = seq;
109
111
  }
112
+ } else if (r.kind === 'proof' && typeof r.id === 'string') {
113
+ // R5: the latest catching-test run for a finding.
114
+ const f = findings.get(r.id);
115
+ if (f) f.proof = { status: r.status, test: r.test };
110
116
  } else if (r.kind === 'disposition' && typeof r.id === 'string') {
111
117
  const f = findings.get(r.id);
112
118
  if (!f || !V.DISPOSITIONS.includes(r.disposition)) return;
@@ -130,7 +136,8 @@ function foldFindings(rows) {
130
136
  /** A finding as the CLI shows it (internal sequence fields removed). */
131
137
  function publicFinding(f) {
132
138
  const { _reportedSeq, _disposedSeq, _returnedSeq, ...rest } = f;
133
- return rest;
139
+ // R5: confirmed only with a catching test that passed its proof; a candidate otherwise.
140
+ return { ...rest, confidence: f.proof && f.proof.status === 'confirmed' ? 'confirmed' : 'candidate' };
134
141
  }
135
142
 
136
143
  function countBy(list, key) {
@@ -324,6 +331,7 @@ function recordVerdict(cwd, input) {
324
331
  const supps = learnedSuppressions(cwd, fold);
325
332
  const suppressed = [];
326
333
  const returned = [];
334
+ const preExisting = [];
327
335
  let rematched = 0;
328
336
  for (const f of verdict.findings) {
329
337
  let id = V.findingId(phase, agent, f);
@@ -347,7 +355,8 @@ function recordVerdict(cwd, input) {
347
355
  }
348
356
  ids.push(id);
349
357
  if (!fold.findings.has(id)) {
350
- findingRows.push({ v: LEDGER_V, kind: 'finding', ts, id, verdict_id: verdictId, phase, milestone, agent, class: f.class, severity: f.severity, where: f.where, summary: f.summary });
358
+ findingRows.push({ v: LEDGER_V, kind: 'finding', ts, id, verdict_id: verdictId, phase, milestone, agent, class: f.class, severity: f.severity, where: f.where, summary: f.summary, ...(f.pre_existing ? { pre_existing: true } : {}) });
359
+ if (f.pre_existing) preExisting.push(id);
351
360
  }
352
361
  }
353
362
 
@@ -395,11 +404,15 @@ function recordVerdict(cwd, input) {
395
404
  ...(suppressed.length ? { suppressed } : {}),
396
405
  by_severity: bySeverity, by_class: byClass, warnings: parsed.warnings,
397
406
  ...(notChecked.length ? { not_checked: notChecked } : {}),
407
+ ...(verdict.declined && verdict.declined.length ? { declined: verdict.declined } : {}),
398
408
  // A plan check is bound to the plans it checked (V7): exec-phase compares this
399
409
  // hash with the plans it is about to run.
400
410
  ...(agent === 'pan-plan-checker' ? { plans_sha: require('./verify-plan-set.cjs').plansShaFor(cwd, phase) } : {}),
401
411
  });
402
- appendRows(cwd, [...findingRows, ...newRows, ...resolveRows]);
412
+ // A pre-existing finding (M52) is in code the phase did not change: debt the milestone
413
+ // audit reads, never a blocker for this phase.
414
+ const deferRows = preExisting.map((id) => ({ v: LEDGER_V, kind: 'disposition', ts, id, disposition: 'deferred', reason: `pre-existing: in code phase ${phase} did not change; recorded as debt, not a blocker`, auto: true, verdict_id: verdictId }));
415
+ appendRows(cwd, [...findingRows, ...newRows, ...resolveRows, ...deferRows]);
403
416
 
404
417
  const category = { pass: 'verdict_passed', fail: 'verdict_failed', needs_human: 'verdict_needs_human' }[verdict.outcome];
405
418
  const serious = (bySeverity.critical || 0) + (bySeverity.high || 0) > 0;
@@ -424,7 +437,7 @@ function recordVerdict(cwd, input) {
424
437
  });
425
438
  }
426
439
 
427
- return { ...base, recorded: true, duplicate: false, verdict_id: verdictId, attempt, phase, findings: ids.length, new: findingRows.length, rematched, resolved: resolveRows.length, unaddressed, reopened, suppressed, returned: returned.length, ledger: planningRel(FINDINGS_FILE) };
440
+ return { ...base, recorded: true, duplicate: false, verdict_id: verdictId, attempt, phase, findings: ids.length, new: findingRows.length, rematched, resolved: resolveRows.length, unaddressed, reopened, suppressed, returned: returned.length, pre_existing: preExisting.length, ...(verdict.declined && verdict.declined.length ? { declined: verdict.declined } : {}), ledger: planningRel(FINDINGS_FILE) };
428
441
  }
429
442
 
430
443
  /**
@@ -577,6 +590,21 @@ function cmdFindingsSuppressions(cwd, opts, raw) {
577
590
  output(r, raw, String(r.active));
578
591
  }
579
592
 
593
+ /**
594
+ * `findings prove <id> --test <file>` (program R5): run the catching test at the phase's
595
+ * base and at HEAD, and record the proof. Exit 1 unless it confirms the finding.
596
+ */
597
+ function cmdFindingsProve(cwd, opts, raw) {
598
+ if (!requirePlanningTree(cwd)) { output({ error: 'not_a_pan_project', reason: 'no .planning/ directory here' }, raw); return; }
599
+ const id = opts.id;
600
+ if (!id || !FINDING_ID_RE.test(id) || !opts.test) error('Usage: findings prove <f_id> --test <test-file> [--timeout <ms>]');
601
+ const f = foldFindings(readLedger(cwd).rows).findings.get(id);
602
+ if (!f) { output({ error: 'finding_not_found', id }, raw, 'error', 1); return; }
603
+ const p = require('./verify-red.cjs').proveFinding(cwd, f.phase, opts.test, { timeoutMs: opts.timeout ? Number(opts.timeout) : undefined });
604
+ appendRows(cwd, [{ v: LEDGER_V, kind: 'proof', ts: new Date().toISOString(), id, test: p.test, status: p.status }]);
605
+ output({ id, ...p, confidence: p.status === 'confirmed' ? 'confirmed' : 'candidate' }, raw, p.status, p.status === 'confirmed' ? 0 : 1);
606
+ }
607
+
580
608
  function cmdFindingsDebt(cwd, opts, raw) {
581
609
  const r = findingsDebt(cwd, opts);
582
610
  output(r, raw, `${r.deferred_count} deferred, ${r.open_count} open`);
@@ -589,6 +617,8 @@ function findingIdsIn(args) {
589
617
 
590
618
  module.exports = {
591
619
  FINDINGS_FILE,
620
+ sameFinding,
621
+ cmdFindingsProve,
592
622
  STATUSES,
593
623
  readLedger,
594
624
  foldFindings,
@@ -608,7 +608,7 @@ function listBatchFiles(cwd) {
608
608
  *
609
609
  * Newest, not oldest. v2.10.0 (E-12) sorted ascending so "older unfinished batches
610
610
  * execute before newer ones", but nothing ever marks a batch finished, so it returned
611
- * the first batch a project planned, every time. `/pan:focus-exec` executes the newest
611
+ * the first batch a project planned, every time. `/pan:focus-auto --step exec` executes the newest
612
612
  * batch, so its `classify-stages` step was classifying a different batch from the one it
613
613
  * ran. A batch is a snapshot of the backlog: a newer plan already carries whatever an
614
614
  * older one left undone.
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+ /**
3
+ * One host table (program A0): how PAN runs each host CLI.
4
+ *
5
+ * runner the autonomous runner (experiments; the autodev launch builds on it):
6
+ * `buildArgs(prompt, opts)` for a run that acts on its own, and `shell`
7
+ * ('win32' launches the npm .cmd shim through a shell on Windows, where
8
+ * spawnSync cannot resolve it). null: no adapter is written yet.
9
+ * family the model family the host runs by default; the cross-family review lane
10
+ * (R4) exists to get a second opinion from a different one. Copilot and
11
+ * OpenCode route to several vendors, so theirs names the router.
12
+ * review the cross-family review lane (R4): a read-only run, `promptVia` and
13
+ * `args(prompt)`. No flag that writes or auto-approves tools: the reviewer
14
+ * reads the change it is given and answers with a verdict block.
15
+ * headless the harness's model steps: `promptVia` ('stdin' | 'arg'), `args(prompt)`
16
+ * for hosts whose JSON is not parsed yet, and `parse: true` for the one
17
+ * whose output and usage PAN reads (Claude Code; its argv is built by the
18
+ * harness with the step's options).
19
+ *
20
+ * runner.cjs and harness/src/model.cjs used to keep their own tables, and a host change
21
+ * had to be made twice. tests/hosts.test.cjs pins the argv both produce.
22
+ */
23
+
24
+ const HOSTS = Object.freeze({
25
+ claude: Object.freeze({
26
+ bin: 'claude',
27
+ family: 'anthropic',
28
+ review: Object.freeze({ promptVia: 'stdin', args: () => ['-p', '--allowedTools', 'Read,Grep,Glob', '--output-format', 'text'] }),
29
+ runner: Object.freeze({
30
+ bin: 'claude',
31
+ buildArgs: (prompt, opts) => {
32
+ const args = ['-p', '--dangerously-skip-permissions'];
33
+ if (opts && opts.captureMetrics) args.push('--output-format', 'json');
34
+ args.push(prompt);
35
+ return args;
36
+ },
37
+ shell: 'win32',
38
+ }),
39
+ headless: Object.freeze({ bin: 'claude', promptVia: 'stdin', parse: true }),
40
+ }),
41
+ codex: Object.freeze({
42
+ bin: 'codex',
43
+ family: 'openai',
44
+ review: Object.freeze({ promptVia: 'stdin', args: () => ['exec', '--sandbox', 'read-only', '--skip-git-repo-check', '-'] }),
45
+ runner: Object.freeze({ bin: 'codex', buildArgs: (prompt) => ['exec', prompt], shell: 'win32' }),
46
+ // `codex exec -` reads the prompt from stdin; --json streams JSONL events.
47
+ headless: Object.freeze({ bin: 'codex', promptVia: 'stdin', args: () => ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '-'] }),
48
+ }),
49
+ copilot: Object.freeze({
50
+ bin: 'copilot',
51
+ family: 'github',
52
+ // No --allow-all-tools: a non-interactive run is refused any tool it would ask about.
53
+ review: Object.freeze({ promptVia: 'arg', args: (p) => ['-p', p] }),
54
+ // No autonomous adapter yet: `copilot -p` exists, but nothing is written around it.
55
+ runner: null,
56
+ // `-p` takes the prompt as an argument; --output-format json is JSONL.
57
+ headless: Object.freeze({ bin: 'copilot', promptVia: 'arg', args: (p) => ['-p', p, '--output-format', 'json', '--allow-all-tools', '--allow-all-paths'] }),
58
+ }),
59
+ gemini: Object.freeze({
60
+ bin: 'gemini',
61
+ family: 'google',
62
+ // Headless `-p` appends standard input to its prompt; default approval refuses writes.
63
+ review: Object.freeze({ promptVia: 'stdin', args: () => ['-p', 'Review the change on standard input. Reply with the pan-verdict block only.'] }),
64
+ runner: Object.freeze({ bin: 'gemini', buildArgs: (prompt) => ['-p', '--yolo', prompt], shell: 'win32' }),
65
+ // `-p` runs headless; -o json; --approval-mode yolo auto-approves tools.
66
+ headless: Object.freeze({ bin: 'gemini', promptVia: 'arg', args: (p) => ['-p', p, '-o', 'json', '--approval-mode', 'yolo'] }),
67
+ }),
68
+ opencode: Object.freeze({
69
+ bin: 'opencode',
70
+ family: 'opencode',
71
+ review: Object.freeze({ promptVia: 'arg', args: (p) => ['run', p] }),
72
+ runner: Object.freeze({ bin: 'opencode', buildArgs: (prompt) => [prompt], shell: 'win32' }),
73
+ // `run <message..>` with --format json (raw JSON events).
74
+ headless: Object.freeze({ bin: 'opencode', promptVia: 'arg', args: (p) => ['run', '--format', 'json', p] }),
75
+ }),
76
+ });
77
+
78
+ /** The runner adapters by host, as runner.cjs exposes them (RUNTIME_RUNNERS). */
79
+ function runnerTable() {
80
+ return Object.freeze(Object.fromEntries(Object.entries(HOSTS).map(([k, h]) => [k, h.runner])));
81
+ }
82
+
83
+ /** The headless model-step entries by host, as the harness exposes them (MODEL_RUNTIMES). */
84
+ function headlessTable() {
85
+ return Object.fromEntries(Object.entries(HOSTS).map(([k, h]) => [k, h.headless]));
86
+ }
87
+
88
+ module.exports = { HOSTS, runnerTable, headlessTable };
@@ -71,6 +71,11 @@ function attachPhaseArtifactPaths(result, cwd, phaseDirectory) {
71
71
  if (uatFile) {
72
72
  result.uat_path = toPosix(path.join(phaseDirectory, uatFile));
73
73
  }
74
+ // L6b: the user's corrections to /pan:assumptions, which discuss and plan read.
75
+ const assumptionsFile = files.find((f) => f.endsWith('-assumptions.md') || f === 'assumptions.md');
76
+ if (assumptionsFile) {
77
+ result.assumptions_path = toPosix(path.join(phaseDirectory, assumptionsFile));
78
+ }
74
79
  } catch {
75
80
  // Phase directory may not exist yet or may be unreadable
76
81
  }
@@ -150,6 +155,8 @@ function cmdInitExecutePhase(cwd, phase, raw, opts) {
150
155
  // Config flags
151
156
  commit_docs: config.commit_docs,
152
157
  parallelization: config.parallelization,
158
+ // A3: each pan-exec-waves executor in its own git worktree (Claude Code's Workflow tool).
159
+ worktree_isolation: (require('./test-gate.cjs').rawConfig(cwd).workflow || {}).worktree_isolation === true,
153
160
  branching_strategy: config.branching_strategy,
154
161
  phase_branch_template: config.phase_branch_template,
155
162
  milestone_branch_template: config.milestone_branch_template,
@@ -337,7 +344,6 @@ function cmdInitNewProject(cwd, raw) {
337
344
  const result = {
338
345
  // Models
339
346
  researcher_model: resolveModelInternal(cwd, 'pan-project-researcher'),
340
- synthesizer_model: resolveModelInternal(cwd, 'pan-research-synthesizer'),
341
347
  roadmapper_model: resolveModelInternal(cwd, 'pan-roadmapper'),
342
348
 
343
349
  // Config
@@ -386,7 +392,6 @@ function cmdInitNewMilestone(cwd, raw) {
386
392
  const result = {
387
393
  // Models
388
394
  researcher_model: resolveModelInternal(cwd, 'pan-project-researcher'),
389
- synthesizer_model: resolveModelInternal(cwd, 'pan-research-synthesizer'),
390
395
  roadmapper_model: resolveModelInternal(cwd, 'pan-roadmapper'),
391
396
 
392
397
  // Config
@@ -51,6 +51,8 @@ const CITATION_ROOTS = [
51
51
  '.planning/phases',
52
52
  '.planning/milestones',
53
53
  '.planning/memory',
54
+ // P5: resolved debug sessions are evidence a later question can cite.
55
+ '.planning/debug/resolved',
54
56
  'docs',
55
57
  'CHANGELOG.md',
56
58
  'README.md',
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+ /**
3
+ * The human merge gate on the CLI (program L6a): what `/pan:army`'s release step uses.
4
+ *
5
+ * `merge request <branch> [--phase N] [--ci-green]` stages a merge through the same
6
+ * gate the MCP bridge uses (mcp/merge-gate.cjs). With `--phase`, verify_pass is the
7
+ * phase gate's verdict, not the caller's word. The approval token is never printed: it
8
+ * is for a person, not for the agent that staged the merge.
9
+ *
10
+ * `merge confirm <branch>` performs the squash-merge only in an interactive terminal,
11
+ * after the person types the branch name. An agent runs commands without a TTY, so it
12
+ * can stage a merge but never approve one. There is deliberately no environment
13
+ * variable or flag that skips the terminal check: anything an agent can set, an agent
14
+ * can set to approve itself.
15
+ */
16
+ const readline = require('readline');
17
+ const { output, error, execGit } = require('./core.cjs');
18
+ const gate = require('../../mcp/merge-gate.cjs');
19
+
20
+ function cmdMergeRequest(cwd, branch, opts, raw) {
21
+ if (!branch) error('Usage: merge request <branch> [--phase N] [--ci-green]');
22
+ let rec;
23
+ try {
24
+ rec = gate.requestMerge(cwd, { branch, phase: opts.phase, ci_green: !!opts.ciGreen, now: new Date().toISOString() });
25
+ } catch (e) { output({ error: 'invalid_branch', reason: e.message }, raw, 'error', 1); return; }
26
+ const { approval_token: _token, ...shown } = rec;
27
+ output({ ...shown, next: `A person runs, in their own terminal: pan-tools merge confirm ${rec.branch}` }, raw, rec.status);
28
+ }
29
+
30
+ function cmdMergeConfirm(cwd, branch, raw) {
31
+ if (!branch) error('Usage: merge confirm <branch>');
32
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
33
+ output({ error: 'human_terminal_required', reason: 'merge confirm runs only in an interactive terminal: an agent can stage a merge, a person approves it', branch }, raw, 'error', 1);
34
+ return;
35
+ }
36
+ let record;
37
+ try { record = JSON.parse(require('fs').readFileSync(require('path').join(gate.approvalsDir(cwd), `${gate.slugify(gate.validateBranch(branch))}.json`), 'utf8')); } catch { record = null; }
38
+ if (!record) { output({ error: 'no_merge_request', branch }, raw, 'error', 1); return; }
39
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
40
+ rl.question(`Squash-merge ${record.branch} into the current branch? CI green: ${record.ci_green}, verified: ${record.verify_pass}. Type the branch name to approve: `, (answer) => {
41
+ rl.close();
42
+ if (answer.trim() !== record.branch) { output({ merged: false, reasons: ['not_confirmed'], branch: record.branch }, raw, 'not_confirmed', 1); return; }
43
+ const env = { ...process.env, PAN_MERGE_APPROVAL: record.approval_token };
44
+ const git = (argv) => { const r = execGit(cwd, argv); return { ok: r.exitCode === 0, stdout: r.stdout, stderr: r.stderr }; };
45
+ const r = gate.confirmMerge(cwd, { branch: record.branch }, env, git);
46
+ output(r, raw, r.merged ? 'merged' : 'refused', r.merged ? 0 : 1);
47
+ });
48
+ }
49
+
50
+ module.exports = { cmdMergeRequest, cmdMergeConfirm };
@@ -17,7 +17,9 @@
17
17
  * `{NN}-mutation.json`
18
18
  *
19
19
  * `workflow.mutation_gate`: `off` · `report` (the default: a report, never a refusal) ·
20
- * `gate` (reserved for V5G, which turns survivors into gaps). No coverage is collected:
20
+ * `gate` (V5G, opt-in: a score below `workflow.mutation_floor`, 0.6, fails the gate, and
21
+ * verify reconcile then treats a passed verification as a contradiction, so the phase gate
22
+ * refuses until the tests are stronger). No coverage is collected:
21
23
  * the full suite runs per mutant, so the sample is kept small.
22
24
  *
23
25
  * The operator table started as scripts/mutation-probe.cjs's, which PAN runs on itself;
@@ -29,6 +31,7 @@ const { execGit, findPhaseInternal, output, error, toPosix } = require('./core.c
29
31
 
30
32
  const MODES = Object.freeze(['off', 'report', 'gate']);
31
33
  const DEFAULT_MAX = 8;
34
+ const DEFAULT_FLOOR = 0.6;
32
35
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
33
36
 
34
37
  const C_FAMILY_COMPARE = [
@@ -171,6 +174,12 @@ function mutationMode(cwd) {
171
174
  * @param {string} phase
172
175
  * @param {{max?: number, seed?: number, timeoutMs?: number}} [opts]
173
176
  */
177
+ /** `workflow.mutation_floor`: the score gate mode needs, 0..1; 0.6 unless set. */
178
+ function mutationFloor(cwd) {
179
+ const v = Number((require('./test-gate.cjs').rawConfig(cwd).workflow || {}).mutation_floor);
180
+ return Number.isFinite(v) && v >= 0 && v <= 1 ? v : DEFAULT_FLOOR;
181
+ }
182
+
174
183
  function mutatePhase(cwd, phase, opts = {}) {
175
184
  const TG = require('./test-gate.cjs');
176
185
  const { phaseBase, withWorktree } = require('./verify-red.cjs');
@@ -231,16 +240,18 @@ function mutatePhase(cwd, phase, opts = {}) {
231
240
  const results = wt.value.results;
232
241
  const killed = results.filter((r) => r.killed).length;
233
242
  const survivors = results.filter((r) => !r.killed).map(({ file, line, op, before, after }) => ({ file: toPosix(file), line, op, before: before.trim(), after: after.trim() }));
243
+ const score = results.length ? Number((killed / results.length).toFixed(2)) : null;
244
+ const gate = mode === 'gate' && score !== null ? { floor: mutationFloor(cwd), failed: score < mutationFloor(cwd) } : undefined;
234
245
  return {
235
246
  ...base, checked: results.length > 0, base: b.base, head, command: suite.command,
236
247
  candidates: candidates.length, sampled: results.length, killed, survived: survivors.length,
237
- score: results.length ? Number((killed / results.length).toFixed(2)) : null, survivors,
248
+ score, survivors, ...(gate ? { gate } : {}),
238
249
  };
239
250
  }
240
251
 
241
252
  /**
242
253
  * `pan-tools verify mutate --phase N [--max n] [--seed n] [--timeout ms]`: writes
243
- * `{NN}-mutation.json` and exits 0 (report mode; V5G adds the gate).
254
+ * `{NN}-mutation.json`. Exits 0, except in gate mode when the score is below the floor (V5G).
244
255
  */
245
256
  function cmdVerifyMutate(cwd, opts, raw) {
246
257
  if (!opts.phase) error('Usage: verify mutate --phase <N> [--max <n>] [--seed <n>] [--timeout <ms>]');
@@ -251,7 +262,7 @@ function cmdVerifyMutate(cwd, opts, raw) {
251
262
  const file = path.join(cwd, info.directory, `${prefix}-mutation.json`);
252
263
  fs.writeFileSync(file, JSON.stringify(r, null, 2) + '\n');
253
264
  r.record = toPosix(path.relative(cwd, file));
254
- output(r, raw, r.aborted ? `aborted:${r.aborted}` : (r.checked ? `${r.killed}/${r.sampled} killed` : 'not_checked'));
265
+ output(r, raw, r.aborted ? `aborted:${r.aborted}` : (r.checked ? `${r.killed}/${r.sampled} killed${r.gate && r.gate.failed ? ' (gate failed)' : ''}` : 'not_checked'), r.gate && r.gate.failed ? 1 : 0);
255
266
  }
256
267
 
257
- module.exports = { OPERATORS, MODES, languageOf, mutableLine, mutationsFor, applyMutation, sample, changedLines, mutationMode, mutatePhase, cmdVerifyMutate };
268
+ module.exports = { mutationFloor, DEFAULT_FLOOR, OPERATORS, MODES, languageOf, mutableLine, mutationsFor, applyMutation, sample, changedLines, mutationMode, mutatePhase, cmdVerifyMutate };