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,143 @@
1
+ 'use strict';
2
+ /**
3
+ * CI as an oracle for the autodev loop (program A2).
4
+ *
5
+ * The loop never takes a session's word for its tests. Locally the oracle is the phase
6
+ * gate and the test-run engine (V1, V2); with `autodev run --ci` it is also the pull
7
+ * request's checks, read through GitHub's `gh` CLI (optional: without it the CI oracle
8
+ * is `unavailable`). A check that already fails on the base branch is not this change's
9
+ * doing and is ignored. The failing log tail is what a fix round gets as feedback, and a
10
+ * breaker stops after N rounds.
11
+ *
12
+ * `autodev run --ci local` needs no pull request: the oracle is the project's own gate,
13
+ * run here — `npm run ci:local` when package.json has that script, else `npm test`.
14
+ */
15
+
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const DEFAULT_CI_ROUNDS = 3;
19
+
20
+ function defaultExec(cwd) {
21
+ const refused = require('./autodev.cjs').standinMissing('gh');
22
+ if (refused) return () => ({ status: null, error: new Error(refused), stdout: '', stderr: refused });
23
+ return (args) => spawnSync('gh', args, { cwd, encoding: 'utf8', shell: process.platform === 'win32', timeout: 120000 });
24
+ }
25
+
26
+ function parseJson(r) {
27
+ if (!r || r.error || r.status !== 0) return null;
28
+ try { return JSON.parse(r.stdout); } catch { return null; }
29
+ }
30
+
31
+ const BUCKET = { pass: 'pass', fail: 'fail', pending: 'pending', skipping: 'skip', cancel: 'fail' };
32
+ function bucketOf(c) {
33
+ if (c.bucket && BUCKET[c.bucket]) return BUCKET[c.bucket];
34
+ const s = String(c.state || c.conclusion || '').toUpperCase();
35
+ if (['SUCCESS', 'NEUTRAL'].includes(s)) return 'pass';
36
+ if (['SKIPPED'].includes(s)) return 'skip';
37
+ if (['FAILURE', 'ERROR', 'CANCELLED', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE'].includes(s)) return 'fail';
38
+ return 'pending';
39
+ }
40
+
41
+ /** Names (check and workflow) whose latest run on the base branch failed. */
42
+ function baseFailing(exec, base) {
43
+ if (!base) return new Set();
44
+ const runs = parseJson(exec(['run', 'list', '--branch', base, '--limit', '30', '--json', 'name,workflowName,conclusion']));
45
+ const latest = new Map();
46
+ for (const r of Array.isArray(runs) ? runs : []) {
47
+ const key = r.workflowName || r.name;
48
+ if (!latest.has(key)) latest.set(key, r);
49
+ }
50
+ const out = new Set();
51
+ for (const [key, r] of latest) if (String(r.conclusion).toLowerCase() === 'failure') { out.add(key); if (r.name) out.add(r.name); }
52
+ return out;
53
+ }
54
+
55
+ function logTail(exec, checks) {
56
+ for (const c of checks) {
57
+ const m = String(c.link || '').match(/\/actions\/runs\/(\d+)/);
58
+ if (!m) continue;
59
+ const r = exec(['run', 'view', m[1], '--log-failed']);
60
+ if (r && r.status === 0 && r.stdout) return r.stdout.trim().split(/\r?\n/).slice(-40).join('\n');
61
+ }
62
+ return '';
63
+ }
64
+
65
+ /**
66
+ * The CI oracle for the current branch's pull request.
67
+ * @param {string} cwd
68
+ * @param {{exec?: Function, ref?: string, base?: string, head?: string}} opts
69
+ * @returns {{status: 'passed'|'failed'|'pending'|'unavailable'|'stale', failed: string[], ignored: string[], tail: string, reason?: string}}
70
+ */
71
+ function ciOracle(cwd, opts = {}) {
72
+ const exec = opts.exec || defaultExec(cwd);
73
+ const res = { status: 'unavailable', failed: [], ignored: [], tail: '' };
74
+ const checksRaw = exec(['pr', 'checks', ...(opts.ref ? [opts.ref] : []), '--json', 'name,state,bucket,link,workflow']);
75
+ if (!checksRaw || checksRaw.error || checksRaw.status == null) return { ...res, reason: 'gh is not available' };
76
+ const checks = parseJson(checksRaw);
77
+ if (!Array.isArray(checks)) return { ...res, reason: (checksRaw.stderr || 'no pull request checks').trim().slice(0, 200) };
78
+ if (opts.head) {
79
+ const pr = parseJson(exec(['pr', 'view', ...(opts.ref ? [opts.ref] : []), '--json', 'headRefOid']));
80
+ if (pr && pr.headRefOid && pr.headRefOid !== opts.head) return { ...res, status: 'stale', reason: `the checks are for ${pr.headRefOid.slice(0, 9)}, HEAD is ${opts.head.slice(0, 9)}: push the branch` };
81
+ }
82
+ const onBase = baseFailing(exec, opts.base);
83
+ const failing = checks.filter((c) => bucketOf(c) === 'fail');
84
+ const ignored = failing.filter((c) => onBase.has(c.name) || onBase.has(c.workflow));
85
+ const counted = failing.filter((c) => !ignored.includes(c));
86
+ res.ignored = ignored.map((c) => c.name);
87
+ res.failed = counted.map((c) => c.name);
88
+ if (counted.length) return { ...res, status: 'failed', tail: logTail(exec, counted) };
89
+ if (checks.some((c) => bucketOf(c) === 'pending')) return { ...res, status: 'pending' };
90
+ return { ...res, status: 'passed' };
91
+ }
92
+
93
+ /** Wait for pending checks, up to `timeoutMs`. */
94
+ function waitForCi(cwd, opts = {}) {
95
+ const deadline = Date.now() + (opts.timeoutMs || 30 * 60 * 1000);
96
+ for (;;) {
97
+ const r = ciOracle(cwd, opts);
98
+ if (r.status !== 'pending' || Date.now() >= deadline) return r;
99
+ spawnSync(process.execPath, ['-e', `setTimeout(() => {}, ${Math.min(opts.pollMs || 30000, Math.max(0, deadline - Date.now()))})`]);
100
+ }
101
+ }
102
+
103
+ /** The project's own gate: its `ci:local` script, else its `test` script, else none. */
104
+ function localGate(cwd) {
105
+ let scripts = {};
106
+ try { scripts = JSON.parse(require('fs').readFileSync(require('path').join(cwd, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
107
+ if (scripts['ci:local']) return { name: 'ci:local', args: ['run', 'ci:local'] };
108
+ if (scripts.test) return { name: 'test', args: ['test'] };
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * The local CI oracle: run the project's gate in `cwd` and judge it by its exit code.
114
+ * With ci:local, the failing gate ids come from .ci-local/report.json.
115
+ * @param {string} cwd
116
+ * @param {{exec?: Function, timeoutMs?: number}} opts
117
+ * @returns {{status: 'passed'|'failed'|'unavailable', failed: string[], ignored: string[], tail: string, reason?: string}}
118
+ */
119
+ function localOracle(cwd, opts = {}) {
120
+ const res = { status: 'unavailable', failed: [], ignored: [], tail: '' };
121
+ const gate = localGate(cwd);
122
+ if (!gate) return { ...res, reason: 'package.json has neither a ci:local nor a test script' };
123
+ const exec = opts.exec || ((args) => spawnSync('npm', args, { cwd, encoding: 'utf8', shell: process.platform === 'win32', timeout: opts.timeoutMs || 60 * 60 * 1000, maxBuffer: 256 * 1024 * 1024 }));
124
+ const r = exec(gate.args);
125
+ if (!r || r.error || r.status == null) return { ...res, reason: `npm ${gate.args.join(' ')} did not run${r && r.error ? `: ${r.error.message}` : ''}` };
126
+ const tail = `${r.stdout || ''}${r.stderr || ''}`.trim().split(/\r?\n/).slice(-40).join('\n');
127
+ if (r.status === 0) return { ...res, status: 'passed', tail };
128
+ let failed = [gate.name];
129
+ if (gate.name === 'ci:local') {
130
+ try {
131
+ const report = JSON.parse(require('fs').readFileSync(require('path').join(cwd, '.ci-local', 'report.json'), 'utf8'));
132
+ const ids = (report.gates || []).filter((g) => g.status === 'fail').map((g) => g.id);
133
+ if (ids.length) failed = ids;
134
+ } catch { /* the exit code is the verdict; the gate names are a courtesy */ }
135
+ }
136
+ return { ...res, status: 'failed', failed, tail };
137
+ }
138
+
139
+ function breakerTripped(rounds, max = DEFAULT_CI_ROUNDS) {
140
+ return rounds >= max;
141
+ }
142
+
143
+ module.exports = { ciOracle, waitForCi, localOracle, localGate, breakerTripped, baseFailing, bucketOf, DEFAULT_CI_ROUNDS };
@@ -941,7 +941,7 @@ function cmdScaffold(cwd, type, options, raw) {
941
941
  }
942
942
  case 'uat': {
943
943
  filePath = path.join(phaseDir, `${padded}${UAT_SUFFIX}`);
944
- content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} -- User Acceptance Testing\n\n## Test Results\n\n| # | Test | Status | Notes |\n|---|------|--------|-------|\n\n## Summary\n\n_Pending UAT_\n`;
944
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} -- User Acceptance Testing\n\n## Tests\n\n_One \`### N. <name>\` per user-observable truth, with \`expected:\` and \`result: [pending]\` (the walkthrough writes them; \`uat record\` fills the results)_\n\n## Gaps\n\n## Summary\n\n_Pending UAT_\n`;
945
945
  break;
946
946
  }
947
947
  case 'verification': {
@@ -48,7 +48,9 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
48
48
  phase_branch_template: 'pan/phase-{phase}-{slug}',
49
49
  milestone_branch_template: 'pan/{milestone}-{slug}',
50
50
  workflow: {
51
- research: true,
51
+ // Research is opt-in (2026-10-25): `--research` on new-project or plan-phase, or
52
+ // this key set true. A config that already says true keeps it.
53
+ research: false,
52
54
  plan_check: true,
53
55
  verifier: true,
54
56
  nyquist_validation: false,
@@ -59,6 +61,23 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
59
61
  // V5: the mutation report on a phase's diff. `report` lists surviving mutants and
60
62
  // never refuses; `off` skips it; `gate` is reserved for V5G.
61
63
  mutation_gate: 'report',
64
+ // V5G: the score `gate` mode needs (killed / sampled, 0..1).
65
+ mutation_floor: 0.6,
66
+ // R2: `single` runs one reviewer pass; `lenses` runs one per lens (review-vote.cjs
67
+ // LENSES) and keeps only the findings a quorum of passes reported. Opt-in until
68
+ // its paired corpus run decides.
69
+ review_mode: 'single',
70
+ // R4: a second review on another host's CLI (another model family), read-only.
71
+ // `off`, or a host: claude | codex | copilot | gemini | opencode.
72
+ cross_family_review: 'off',
73
+ // A3: pan-exec-waves runs each executor in its own git worktree (Claude Code).
74
+ worktree_isolation: false,
75
+ // Right-sized ceremony (sizing.cjs): `auto` sizes each phase and quick task and gives a
76
+ // small, well-specified one the lean path; `full` always runs the whole ceremony. The
77
+ // `quality` profile also means full.
78
+ ceremony: 'auto',
79
+ // The most requirements a phase can map and still be sized lean.
80
+ lean_max_requirements: 5,
62
81
  // M2: opt-in per-phase HTML reports at the verify→complete gate.
63
82
  phase_reports: { enabled: false, open: false, theme: 'auto', index: true },
64
83
  },
@@ -80,6 +99,10 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
80
99
  // Fraction of the spawn budget held back for re-verification (0..0.5).
81
100
  // Surfaced as an indicator always; a hard early stop only under enforce.
82
101
  verify_reserve: 0.15,
102
+ // A6: autodev's dollar caps, per run and per day (UTC), across every run's ledger.
103
+ // null: none set. An unattended run needs a run cap, here or as --max-usd.
104
+ max_usd_run: null,
105
+ max_usd_day: null,
83
106
  },
84
107
  commit: {
85
108
  safety_checks: true,
@@ -156,8 +179,13 @@ function knownConfigKeys() {
156
179
  * @param {string} keyPath
157
180
  * @returns {boolean}
158
181
  */
182
+ // A lowercase `x-` key is the user's own metadata, never a typo (market-ideas M51,
183
+ // after OpenSpec v1.14.2): PAN ignores it at the top level and inside a section.
184
+ const USER_KEY = /^x-/;
185
+
159
186
  function isKnownConfigPath(keyPath) {
160
187
  const [head, sub] = String(keyPath).split('.');
188
+ if (USER_KEY.test(head) || (sub !== undefined && USER_KEY.test(sub))) return true;
161
189
  const { top, sections } = knownConfigKeys();
162
190
  if (!top.has(head)) return false;
163
191
  return sub === undefined || !sections.has(head) || sections.get(head).has(sub);
@@ -174,10 +202,11 @@ function unknownConfigKeys(config) {
174
202
  const { top, sections } = knownConfigKeys();
175
203
  const unknown = [];
176
204
  for (const [k, v] of Object.entries(config)) {
205
+ if (USER_KEY.test(k)) continue;
177
206
  if (!top.has(k)) { unknown.push(k); continue; }
178
207
  const known = sections.get(k);
179
208
  if (!known || !v || typeof v !== 'object' || Array.isArray(v)) continue;
180
- for (const sub of Object.keys(v)) if (!known.has(sub)) unknown.push(`${k}.${sub}`);
209
+ for (const sub of Object.keys(v)) if (!known.has(sub) && !USER_KEY.test(sub)) unknown.push(`${k}.${sub}`);
181
210
  }
182
211
  return unknown;
183
212
  }
@@ -165,6 +165,29 @@ const CATEGORY_DEFAULTS = {
165
165
  /** Doc files to scan for staleness (focus sync) */
166
166
  const DOC_SYNC_FILES = ['README.md', 'docs/DEVELOPMENT.md', 'docs/CLI-REFERENCE.md', 'docs/USER-GUIDE.md', 'docs/ARCHITECTURE.md'];
167
167
 
168
+ /**
169
+ * Commands folded into another (program L7). The installer writes an alias command for
170
+ * each old name on every runtime, so `/pan:<old>` keeps working: it runs `to` with
171
+ * `args` in front of whatever the user passed.
172
+ */
173
+ const COMMAND_ALIASES = {
174
+ dashboard: { to: 'hud', args: '' },
175
+ 'milestone-cleanup': { to: 'milestone-done', args: '--cleanup' },
176
+ 'milestone-gaps': { to: 'milestone-audit', args: '--gaps' },
177
+ optimize: { to: 'learn', args: '--optimize' },
178
+ 'research-phase': { to: 'plan-phase', args: '--research-only' },
179
+ profile: { to: 'settings', args: '--profile' },
180
+ 'focus-doc-audit': { to: 'focus-docs', args: '' },
181
+ 'focus-sync': { to: 'focus-docs', args: '--sync' },
182
+ 'focus-scan': { to: 'focus-auto', args: '--step scan' },
183
+ 'focus-plan': { to: 'focus-auto', args: '--step plan' },
184
+ 'focus-exec': { to: 'focus-auto', args: '--step exec' },
185
+ 'focus-drift-walking': { to: 'focus-auto', args: '--step drift' },
186
+ };
187
+
188
+ /** Commands removed with no replacement (program L7). */
189
+ const RETIRED_COMMANDS = ['discord', 'mcp-bridge'];
190
+
168
191
  /** Old→new command name mapping for detecting stale docs */
169
192
  const COMMAND_RENAME_MAP = {
170
193
  'execute-phase': 'exec-phase',
@@ -178,7 +201,8 @@ const COMMAND_RENAME_MAP = {
178
201
  'new-milestone': 'milestone-new',
179
202
  'complete-milestone': 'milestone-done',
180
203
  'audit-milestone': 'milestone-audit',
181
- 'plan-milestone-gaps': 'milestone-gaps',
204
+ 'plan-milestone-gaps': 'milestone-audit --gaps',
205
+ ...Object.fromEntries(Object.entries(COMMAND_ALIASES).map(([old, a]) => [old, a.args ? `${a.to} ${a.args}` : a.to])),
182
206
  };
183
207
 
184
208
  /** Default max cycles for auto-runner */
@@ -818,6 +842,8 @@ module.exports = {
818
842
  FOCUS_SOURCES,
819
843
  DOC_SYNC_FILES,
820
844
  COMMAND_RENAME_MAP,
845
+ COMMAND_ALIASES,
846
+ RETIRED_COMMANDS,
821
847
  CATEGORY_PRIORITY_RANGE,
822
848
  CATEGORY_DEFAULTS,
823
849
  DEFAULT_MAX_CYCLES,
@@ -132,13 +132,14 @@ const MODEL_PROFILES = {
132
132
  'pan-designer': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
133
133
  'pan-roadmapper': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
134
134
  'pan-executor': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
135
- 'pan-phase-researcher': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
136
- 'pan-project-researcher': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
137
- 'pan-research-synthesizer': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
135
+ // Right-sized ceremony (2026-10-24): balanced runs the researchers and the plan-checker
136
+ // on the mid tier, about half the launch model's price; quality keeps them on reasoning.
137
+ 'pan-phase-researcher': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
138
+ 'pan-project-researcher': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
138
139
  'pan-debugger': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
139
140
  'pan-document_code': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
140
141
  'pan-verifier': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
141
- 'pan-plan-checker': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
142
+ 'pan-plan-checker': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
142
143
  'pan-design-checker': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
143
144
  'pan-integration-checker': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
144
145
  'pan-reviewer': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
@@ -193,7 +194,6 @@ const AGENT_BASE_EFFORT = {
193
194
  // Research/synthesis/review — moderate depth
194
195
  'pan-phase-researcher': 'medium',
195
196
  'pan-project-researcher': 'medium',
196
- 'pan-research-synthesizer': 'medium',
197
197
  'pan-knowledge': 'medium',
198
198
  'pan-distiller': 'medium',
199
199
  'pan-meta-reviewer': 'medium',
@@ -324,7 +324,7 @@ function reportsFailure(result) {
324
324
  */
325
325
  function output(result, raw, rawValue, exitCode) {
326
326
  if (raw && rawValue !== undefined) {
327
- process.stdout.write(String(rawValue));
327
+ writeStdoutSync(String(rawValue));
328
328
  } else {
329
329
  const json = JSON.stringify(result, null, 2);
330
330
  // Large payloads exceed Claude Code's Bash tool buffer (~50KB).
@@ -337,19 +337,31 @@ function output(result, raw, rawValue, exitCode) {
337
337
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-'));
338
338
  const tmpPath = path.join(tmpDir, 'out.json');
339
339
  fs.writeFileSync(tmpPath, json, { encoding: 'utf-8', flag: 'wx' });
340
- process.stdout.write('@file:' + tmpPath);
340
+ writeStdoutSync('@file:' + tmpPath);
341
341
  } catch {
342
342
  // Tmpfile write failed (disk full, permissions) — truncate and write to stdout
343
343
  const truncated = json.slice(0, MAX_JSON_SIZE);
344
- process.stdout.write(truncated);
344
+ writeStdoutSync(truncated);
345
345
  }
346
346
  } else {
347
- process.stdout.write(json);
347
+ writeStdoutSync(json);
348
348
  }
349
349
  }
350
350
  process.exit(exitCode === undefined ? (reportsFailure(result) ? 1 : 0) : exitCode);
351
351
  }
352
352
 
353
+ /**
354
+ * Write all of `text` to stdout before returning. output() exits right after it writes,
355
+ * and on macOS (and Linux) stdout to a pipe is asynchronous: process.exit() dropped
356
+ * whatever had not drained past the pipe buffer, so a reader got JSON cut at about 8 KB.
357
+ * Making the stdout handle blocking first turns the write synchronous on a pipe too.
358
+ */
359
+ function writeStdoutSync(text) {
360
+ const handle = process.stdout._handle;
361
+ if (handle && typeof handle.setBlocking === 'function') handle.setBlocking(true);
362
+ process.stdout.write(String(text));
363
+ }
364
+
353
365
  /**
354
366
  * Write error message to stderr and exit with code 1.
355
367
  * @param {string} message - Error message
@@ -434,7 +446,7 @@ function loadConfig(cwd) {
434
446
  branching_strategy: 'none',
435
447
  phase_branch_template: 'pan/phase-{phase}-{slug}',
436
448
  milestone_branch_template: 'pan/{milestone}-{slug}',
437
- research: true,
449
+ research: false, // opt-in: a config that says true keeps it
438
450
  plan_checker: true,
439
451
  verifier: true,
440
452
  nyquist_validation: false,
@@ -149,7 +149,8 @@ const DEFAULT_RATES = {
149
149
 
150
150
  // Google Gemini — ai.google.dev/gemini-api/docs/pricing, paid tier, text input,
151
151
  // the ≤200K-token prompt tier where the page splits by length, read 2026-09-23.
152
- // cache_read is the page's "context caching" price: 0.1× input on every row, not
152
+ // cache_read is the page's "context caching" price: 0.1× input on every row but
153
+ // Gemini 4 Argon (0.05×), not
153
154
  // the 0.25× this block carried until 2026-09-22 (cache reads 2.5× high, R26). The
154
155
  // tokens that CREATE a cache bill at the standard input price (Google Cloud's
155
156
  // context-cache overview; the Gemini API pages list only cached-token and storage
@@ -157,7 +158,12 @@ const DEFAULT_RATES = {
157
158
  // HOUR, which a per-token table cannot carry: PAN's Gemini totals exclude it.
158
159
  // The 3.6/3.7/3.8 Flash prices run "through December 31, 2026" and double from
159
160
  // January 1, 2027 (the page states both) — re-read them after that date.
160
- 'gemini-3.8-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
161
+ // Gemini 4 Argon (paid tier only; one price at every prompt length; implicit caching
162
+ // only): these rates run "through November 30, 2026" and double from December 1,
163
+ // 2026 (input $4.00, output $20.00, caching $0.20; read 2026-10-19) — change the row
164
+ // on that date.
165
+ 'gemini-4-argon': { input: 2.00, output: 10.0, cache_read: 0.10, cache_write: 2.00 },
166
+ 'gemini-3.8-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
161
167
  'gemini-3.7-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
162
168
  'gemini-3.6-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
163
169
  'gemini-3.5-flash': { input: 1.50, output: 9.00, cache_read: 0.15, cache_write: 1.50 },
@@ -899,8 +905,12 @@ function cmdCostAppend(cwd, rec, raw) {
899
905
 
900
906
  function cmdCostClear(cwd, raw) {
901
907
  try {
902
- fs.unlinkSync(tokensFile(cwd));
903
- output({ cleared: true, file: tokensFile(cwd) }, raw);
908
+ // L6a: the ledger is the only record of what was spent; keep a copy before deleting it.
909
+ const file = tokensFile(cwd);
910
+ const backup = `${file}.bak-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
911
+ fs.copyFileSync(file, backup);
912
+ fs.unlinkSync(file);
913
+ output({ cleared: true, file, backup }, raw);
904
914
  } catch (e) {
905
915
  output({ cleared: false, error: e.message || 'ledger_clear_failed' }, raw);
906
916
  }
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+ /**
3
+ * The cross-family reviewer lane (program R4): `pan-tools review cross-family <phase>`.
4
+ *
5
+ * A reviewer from the same model family as the author tends to share its blind spots.
6
+ * This lane runs one more review of the phase on another host's CLI, read-only (the
7
+ * argv is hosts.cjs `review`), and keeps its pan-verdict block beside the others, so
8
+ * review-deep can merge it as a fourth source (`--cross-file`).
9
+ *
10
+ * Opt-in per project: `workflow.cross_family_review` is `off` by default, or names a
11
+ * host. `--host` overrides it for one run; `--dry-run` prints the lane, the argv and the
12
+ * prompt and spawns nothing; `--reps N` runs it N times and reports each verdict, which
13
+ * is what the lane's evidence run needs.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawnSync } = require('child_process');
19
+ const { output, execGit, safeReadFile, toPosix } = require('./core.cjs');
20
+ const { planningPath } = require('./utils.cjs');
21
+ const { HOSTS } = require('./hosts.cjs');
22
+ const { findVerdictBlocks, validateVerdict } = require('./verdict.cjs');
23
+
24
+ const MAX_DIFF_CHARS = 120000;
25
+ // Windows caps a command line near 32k characters; a longer prompt goes through a file.
26
+ const MAX_ARG_PROMPT = 24000;
27
+
28
+ function laneHost(cwd, override) {
29
+ if (override) return override;
30
+ const wf = (require('./test-gate.cjs').rawConfig(cwd).workflow) || {};
31
+ const v = wf.cross_family_review;
32
+ return typeof v === 'string' && v && v !== 'off' ? v : null;
33
+ }
34
+
35
+ function buildPrompt(cwd, phase) {
36
+ const { reviewScope } = require('./verify-scope.cjs');
37
+ const scope = reviewScope(cwd, phase);
38
+ if (scope.error) return { error: scope.error, reason: scope.reason };
39
+ let diff = '';
40
+ if (scope.base) {
41
+ const r = execGit(cwd, ['diff', scope.base, '--', ...scope.files.map((f) => f.path)]);
42
+ if (r.exitCode === 0) diff = r.stdout;
43
+ } else if (scope.files.length) {
44
+ const r = execGit(cwd, ['diff', 'HEAD', '--', ...scope.files.map((f) => f.path)]);
45
+ if (r.exitCode === 0) diff = r.stdout;
46
+ }
47
+ const truncated = diff.length > MAX_DIFF_CHARS;
48
+ if (truncated) diff = diff.slice(0, MAX_DIFF_CHARS);
49
+ const contract = safeReadFile(path.join(__dirname, '..', '..', 'references', 'verdict-contract.md')) || '';
50
+ const intent = scope.intent.plans.map((p) => `### ${p.plan}\n${p.objective || ''}${p.review_focus ? `\n\nReview focus:\n${p.review_focus}` : ''}`).join('\n\n');
51
+ const reqs = scope.intent.requirements.map((r) => `- ${r.id}: ${r.text || '(missing)'}`).join('\n');
52
+ const prompt = [
53
+ `You are an independent code reviewer from a different model family than the author. Review phase ${scope.phase} of this project: find correctness, security and maintainability defects in the change below, judged against the plans' intent. Do not edit anything.`,
54
+ `Reply with one fenced \`pan-verdict\` block, as the contract below defines: \`"contract": "1.0"\`, \`"agent": "cross-family-reviewer"\`, \`"phase": "${scope.phase}"\`, a \`verdict\` and \`outcome\`, and one finding per defect (\`where\` as \`file:line\`). Name what you could not judge in \`declined\`.`,
55
+ '## Verdict contract', contract.trim(),
56
+ '## Intent', intent || '(no plans found)', reqs ? `## Requirements\n${reqs}` : '',
57
+ `## Files in scope\n${scope.files.map((f) => `- ${f.path}`).join('\n') || '(none)'}`,
58
+ `## Diff${scope.base ? ` (from ${scope.base.slice(0, 12)})` : ''}${truncated ? ' (truncated)' : ''}`, '```diff', diff || '(no diff)', '```',
59
+ ].filter(Boolean).join('\n\n');
60
+ return { phase: scope.phase, prompt, truncated };
61
+ }
62
+
63
+ /**
64
+ * How to launch a host binary on this platform. Off Windows: by name, no shell. On Windows
65
+ * a binary that resolves to an .exe (Copilot CLI's installer, say) is launched directly by
66
+ * its path, so Node quotes each argument and a multi-line prompt arrives whole. Anything
67
+ * else (an npm .cmd shim, a stand-in) needs cmd.exe, which joins arguments unquoted and
68
+ * ends the command at a newline: there the prompt goes through a file and each argument
69
+ * with whitespace or a quote is quoted (2026-10-23: `-p <prompt>` reached cmd.exe as words).
70
+ */
71
+ function launchPlan(bin, platform = process.platform, env = process.env) {
72
+ if (platform !== 'win32') return { file: bin, shell: false, promptFile: false };
73
+ const exts = String(env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean).map((e) => e.toLowerCase());
74
+ for (const dir of String(env.PATH || env.Path || '').split(path.delimiter).map((d) => d.trim().replace(/^"(.*)"$/, '$1')).filter(Boolean)) {
75
+ for (const ext of exts) {
76
+ const p = path.join(dir, bin + ext);
77
+ if (!fs.existsSync(p)) continue;
78
+ if (ext === '.exe' || ext === '.com') return { file: p, shell: false, promptFile: false };
79
+ return { file: bin, shell: true, promptFile: true };
80
+ }
81
+ }
82
+ return { file: bin, shell: true, promptFile: true };
83
+ }
84
+
85
+ /**
86
+ * One argument for cmd.exe's command line: quoted when it holds whitespace, a quote or one of
87
+ * cmd's operators (& | < > ^), which are literal inside quotes. Pure.
88
+ */
89
+ function quoteForCmd(arg) {
90
+ const s = String(arg);
91
+ return /[\s"&|<>^]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
92
+ }
93
+
94
+ function parseVerdict(text) {
95
+ const blocks = findVerdictBlocks(text || '');
96
+ const last = blocks.filter((b) => b.closed).pop();
97
+ if (!last) return { verdict: null, error: 'no_verdict_block' };
98
+ try {
99
+ const r = validateVerdict(JSON.parse(last.body));
100
+ return r.ok ? { verdict: r.verdict } : { verdict: null, error: r.error };
101
+ } catch { return { verdict: null, error: 'invalid_json' }; }
102
+ }
103
+
104
+ /**
105
+ * Plan or run the lane. Pure apart from reading the project and, without dryRun,
106
+ * spawning the host and writing `.planning/reviews/<NN>/cross-family-<host>.md`.
107
+ */
108
+ function crossFamilyReview(cwd, phase, opts = {}) {
109
+ if (!phase) return { error: 'phase_required', reason: 'Usage: review cross-family <phase> [--host <name>] [--reps N] [--dry-run]' };
110
+ const host = laneHost(cwd, opts.host);
111
+ if (!host) return { enabled: false, reason: 'lane_off', hint: 'set workflow.cross_family_review to a host, or pass --host' };
112
+ const h = HOSTS[host];
113
+ if (!h || !h.review) return { error: 'unknown_host', reason: `no review lane for host "${host}"`, hosts: Object.keys(HOSTS) };
114
+ const built = buildPrompt(cwd, phase);
115
+ if (built.error) return built;
116
+ const nn = String(built.phase).padStart(2, '0');
117
+ const dir = planningPath(cwd, 'reviews', nn);
118
+ let prompt = built.prompt;
119
+ let promptFile = null;
120
+ const launch = launchPlan(h.bin);
121
+ if (h.review.promptVia === 'arg' && (prompt.length > MAX_ARG_PROMPT || launch.promptFile)) {
122
+ promptFile = path.join(dir, `cross-family-${host}-prompt.md`);
123
+ prompt = `Read ${toPosix(path.relative(cwd, promptFile))} and follow it exactly. Do not edit anything.`;
124
+ }
125
+ const argv = h.review.args(h.review.promptVia === 'arg' ? prompt : '');
126
+ const base = {
127
+ enabled: true, lane: `cross-family:${host}`, host, family: h.family, bin: h.bin,
128
+ argv, prompt_via: h.review.promptVia, truncated_diff: built.truncated,
129
+ };
130
+ if (opts.dryRun) return { ...base, prompt: built.prompt, prompt_file: promptFile && toPosix(path.relative(cwd, promptFile)), dry_run: true };
131
+
132
+ fs.mkdirSync(dir, { recursive: true });
133
+ if (promptFile) fs.writeFileSync(promptFile, built.prompt);
134
+ const reps = Math.max(1, Math.min(10, Number(opts.reps) || 1));
135
+ const runs = [];
136
+ const outputs = [];
137
+ const refused = require('./autodev.cjs').standinMissing(h.bin);
138
+ if (refused) return { ...base, error: 'standin_missing', reason: refused };
139
+ for (let i = 0; i < reps; i++) {
140
+ const r = spawnSync(launch.file, launch.shell ? argv.map(quoteForCmd) : argv, {
141
+ cwd, encoding: 'utf8', input: h.review.promptVia === 'stdin' ? built.prompt : undefined,
142
+ shell: launch.shell, timeout: Number(opts.timeoutMs) || 15 * 60 * 1000, maxBuffer: 32 * 1024 * 1024,
143
+ });
144
+ if (r.error) return { ...base, error: 'spawn_failed', reason: r.error.message };
145
+ const v = parseVerdict(r.stdout);
146
+ outputs.push(r.stdout || '');
147
+ runs.push({ rep: i + 1, exit: r.status, verdict: v.verdict ? v.verdict.verdict : null, outcome: v.verdict ? v.verdict.outcome : null, findings: v.verdict ? v.verdict.findings.length : 0, error: v.error });
148
+ }
149
+ const file = path.join(dir, `cross-family-${host}.md`);
150
+ // The last parseable run is the one review-deep merges; every rep is kept for the record.
151
+ const keep = outputs.map((o, i) => [o, runs[i]]).filter(([, r]) => r.outcome).pop();
152
+ fs.writeFileSync(file, `# Cross-family review: phase ${nn} on ${host} (${h.family})\n\n${(keep ? keep[0] : outputs[outputs.length - 1]).trim()}\n`);
153
+ const counts = {};
154
+ for (const r of runs) if (r.verdict) counts[r.verdict] = (counts[r.verdict] || 0) + 1;
155
+ const majority = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
156
+ return { ...base, reps, runs, verdict: majority ? majority[0] : null, file: toPosix(path.relative(cwd, file)) };
157
+ }
158
+
159
+ function cmdReviewCrossFamily(cwd, phase, opts, raw) {
160
+ const r = crossFamilyReview(cwd, phase, opts);
161
+ output(r, raw, r.file || r.reason || r.error);
162
+ }
163
+
164
+ module.exports = { crossFamilyReview, cmdReviewCrossFamily, buildPrompt, parseVerdict, launchPlan, quoteForCmd };