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,149 @@
1
+ 'use strict';
2
+ /**
3
+ * UAT through the engine (program L8): the walkthrough is a portable markdown workflow
4
+ * (workflows/uat-walkthrough.md, `/pan:verify-phase N --uat`), and what it records goes
5
+ * through these verbs, so every runtime leaves the same `{phase}-uat.md`:
6
+ *
7
+ * uat record <phase> --test N --result pass|issue|skipped [--reported "…"] [--severity S] [--reason "…"]
8
+ * sets one test's result, then rebuilds `## Summary` and `## Gaps` (the YAML list
9
+ * diagnose-issues and `plan-phase --gaps` read). Diagnosis fields a gap already
10
+ * carries (root_cause, artifacts, missing, debug_session) survive a re-record.
11
+ * The session is `complete` when no test is pending.
12
+ * uat status <phase>
13
+ * counts and the gaps, for the walkthrough to decide whether to diagnose.
14
+ *
15
+ * The file shape is templates/uat.md.
16
+ */
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { findPhaseInternal, output, error, toLf, dominantEol, withEol } = require('./core.cjs');
20
+
21
+ const RESULTS = Object.freeze(['pass', 'issue', 'skipped']);
22
+ const SEVERITIES = Object.freeze(['blocker', 'major', 'minor', 'cosmetic']);
23
+ const KEEP_GAP_FIELDS = Object.freeze(['root_cause', 'artifacts', 'missing', 'debug_session']);
24
+
25
+ const unquote = (s) => String(s || '').trim().replace(/^"(.*)"$/, '$1');
26
+ const quote = (s) => `"${String(s || '').replace(/"/g, "'")}"`;
27
+
28
+ function uatPath(cwd, phase) {
29
+ const info = findPhaseInternal(cwd, phase);
30
+ if (!info || !info.found) return null;
31
+ const dir = path.join(cwd, info.directory);
32
+ let files = [];
33
+ try { files = fs.readdirSync(dir).filter((f) => /-uat\.md$/i.test(f)); } catch { /* none */ }
34
+ return files.length ? path.join(dir, files[0]) : null;
35
+ }
36
+
37
+ /** Parse a uat.md: frontmatter status, the tests, and the gaps' extra fields by test number. */
38
+ function parseUat(raw) {
39
+ const text = toLf(String(raw || ''));
40
+ const status = ((/^---\n[\s\S]*?^status:\s*(.+)$/m.exec(text) || [])[1] || '').trim();
41
+ const tests = [];
42
+ const testsSec = (/^## Tests\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(text) || [])[1] || '';
43
+ for (const block of testsSec.split(/^(?=### \d+\.)/m)) {
44
+ const h = /^### (\d+)\.\s*(.+)$/m.exec(block);
45
+ if (!h) continue;
46
+ const field = (k) => ((new RegExp(`^${k}:\\s*(.*)$`, 'm').exec(block) || [])[1] || '').trim();
47
+ let result = field('result').replace(/^\[|\]$/g, '') || 'pending';
48
+ tests.push({ number: Number(h[1]), name: h[2].trim(), expected: field('expected'), result, reported: unquote(field('reported')) || null, severity: field('severity') || null, reason: field('reason') || null });
49
+ }
50
+ const gapsSec = (/^## Gaps\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(text) || [])[1] || '';
51
+ const gapExtras = new Map();
52
+ for (const item of gapsSec.split(/^(?=- truth:)/m)) {
53
+ const n = /^\s+test:\s*(\d+)/m.exec(item);
54
+ if (!n) continue;
55
+ const extras = {};
56
+ for (const k of KEEP_GAP_FIELDS) {
57
+ const m = new RegExp(`^\\s+${k}:\\s*(.*)$`, 'm').exec(item);
58
+ if (m && m[1].trim() && m[1].trim() !== '""' && m[1].trim() !== '[]') extras[k] = m[1].trim();
59
+ }
60
+ gapExtras.set(Number(n[1]), extras);
61
+ }
62
+ return { status, tests, gapExtras };
63
+ }
64
+
65
+ function renderTest(t) {
66
+ const lines = [`### ${t.number}. ${t.name}`, `expected: ${t.expected}`, `result: ${t.result === 'pending' ? '[pending]' : t.result}`];
67
+ if (t.result === 'issue') {
68
+ lines.push(`reported: ${quote(t.reported)}`);
69
+ lines.push(`severity: ${t.severity || 'major'}`);
70
+ }
71
+ if (t.result === 'skipped' && t.reason) lines.push(`reason: ${t.reason}`);
72
+ return lines.join('\n');
73
+ }
74
+
75
+ function summaryOf(tests) {
76
+ const n = (r) => tests.filter((t) => t.result === r).length;
77
+ return { total: tests.length, passed: n('pass'), issues: n('issue'), pending: n('pending'), skipped: n('skipped') };
78
+ }
79
+
80
+ function gapsOf(tests, gapExtras) {
81
+ return tests.filter((t) => t.result === 'issue').map((t) => ({
82
+ truth: t.expected, status: 'failed', reason: `User reported: ${t.reported || ''}`.trim(), severity: t.severity || 'major', test: t.number,
83
+ ...(gapExtras.get(t.number) || {}),
84
+ }));
85
+ }
86
+
87
+ function renderGaps(gaps) {
88
+ return gaps.map((g) => {
89
+ const lines = [`- truth: ${quote(g.truth)}`, ` status: ${g.status}`, ` reason: ${quote(g.reason)}`, ` severity: ${g.severity}`, ` test: ${g.test}`];
90
+ for (const k of KEEP_GAP_FIELDS) if (g[k]) lines.push(` ${k}: ${g[k]}`);
91
+ return lines.join('\n');
92
+ }).join('\n');
93
+ }
94
+
95
+ /** Set one test's result and rebuild the summary and the gaps. */
96
+ function recordUatResult(cwd, phase, input) {
97
+ const file = uatPath(cwd, phase);
98
+ if (!file) return { error: 'uat_not_found', reason: `no {phase}-uat.md in phase ${phase} (create one with scaffold uat)` };
99
+ if (!RESULTS.includes(input.result)) return { error: 'invalid_result', reason: `result is one of ${RESULTS.join(', ')}` };
100
+ if (input.severity && !SEVERITIES.includes(input.severity)) return { error: 'invalid_severity', reason: `severity is one of ${SEVERITIES.join(', ')}` };
101
+ const raw = fs.readFileSync(file, 'utf8');
102
+ const eol = dominantEol(raw);
103
+ let text = toLf(raw);
104
+ const u = parseUat(text);
105
+ const t = u.tests.find((x) => x.number === Number(input.test));
106
+ if (!t) return { error: 'test_not_found', reason: `uat.md has no test ${input.test}` };
107
+ t.result = input.result;
108
+ t.reported = input.result === 'issue' ? (input.reported || t.reported || '') : null;
109
+ t.severity = input.result === 'issue' ? (input.severity || t.severity || 'major') : null;
110
+ t.reason = input.result === 'skipped' ? (input.reason || t.reason || null) : null;
111
+
112
+ const testsBody = '\n' + u.tests.map(renderTest).join('\n\n') + '\n\n';
113
+ text = text.replace(/^## Tests\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, `## Tests\n${testsBody}`);
114
+ const s = summaryOf(u.tests);
115
+ const summary = `## Summary\n\ntotal: ${s.total}\npassed: ${s.passed}\nissues: ${s.issues}\npending: ${s.pending}\nskipped: ${s.skipped}\n\n`;
116
+ text = /^## Summary\s*$/m.test(text) ? text.replace(/^## Summary\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, summary) : text + `\n${summary}`;
117
+ const gaps = gapsOf(u.tests, u.gapExtras);
118
+ const gapsBlock = `## Gaps\n\n<!-- YAML format for plan-phase --gaps consumption -->\n${gaps.length ? renderGaps(gaps) + '\n' : ''}`;
119
+ text = /^## Gaps\s*$/m.test(text) ? text.replace(/^## Gaps\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, gapsBlock) : text + `\n${gapsBlock}`;
120
+ const now = new Date().toISOString();
121
+ text = text.replace(/^updated:.*$/m, `updated: ${now}`);
122
+ if (s.pending === 0) text = text.replace(/^status:\s*testing\s*$/m, 'status: complete');
123
+ fs.writeFileSync(file, withEol(text, eol));
124
+ return { recorded: true, test: t.number, result: t.result, ...s, complete: s.pending === 0, gaps: gaps.length };
125
+ }
126
+
127
+ function uatStatus(cwd, phase) {
128
+ const file = uatPath(cwd, phase);
129
+ if (!file) return { error: 'uat_not_found', reason: `no {phase}-uat.md in phase ${phase}` };
130
+ const u = parseUat(fs.readFileSync(file, 'utf8'));
131
+ const s = summaryOf(u.tests);
132
+ return { file: path.basename(file), status: u.status, ...s, complete: s.pending === 0, gaps: gapsOf(u.tests, u.gapExtras) };
133
+ }
134
+
135
+ function cmdUatRecord(cwd, phase, opts, raw) {
136
+ if (!phase || !opts.test || !opts.result) error('Usage: uat record <phase> --test N --result pass|issue|skipped [--reported "…"] [--severity S] [--reason "…"]');
137
+ const r = recordUatResult(cwd, phase, opts);
138
+ if (r.error) { output(r, raw, 'error', 1); return; }
139
+ output(r, raw, `${r.test}: ${r.result}`);
140
+ }
141
+
142
+ function cmdUatStatus(cwd, phase, raw) {
143
+ if (!phase) error('Usage: uat status <phase>');
144
+ const r = uatStatus(cwd, phase);
145
+ if (r.error) { output(r, raw, 'error', 1); return; }
146
+ output(r, raw, `${r.passed} passed, ${r.issues} issues, ${r.pending} pending`);
147
+ }
148
+
149
+ module.exports = { parseUat, recordUatResult, uatStatus, cmdUatRecord, cmdUatStatus };
@@ -36,7 +36,7 @@ const KNOWN_VERDICTS = Object.freeze({
36
36
  'pan-design-checker': Object.freeze({ PASS: 'pass', GAPS: 'fail' }),
37
37
  });
38
38
 
39
- const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80 });
39
+ const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80, declined: 20 });
40
40
  // What the verifier's test gate can report (verify-phase.md, run_test_suite).
41
41
  const TEST_GATE_VALUES = Object.freeze(['passed', 'failed', 'skipped']);
42
42
  const RESOLUTION_STATUSES = Object.freeze(['resolved', 'still_present', 'cannot_tell']);
@@ -192,12 +192,15 @@ function validateVerdict(obj, opts = {}) {
192
192
  warnings.push(`summary_truncated: #${i}`);
193
193
  }
194
194
  const where = f.where == null || cleanText(f.where) === '' ? null : truncate(cleanText(f.where).replace(/\\/g, '/'), LIMITS.where);
195
- findings.push({ class: cls, severity, where, summary });
195
+ // pre_existing (M52): the finding is in code the change did not touch. It is
196
+ // recorded as deferred debt, never a blocker; findings.cjs files it.
197
+ findings.push({ class: cls, severity, where, summary, ...(f.pre_existing === true ? { pre_existing: true } : {}) });
196
198
  });
197
199
 
198
200
  const notChecked = normaliseNotChecked(obj.not_checked, warnings);
199
201
  const resolutions = normaliseResolutions(obj.resolutions, warnings);
200
- return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked, resolutions }, warnings };
202
+ const declined = normaliseDeclined(obj.declined, warnings);
203
+ return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked, resolutions, ...(declined.length ? { declined } : {}) }, warnings };
201
204
  }
202
205
 
203
206
  /**
@@ -234,6 +237,26 @@ function normaliseResolutions(value, warnings) {
234
237
  return out;
235
238
  }
236
239
 
240
+ /**
241
+ * The behaviours a judge set aside, each `{behaviour, reason}` (MI-118, program R2).
242
+ * A review that names what it declined to judge says where its silence is not a pass.
243
+ * Accepts bare strings; drops entries that name nothing.
244
+ */
245
+ function normaliseDeclined(value, warnings) {
246
+ if (value == null) return [];
247
+ if (!Array.isArray(value)) { warnings.push('declined_not_an_array'); return []; }
248
+ const out = [];
249
+ for (const entry of value.slice(0, LIMITS.declined)) {
250
+ const obj = typeof entry === 'string' ? { behaviour: entry } : (entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null);
251
+ const behaviour = obj ? truncate(cleanText(obj.behaviour ?? obj.behavior ?? ''), LIMITS.summary) : '';
252
+ if (!behaviour) continue;
253
+ const reason = obj.reason == null || cleanText(obj.reason) === '' ? null : truncate(cleanText(obj.reason), LIMITS.summary);
254
+ out.push({ behaviour, reason });
255
+ }
256
+ if (value.length > LIMITS.declined) warnings.push(`declined_truncated: ${value.length} reported, ${LIMITS.declined} kept`);
257
+ return out;
258
+ }
259
+
237
260
  /**
238
261
  * The checks a judge could not run, each `{check, reason}` (market-ideas M23).
239
262
  * A verdict that carries them says what it did not see: a `pass` with tests that
@@ -176,9 +176,38 @@ function checkPlanSet(cwd, phase) {
176
176
  }
177
177
  }
178
178
 
179
+ // 5. Property tasks (V10): a requirement whose EARS criterion is a validation rule
180
+ // (IF … THEN … SHALL, or a claim over any / every / all inputs, or a "never") is a
181
+ // claim over many inputs, which one example test cannot show. The plan that lists it
182
+ // must carry a property-test task: property="true" on the task, or "property test" /
183
+ // "property-based" in its name or action.
184
+ let reqText = '';
185
+ try { reqText = fs.readFileSync(planningPath(cwd, 'requirements.md'), 'utf-8'); } catch { /* none */ }
186
+ if (reqText && required.length) {
187
+ const RL = require('./requirements-lint.cjs');
188
+ const byId = new Map(RL.parseRequirements(reqText).map((r) => [key(r.id), r]));
189
+ for (const id of required) {
190
+ const r = byId.get(key(id));
191
+ if (!r || !r.criteria.some(isValidationRule)) continue;
192
+ const covering = plans.filter((p) => p.requirements.map(key).includes(key(id)));
193
+ const hasProperty = covering.some((p) => {
194
+ let text = '';
195
+ try { text = toLf(fs.readFileSync(path.join(pf.dir, p.file), 'utf-8')); } catch { /* unreadable */ }
196
+ return /<task\b[^>]*\bproperty=["']?true/i.test(text) || /property[- ]based|property test/i.test(text);
197
+ });
198
+ if (!hasProperty) errors.push({ code: 'missing_property_task', requirement: id, detail: `${id} states a validation rule ("${r.criteria.find(isValidationRule).slice(0, 80)}"): a plan covering it needs a property-test task, which checks the rule over generated inputs with the project's own property library` });
199
+ }
200
+ }
201
+
179
202
  return { phase: phaseNumber, ok: errors.length === 0, errors, warnings, plans: plans.length, plans_sha: plansSha(pf.dir, pf.planFiles) };
180
203
  }
181
204
 
205
+ /** A validation-rule criterion (V10): unwanted-behaviour EARS, or a claim over all inputs. */
206
+ function isValidationRule(criterion) {
207
+ const c = String(criterion || '');
208
+ return /^IF\b[\s\S]*\bTHEN\b/i.test(c) || /\b(any|every|all|each)\s+\w+/i.test(c) || /\bnever\b/i.test(c);
209
+ }
210
+
182
211
  /**
183
212
  * Whether the plans exec-phase is about to run are the plans that passed a plan check.
184
213
  * @returns {{status: 'ok'|'stale'|'failed'|'missing'|'legacy'|'disabled', reason?: string, plans_sha?: string, checked_sha?: string, attempt?: number}}
@@ -227,4 +256,4 @@ function cmdVerifyPlanSet(cwd, phase, raw) {
227
256
  output(result, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
228
257
  }
229
258
 
230
- module.exports = { phaseFiles, checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
259
+ module.exports = { isValidationRule, phaseFiles, checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
@@ -103,11 +103,11 @@ function cmdPreflight(cwd, target, raw) {
103
103
  checks.push({ name: 'batch_exists', passed: true, detail: files[files.length - 1] });
104
104
  } else {
105
105
  checks.push({ name: 'batch_exists', passed: false, detail: 'no batch file found' });
106
- blockers.push('No batch file — run /pan:focus-plan first');
106
+ blockers.push('No batch file — run /pan:focus-auto --step plan first');
107
107
  }
108
108
  } catch {
109
109
  checks.push({ name: 'batch_exists', passed: false, detail: 'focus/ directory not found' });
110
- blockers.push('No focus/ directory — run /pan:focus-scan first');
110
+ blockers.push('No focus/ directory — run /pan:focus-auto --step scan first');
111
111
  }
112
112
  } else {
113
113
  // target is a phase number — check the phase directory exists
@@ -127,6 +127,42 @@ function checkRed(cwd, phase, opts = {}) {
127
127
  };
128
128
  }
129
129
 
130
+ /**
131
+ * Prove a review finding with a catching test (program R5): the inverse of RED. The
132
+ * test shows the defect the phase introduced when it PASSES at the phase's base (the
133
+ * behaviour was right before) and FAILS at HEAD (the phase broke it).
134
+ *
135
+ * confirmed passes at the base, fails at HEAD
136
+ * fails_at_base fails before the phase too: not specific to the change
137
+ * passes_at_head the defect it claims is not there, or the test does not reach it
138
+ * not_checked no base, no command for the file, or the worktree failed (with why)
139
+ *
140
+ * @returns {{status, test, base?, command?, base_exit?, head_exit?, detail?}}
141
+ */
142
+ function proveFinding(cwd, phase, testFile, opts = {}) {
143
+ const test = String(testFile || '').split(path.sep).join('/').split('\\').join('/');
144
+ if (!test || !fs.existsSync(path.join(cwd, test))) return { status: 'not_checked', test, detail: 'the test file does not exist' };
145
+ const info = findPhaseInternal(cwd, phase);
146
+ const b = phaseBase(cwd, (info && info.phase_number) || String(phase));
147
+ if (!b.base) return { status: 'not_checked', test, detail: b.reason };
148
+ const fc = TG.fileCommand(cwd, test);
149
+ if (!fc.command) return { status: 'not_checked', test, detail: fc.reason };
150
+ const timeoutMs = opts.timeoutMs || DEFAULT_FILE_TIMEOUT_MS;
151
+ let atBase = null;
152
+ const wt = withWorktree(cwd, b.base, (dir) => {
153
+ const dest = path.join(dir, test);
154
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
155
+ fs.copyFileSync(path.join(cwd, test), dest);
156
+ atBase = TG.runCommand(dir, fc.command, timeoutMs);
157
+ });
158
+ if (wt.error) return { status: 'not_checked', test, base: b.base, detail: wt.error };
159
+ const atHead = TG.runCommand(cwd, fc.command, timeoutMs);
160
+ const status = atBase.exit !== 0 ? 'fails_at_base' : (atHead.exit === 0 ? 'passes_at_head' : 'confirmed');
161
+ const detail = status === 'fails_at_base' ? `fails before the phase too: ${tail(atBase.output)}`
162
+ : status === 'passes_at_head' ? 'passes at HEAD: it does not show the defect the finding claims' : null;
163
+ return { status, test, base: b.base, command: fc.command, base_exit: atBase.exit, head_exit: atHead.exit, ...(detail ? { detail } : {}) };
164
+ }
165
+
130
166
  /** `pan-tools verify red <phase> [--timeout <ms>]`: exit 1 when a test is not_red or not_green. */
131
167
  function cmdVerifyRed(cwd, phase, raw, opts = {}) {
132
168
  if (!phase) error('Usage: verify red <phase> [--timeout <ms>]');
@@ -135,4 +171,4 @@ function cmdVerifyRed(cwd, phase, raw, opts = {}) {
135
171
  output(r, raw, !r.checked ? 'not_checked' : (r.ok ? 'caught' : 'not_caught'), r.ok ? 0 : 1);
136
172
  }
137
173
 
138
- module.exports = { checkRed, cmdVerifyRed, phaseBase, withWorktree };
174
+ module.exports = { checkRed, cmdVerifyRed, phaseBase, withWorktree, proveFinding };
@@ -38,9 +38,10 @@ function collectVerificationStats(phasesDir) {
38
38
  // Extract gap descriptions from ## Gaps section
39
39
  const gapsMatch = content.match(/## Gaps[\s\S]*?(?=\n## |$)/);
40
40
  if (gapsMatch) {
41
- const lines = gapsMatch[0].split('\n').filter(l => l.match(/^[-*]\s+/));
41
+ // Bullets and numbered items both (L6b: numbered gap lists were dropped).
42
+ const lines = gapsMatch[0].split('\n').filter(l => l.match(/^(?:[-*]|\d+[.)])\s+/));
42
43
  for (const line of lines) {
43
- const desc = line.replace(/^[-*]\s+/, '').trim();
44
+ const desc = line.replace(/^(?:[-*]|\d+[.)])\s+/, '').trim();
44
45
  if (desc) stats.gap_patterns.push(desc);
45
46
  }
46
47
  }
@@ -168,6 +168,36 @@ function checkCheckpointTask(type, attrs, body, errors, warnings) {
168
168
  return { name: label, type, auto_select: autoMatch ? autoMatch[1] : null };
169
169
  }
170
170
 
171
+ // The elements a plan nests. A file that ends with one of them still open was cut
172
+ // off: runtimes cap tool output, and a long plan has been truncated mid-tag while
173
+ // the write reported success (market-ideas M47; gsd-core v1.18.0).
174
+ const PLAN_TAGS = ['tasks', 'task', 'name', 'files', 'action', 'verify', 'done'];
175
+ const TRUNCATION_MARKER = /\[\s*\.{0,3}\s*(?:output\s+)?truncated\b|<!--\s*(?:continue|continued|truncated)\b|\(output truncated\)|\bcontinued in next (?:message|write)\b/i;
176
+
177
+ /**
178
+ * Signs that a plan file was cut off. Pure; tags inside fenced code blocks do not count.
179
+ * @param {string} content - plan file content
180
+ * @returns {string[]} one message per problem, empty when the file is whole
181
+ */
182
+ function detectPlanTruncation(content) {
183
+ const text = String(content || '').replace(/\r\n/g, '\n').replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, '');
184
+ const out = [];
185
+ const open = [];
186
+ const tagRe = new RegExp(`<(/?)(${PLAN_TAGS.join('|')})\\b[^>]*?(/?)>`, 'g');
187
+ let m;
188
+ while ((m = tagRe.exec(text)) !== null) {
189
+ const [, closing, tag, selfClosing] = m;
190
+ if (selfClosing) continue;
191
+ if (!closing) { open.push(tag); continue; }
192
+ const at = open.lastIndexOf(tag);
193
+ if (at >= 0) open.splice(at);
194
+ }
195
+ if (open.length) out.push(`Plan looks truncated: the file ends with ${open.map((t) => `<${t}>`).join(' > ')} still open`);
196
+ const marker = text.match(TRUNCATION_MARKER);
197
+ if (marker) out.push(`Plan looks truncated: it contains a continuation marker ("${marker[0].trim()}")`);
198
+ return out;
199
+ }
200
+
171
201
  /**
172
202
  * Validate a plan.md structure: required frontmatter, task elements, and consistency.
173
203
  * @param {string} cwd - Working directory path
@@ -180,11 +210,19 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
180
210
  const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
181
211
  const content = safeReadFile(fullPath);
182
212
  if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
213
+ const r = planStructure(content);
214
+ output(r, raw, r.valid ? 'valid' : 'invalid');
215
+ }
183
216
 
217
+ /** A plan's structure check (cmdVerifyPlanStructure without the output). Pure. */
218
+ function planStructure(content) {
184
219
  const fm = extractFrontmatter(content);
185
220
  const errors = [];
186
221
  const warnings = [];
187
222
 
223
+ // A cut-off plan first: its task count and fields would mislead (M47).
224
+ errors.push(...detectPlanTruncation(content));
225
+
188
226
  // Check required frontmatter fields
189
227
  const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
190
228
  for (const field of required) {
@@ -246,14 +284,14 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
246
284
  errors.push('Has checkpoint tasks but autonomous is not false');
247
285
  }
248
286
 
249
- output({
287
+ return {
250
288
  valid: errors.length === 0,
251
289
  errors,
252
290
  warnings,
253
291
  task_count: tasks.length,
254
292
  tasks,
255
293
  frontmatter_fields: Object.keys(fm),
256
- }, raw, errors.length === 0 ? 'valid' : 'invalid');
294
+ };
257
295
  }
258
296
 
259
297
  /**
@@ -584,6 +622,16 @@ function reconcilePhase(cwd, phaseNum) {
584
622
  contradictions.push(`verification status "${status}" but the engine's test run failed${why}`);
585
623
  }
586
624
  }
625
+ // V5G: in gate mode, a mutation score below the floor means the tests do not pin the
626
+ // code, so a passed verification is a contradiction until they do.
627
+ const mutFile = files.find((f) => /-mutation\.json$/.test(f));
628
+ if (claimsPass && mutFile) {
629
+ let mut = null;
630
+ try { mut = JSON.parse(safeReadFile(path.join(dir, mutFile)) || 'null'); } catch {}
631
+ if (mut && mut.mode === 'gate' && mut.gate && mut.gate.failed) {
632
+ contradictions.push(`verification status "${status}" but the mutation score ${mut.score} is below the floor ${mut.gate.floor} (workflow.mutation_gate: gate): ${mut.survived} of ${mut.sampled} mutants survived`);
633
+ }
634
+ }
587
635
  return {
588
636
  phase: String(phaseNum), found: true, verification_status: status, claims_pass: claimsPass,
589
637
  test_run: testRun ? { status: testRun.status, head: testRun.head || null, regressions: (testRun.regressions || []).length } : undefined,
@@ -1473,9 +1521,13 @@ function cmdValidateHealth(cwd, options, raw) {
1473
1521
  }
1474
1522
 
1475
1523
  // Perform repairs if requested and there are repairable issues
1524
+ // L6a: health asks per repair class. Without --repair the result lists the classes;
1525
+ // --only limits a repair to the ones the user chose.
1526
+ const only = Array.isArray(options.only) && options.only.length ? new Set(options.only) : null;
1527
+ const chosen = only ? repairs.filter((r) => only.has(r)) : repairs;
1476
1528
  let repairActions = [];
1477
- if (options.repair && repairs.length > 0) {
1478
- repairActions = repairIssues(cwd, repairs);
1529
+ if (options.repair && chosen.length > 0) {
1530
+ repairActions = repairIssues(cwd, chosen);
1479
1531
  }
1480
1532
 
1481
1533
  // Check 10 (optional): full validation — run tests and build
@@ -1548,6 +1600,7 @@ function cmdValidateHealth(cwd, options, raw) {
1548
1600
  warnings,
1549
1601
  info,
1550
1602
  repairable_count: repairableCount,
1603
+ repairs_available: repairs.length > 0 ? [...new Set(repairs)] : undefined,
1551
1604
  repairs_performed: repairActions.length > 0 ? repairActions : undefined,
1552
1605
  };
1553
1606
  if (options.full) {
@@ -1623,8 +1676,10 @@ function runFullBuildCheck(cwd) {
1623
1676
  // ─── Deployment validation — extracted to verify-deploy.cjs (re-exported below)
1624
1677
 
1625
1678
  module.exports = {
1679
+ planStructure,
1626
1680
  cmdVerifySummary,
1627
1681
  cmdVerifyPlanStructure,
1682
+ detectPlanTruncation,
1628
1683
  cmdVerifyPhaseCompleteness,
1629
1684
  cmdVerifyReferences,
1630
1685
  cmdVerifyCommits,
@@ -208,7 +208,77 @@ function cmdWorktreeCleanup(cwd, raw, opts) {
208
208
  output(r, raw, lines.length ? lines.join('\n') : 'nothing to clean');
209
209
  }
210
210
 
211
+ // ─── Shadow-merge preflight (program A3) ────────────────────────────────────
212
+ // Branches that each pass the oracle can still break it together. Before merging a
213
+ // sub-wave back, merge every branch onto the base in a throwaway worktree, in order,
214
+ // and run the test command there. A conflict is named; a red result is bisected to
215
+ // "each passes alone" (break_together) or the branches that fail on their own.
216
+
217
+ const GIT_IDENTITY = ['-c', 'user.name=PAN preflight', '-c', 'user.email=preflight@pan.local', '-c', 'commit.gpgsign=false'];
218
+
219
+ function runOracle(dir, test) {
220
+ const { spawnSync } = require('child_process');
221
+ const r = spawnSync(test, { cwd: dir, shell: true, encoding: 'utf8', timeout: 20 * 60 * 1000 });
222
+ return r.status === 0;
223
+ }
224
+
225
+ /**
226
+ * @param {string} cwd
227
+ * @param {{base?: string, branches: string[], test?: string}} opts
228
+ * @returns {{ok: boolean, verdict: 'clean'|'conflict'|'break_together'|'branch_fails'|'base_failing'|'no_test', ...}}
229
+ */
230
+ function shadowMergePreflight(cwd, opts = {}) {
231
+ if (!isGitRepo(cwd)) return { error: 'not_git', reason: 'the preflight needs a git repository' };
232
+ const base = opts.base || 'HEAD';
233
+ const branches = (opts.branches || []).filter(Boolean);
234
+ let test = opts.test;
235
+ if (!test) {
236
+ try { test = require('./test-gate.cjs').detectTestCommand(cwd).command; } catch { test = null; }
237
+ }
238
+ if (!test) return { ok: false, verdict: 'no_test', base, branches, reason: 'no test command: pass --test or set one for the project' };
239
+ const os = require('os');
240
+ const fsx = require('fs');
241
+ const dir = fsx.mkdtempSync(path.join(os.tmpdir(), 'pan-preflight-'));
242
+ fsx.rmSync(dir, { recursive: true, force: true });
243
+ const add = execGit(cwd, ['worktree', 'add', '--detach', dir, base]);
244
+ if (add.exitCode !== 0) return { error: 'worktree_failed', reason: add.stderr.trim() };
245
+ const g = (args) => execGit(dir, [...GIT_IDENTITY, ...args]);
246
+ const result = { base, branches, test, merged: [], alone: {} };
247
+ try {
248
+ if (!runOracle(dir, test)) return { ...result, ok: false, verdict: 'base_failing', reason: 'the oracle already fails on the base: a merge cannot be judged' };
249
+ for (const b of branches) {
250
+ const m = g(['merge', '--no-ff', '--no-edit', b]);
251
+ if (m.exitCode !== 0) {
252
+ const files = g(['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').map((x) => x.trim()).filter(Boolean);
253
+ g(['merge', '--abort']);
254
+ return { ...result, ok: false, verdict: 'conflict', conflict: { branch: b, files, after: [...result.merged] } };
255
+ }
256
+ result.merged.push(b);
257
+ }
258
+ if (runOracle(dir, test)) return { ...result, ok: true, verdict: 'clean' };
259
+ for (const b of branches) {
260
+ g(['reset', '--hard', '-q', base]);
261
+ result.alone[b] = g(['merge', '--no-ff', '--no-edit', b]).exitCode === 0 && runOracle(dir, test);
262
+ }
263
+ const failing = branches.filter((b) => !result.alone[b]);
264
+ return failing.length
265
+ ? { ...result, ok: false, verdict: 'branch_fails', failing }
266
+ : { ...result, ok: false, verdict: 'break_together', reason: 'each branch passes alone; merged together they fail' };
267
+ } finally {
268
+ execGit(cwd, ['worktree', 'remove', '--force', dir]);
269
+ try { fsx.rmSync(dir, { recursive: true, force: true }); } catch {}
270
+ execGit(cwd, ['worktree', 'prune']);
271
+ }
272
+ }
273
+
274
+ function cmdWorktreePreflight(cwd, opts, raw) {
275
+ const r = shadowMergePreflight(cwd, opts);
276
+ output(r, raw, r.verdict || r.error);
277
+ }
278
+
211
279
  module.exports = {
280
+ shadowMergePreflight,
281
+ cmdWorktreePreflight,
212
282
  ARMY_BRANCH_PREFIX,
213
283
  createTaskWorktree,
214
284
  removeTaskWorktree,