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
@@ -13,6 +13,18 @@
13
13
  * its type is `stale`, not an error: what a version or a price was two months ago
14
14
  * says little about today, while a practice lasts for years. The plan-checker weighs
15
15
  * stale claims against what the plans rest on.
16
+ *
17
+ * Research answers questions (2026-10-25: research that surveyed a domain never
18
+ * finished and grew the scope). The orchestrator writes at most five open questions
19
+ * that block building; one researcher answers them in a table
20
+ *
21
+ * | # | Question | Answer | Source | Confidence | Decision it settles |
22
+ *
23
+ * `research check <questions> <research> [--market]` holds it to that: at most five
24
+ * questions, every question a row with an answer and a source, or an answer marked
25
+ * `unknown` that names the assumption the plan uses; in market mode, at most five
26
+ * comparable products. Without a `## Claims` table, `research lint` reads the
27
+ * answered rows of that table as claims, with the date in the source cell.
16
28
  */
17
29
  const fs = require('fs');
18
30
  const path = require('path');
@@ -75,10 +87,22 @@ function lintResearch(cwd, target, opts = {}) {
75
87
  const text = toLf(fs.readFileSync(at.abs, 'utf-8'));
76
88
  const base = { file, ok: true, claims: 0, errors: [], warnings: [], stale: [], windows: FRESHNESS_DAYS };
77
89
  const body = sectionBody(text, 'Claims');
90
+ let header;
91
+ let rows;
78
92
  if (body == null) {
79
- return { ...base, ok: false, errors: [{ code: 'claims_missing', detail: 'no `## Claims` table (claim · type · source · checked)' }] };
93
+ // A question-driven research file: its answered rows are the claims.
94
+ const answers = answerTable(text);
95
+ if (!answers) {
96
+ return { ...base, ok: false, errors: [{ code: 'claims_missing', detail: 'no `## Claims` table (claim · type · source · checked) and no answers table (# · question · answer · source)' }] };
97
+ }
98
+ header = ['claim', 'type', 'source', 'checked'];
99
+ rows = answers.rows.filter((r) => !isUnknown(r.answer)).map((r) => {
100
+ const date = (r.source.match(/\d{4}-\d{2}-\d{2}/) || [''])[0];
101
+ return [r.answer, 'general', r.source, date];
102
+ });
103
+ } else {
104
+ ({ header, rows } = claimRows(body));
80
105
  }
81
- const { header, rows } = claimRows(body);
82
106
  const col = (name, fallback) => {
83
107
  const i = header ? header.findIndex((h) => h.startsWith(name)) : -1;
84
108
  return i === -1 ? fallback : i;
@@ -111,6 +135,129 @@ function lintResearch(cwd, target, opts = {}) {
111
135
  return { ...base, ok: errors.length === 0, claims: rows.length, errors, warnings, stale };
112
136
  }
113
137
 
138
+ const MAX_QUESTIONS = 5;
139
+ const MAX_PRODUCTS = 5;
140
+
141
+ /** Every markdown table in `text` as arrays of cells (rule rows dropped); a non-table line ends a table. */
142
+ function tables(text) {
143
+ const out = [];
144
+ let cur = null;
145
+ for (const line of String(text).split('\n')) {
146
+ const t = line.trim();
147
+ if (!t.startsWith('|')) { cur = null; continue; }
148
+ const cells = t.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
149
+ if (cells.every((c) => /^:?-+:?$/.test(c) || c === '')) continue;
150
+ if (!cur) { cur = []; out.push(cur); }
151
+ cur.push(cells);
152
+ }
153
+ return out;
154
+ }
155
+
156
+ /** The answers table (`# · Question · Answer · Source …`), rows as objects, or null. */
157
+ function answerTable(text) {
158
+ for (const t of tables(text)) {
159
+ const h = t[0].map((c) => c.toLowerCase());
160
+ const qi = h.findIndex((c) => c.startsWith('question'));
161
+ const ai = h.findIndex((c) => c.startsWith('answer'));
162
+ const si = h.findIndex((c) => c.startsWith('source'));
163
+ if (qi === -1 || ai === -1 || si === -1) continue;
164
+ const ni = h.findIndex((c) => c === '#' || c === 'no' || c === 'n');
165
+ const ci = h.findIndex((c) => c.startsWith('confidence'));
166
+ const di = h.findIndex((c) => c.startsWith('decision'));
167
+ return {
168
+ rows: t.slice(1).map((r, i) => ({
169
+ n: ni === -1 ? i + 1 : parseInt(String(r[ni] || '').replace(/\D/g, ''), 10),
170
+ question: r[qi] || '', answer: r[ai] || '', source: r[si] || '',
171
+ confidence: ci === -1 ? '' : (r[ci] || ''), decision: di === -1 ? '' : (r[di] || ''),
172
+ })),
173
+ };
174
+ }
175
+ return null;
176
+ }
177
+
178
+ /** Rows of the comparable-products table (first header cell `Product`), or null. */
179
+ function productTable(text) {
180
+ const t = tables(text).find((tb) => /^products?$/i.test(tb[0][0] || ''));
181
+ return t ? t.slice(1) : null;
182
+ }
183
+
184
+ const isUnknown = (answer) => /^\W*unknown\b/i.test(answer);
185
+ const hasAssumption = (cell) => /assum\w*\s*[:\u2014\u2013-]?\s*\S/i.test(cell);
186
+
187
+ /** Numbered questions in a questions file: lines `1. …` or `1) …`, or `| 1 | … |` table rows. */
188
+ function readQuestions(text) {
189
+ const qs = [];
190
+ for (const line of String(text).split('\n')) {
191
+ const m = line.match(/^\s*(\d+)[.)]\s+(\S.*)$/) || line.match(/^\s*\|\s*(\d+)\s*\|\s*([^|]*\S[^|]*)\|/);
192
+ if (m) qs.push({ n: parseInt(m[1], 10), text: m[2].trim() });
193
+ }
194
+ return qs;
195
+ }
196
+
197
+ /**
198
+ * @param {string} cwd
199
+ * @param {string} questionsPath the questions file the orchestrator wrote
200
+ * @param {string} researchPath the researcher's answers
201
+ * @param {{market?: boolean}} [opts] market mode also requires the products table
202
+ */
203
+ function checkResearch(cwd, questionsPath, researchPath, opts = {}) {
204
+ const qAbs = path.resolve(cwd, String(questionsPath));
205
+ const rAbs = path.resolve(cwd, String(researchPath));
206
+ if (!fs.existsSync(qAbs)) return { error: 'file_not_found', reason: `no questions file at ${questionsPath}` };
207
+ if (!fs.existsSync(rAbs)) return { error: 'file_not_found', reason: `no research file at ${researchPath}` };
208
+ const questions = readQuestions(toLf(fs.readFileSync(qAbs, 'utf-8')));
209
+ const text = toLf(fs.readFileSync(rAbs, 'utf-8'));
210
+ const errors = [];
211
+ const warnings = [];
212
+ if (questions.length === 0) errors.push({ code: 'no_questions', detail: 'the questions file lists no numbered question: with nothing blocking the build, no research runs' });
213
+ if (questions.length > MAX_QUESTIONS) errors.push({ code: 'too_many_questions', detail: `${questions.length} questions; at most ${MAX_QUESTIONS}, the ones that block building` });
214
+ const table = answerTable(text);
215
+ let answered = 0;
216
+ let unknown = 0;
217
+ if (!table) {
218
+ errors.push({ code: 'answers_missing', detail: 'no answers table (| # | Question | Answer | Source | Confidence | Decision it settles |)' });
219
+ } else {
220
+ for (const q of questions) {
221
+ const row = table.rows.find((r) => r.n === q.n);
222
+ if (!row || !row.answer || (PLACEHOLDER_RE.test(row.answer) && !isUnknown(row.answer))) {
223
+ errors.push({ code: 'question_unanswered', question: q.n, detail: `question ${q.n} has no answer row` });
224
+ continue;
225
+ }
226
+ if (isUnknown(row.answer)) {
227
+ if (hasAssumption(`${row.answer} ${row.decision}`)) unknown++;
228
+ else errors.push({ code: 'unknown_without_assumption', question: q.n, detail: `question ${q.n} is unknown but names no assumption the plan should use` });
229
+ continue;
230
+ }
231
+ if (!row.source || PLACEHOLDER_RE.test(row.source)) {
232
+ errors.push({ code: 'answer_no_source', question: q.n, detail: `question ${q.n} has an answer with no source (URL or doc, and date)` });
233
+ continue;
234
+ }
235
+ if (!/^(high|medium|low)\b/i.test(row.confidence)) warnings.push({ code: 'answer_no_confidence', question: q.n, detail: `question ${q.n} gives no HIGH, MEDIUM or LOW confidence` });
236
+ answered++;
237
+ }
238
+ const known = new Set(questions.map((q) => q.n));
239
+ for (const r of table.rows) {
240
+ if (!known.has(r.n)) warnings.push({ code: 'extra_row', row: r.n, detail: `row ${r.n} answers no listed question: new scope goes under Ideas, not into the answers` });
241
+ }
242
+ }
243
+ const products = productTable(text);
244
+ if (products && products.length > MAX_PRODUCTS) errors.push({ code: 'too_many_products', detail: `${products.length} comparable products; at most ${MAX_PRODUCTS}` });
245
+ if (opts.market && !products) errors.push({ code: 'products_missing', detail: 'market mode: no table whose first column is Product' });
246
+ const rel = (abs) => { const r = path.relative(path.resolve(cwd), abs); return r && !r.startsWith('..') ? toPosix(r) : toPosix(abs); };
247
+ return {
248
+ questions_file: rel(qAbs), research_file: rel(rAbs), ok: errors.length === 0,
249
+ questions: questions.length, answered, unknown, products: products ? products.length : 0,
250
+ limits: { questions: MAX_QUESTIONS, products: MAX_PRODUCTS }, errors, warnings,
251
+ };
252
+ }
253
+
254
+ function cmdResearchCheck(cwd, questionsPath, researchPath, opts, raw) {
255
+ if (!questionsPath || !researchPath) error('Usage: research check <questions file> <research file> [--market]');
256
+ const r = checkResearch(cwd, questionsPath, researchPath, opts);
257
+ if (r.error) { output(r, raw); return; }
258
+ output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
259
+ }
260
+
114
261
  function cmdResearchLint(cwd, target, opts, raw) {
115
262
  if (!target) error('Usage: research lint <phase|path> [--now YYYY-MM-DD]');
116
263
  const r = lintResearch(cwd, target, opts);
@@ -118,4 +265,4 @@ function cmdResearchLint(cwd, target, opts, raw) {
118
265
  output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
119
266
  }
120
267
 
121
- module.exports = { FRESHNESS_DAYS, lintResearch, cmdResearchLint };
268
+ module.exports = { FRESHNESS_DAYS, MAX_QUESTIONS, MAX_PRODUCTS, lintResearch, cmdResearchLint, checkResearch, cmdResearchCheck };
@@ -14,14 +14,14 @@
14
14
  * - writeDeepReview(cwd, phaseNum, payload) — serialize the merged output
15
15
  * to .planning/reviews/<N>/deep-review.md
16
16
  *
17
- * Agents publish to `review-handoff` channel via bus.cjs for audit trail.
17
+ * It used to publish each merge to a `review-handoff` bus channel that nothing read;
18
+ * deep-review.md is the record (program L7).
18
19
  */
19
20
 
20
21
  const fs = require('fs');
21
22
  const path = require('path');
22
23
  const { output, error, safeReadFile, toPosix, escapeTableCell } = require('./core.cjs');
23
24
  const { planningPath } = require('./utils.cjs');
24
- const { publish } = require('./bus.cjs');
25
25
  const { findVerdictBlocks, validateVerdict } = require('./verdict.cjs');
26
26
 
27
27
  const REVIEWS_DIR = 'reviews';
@@ -126,14 +126,16 @@ function findingsFromVerdictBlock(content, source) {
126
126
  * @param {Array|string} reviewer - reviewer findings array OR markdown content
127
127
  * @param {Array|string} hardener - hardener findings array OR markdown content
128
128
  * @param {Array|string} [meta] - optional meta-reviewer findings
129
+ * @param {Array|string} [cross] - optional cross-family lane findings (R4)
129
130
  * @returns {Object} Merged payload
130
131
  */
131
- function mergeReviews(reviewer, hardener, meta) {
132
+ function mergeReviews(reviewer, hardener, meta, cross) {
132
133
  const r = Array.isArray(reviewer) ? reviewer : parseReviewFindings(reviewer || '', 'reviewer');
133
134
  const h = Array.isArray(hardener) ? hardener : parseReviewFindings(hardener || '', 'hardener');
134
135
  const m = Array.isArray(meta) ? meta : parseReviewFindings(meta || '', 'meta-reviewer');
136
+ const x = Array.isArray(cross) ? cross : parseReviewFindings(cross || '', 'cross-family');
135
137
 
136
- const findings = [...r, ...h, ...m].sort((a, b) => {
138
+ const findings = [...r, ...h, ...m, ...x].sort((a, b) => {
137
139
  const wa = SEVERITY_WEIGHT[a.severity] ?? 0;
138
140
  const wb = SEVERITY_WEIGHT[b.severity] ?? 0;
139
141
  if (wa !== wb) return wb - wa;
@@ -174,6 +176,7 @@ function mergeReviews(reviewer, hardener, meta) {
174
176
  reviewer: r.length,
175
177
  hardener: h.length,
176
178
  meta_reviewer: m.length,
179
+ cross_family: x.length,
177
180
  },
178
181
  by_severity: SEVERITIES.reduce((acc, s) => { acc[s] = findings.filter(f => f.severity === s).length; return acc; }, {}),
179
182
  };
@@ -263,19 +266,6 @@ function writeDeepReview(cwd, phaseNum, payload, opts) {
263
266
  return { error: `Failed to write ${file}: ${e.message}` };
264
267
  }
265
268
 
266
- // Audit trail on the review-handoff bus channel (best-effort).
267
- if (opts?.audit_channel !== false) {
268
- try {
269
- publish(cwd, 'review-handoff', {
270
- phase: phaseNum,
271
- verdict: payload.verdict,
272
- finding_count: payload.coverage.total,
273
- conflict_count: payload.conflicts.length,
274
- file: toPosix(path.relative(cwd, file)),
275
- }, { source: 'pan-meta-reviewer' });
276
- } catch { /* non-blocking */ }
277
- }
278
-
279
269
  return { written: true, file: toPosix(path.relative(cwd, file)) };
280
270
  }
281
271
 
@@ -290,15 +280,16 @@ function resolveAgainstCwd(cwd, p) {
290
280
  }
291
281
 
292
282
  function cmdReviewDeepMerge(cwd, phaseNum, opts, raw) {
293
- if (!phaseNum) error('Usage: review-deep merge <phase> --reviewer-file X --hardener-file Y [--meta-file Z]');
283
+ if (!phaseNum) error('Usage: review-deep merge <phase> --reviewer-file X --hardener-file Y [--meta-file Z] [--cross-file W]');
294
284
  const reviewerContent = opts.reviewerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.reviewerFile)) : '';
295
285
  const hardenerContent = opts.hardenerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.hardenerFile)) : '';
296
286
  const metaContent = opts.metaFile ? safeReadFile(resolveAgainstCwd(cwd, opts.metaFile)) : '';
297
- if (!reviewerContent && !hardenerContent && !metaContent) {
287
+ const crossContent = opts.crossFile ? safeReadFile(resolveAgainstCwd(cwd, opts.crossFile)) : '';
288
+ if (!reviewerContent && !hardenerContent && !metaContent && !crossContent) {
298
289
  output({ error: 'No input files provided or readable' }, raw);
299
290
  return;
300
291
  }
301
- const payload = mergeReviews(reviewerContent, hardenerContent, metaContent);
292
+ const payload = mergeReviews(reviewerContent, hardenerContent, metaContent, crossContent);
302
293
  const result = writeDeepReview(cwd, phaseNum, payload);
303
294
  if (result.error) { output(result, raw); return; }
304
295
  output({ ...result, verdict: payload.verdict, coverage: payload.coverage, conflicts: payload.conflicts.length }, raw);
@@ -306,11 +297,12 @@ function cmdReviewDeepMerge(cwd, phaseNum, opts, raw) {
306
297
 
307
298
  function cmdReviewDeepAnalyze(cwd, phaseNum, opts, raw) {
308
299
  // Returns the merged payload WITHOUT writing a file. Useful for piping.
309
- if (!phaseNum) error('Usage: review-deep analyze <phase> --reviewer-file X --hardener-file Y [--meta-file Z]');
300
+ if (!phaseNum) error('Usage: review-deep analyze <phase> --reviewer-file X --hardener-file Y [--meta-file Z] [--cross-file W]');
310
301
  const reviewerContent = opts.reviewerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.reviewerFile)) : '';
311
302
  const hardenerContent = opts.hardenerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.hardenerFile)) : '';
312
303
  const metaContent = opts.metaFile ? safeReadFile(resolveAgainstCwd(cwd, opts.metaFile)) : '';
313
- output(mergeReviews(reviewerContent, hardenerContent, metaContent), raw);
304
+ const crossContent = opts.crossFile ? safeReadFile(resolveAgainstCwd(cwd, opts.crossFile)) : '';
305
+ output(mergeReviews(reviewerContent, hardenerContent, metaContent, crossContent), raw);
314
306
  }
315
307
 
316
308
  module.exports = {
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+ /**
3
+ * Review lenses and k-pass voting (program R2).
4
+ *
5
+ * With `workflow.review_mode: lenses`, exec-phase runs the reviewer once per lens
6
+ * (LENSES) instead of once, and `review vote` merges the passes in code. A finding
7
+ * survives only when a quorum of passes reported it (the validator: a finding one
8
+ * pass alone raised is a likely false alarm), at the highest severity any pass gave
9
+ * it. The verdict is recomputed from what survived, never taken from one pass. The
10
+ * merged report carries a normal `pan-verdict` block, so `findings record` reads it
11
+ * like any single review.
12
+ *
13
+ * Opt-in until its evidence gate decides: a paired corpus run must show lenses
14
+ * catching more without raising false alarms (docs/specs/product-excellence-superplan.md).
15
+ */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { output, error } = require('./core.cjs');
19
+ const V = require('./verdict.cjs');
20
+ const { sameFinding } = require('./findings.cjs');
21
+
22
+ const LENSES = Object.freeze(['correctness', 'security', 'maintainability']);
23
+ const SEVERITY_RANK = Object.fromEntries(V.SEVERITIES.map((s, i) => [s, V.SEVERITIES.length - i]));
24
+ const FENCE = '```';
25
+
26
+ /**
27
+ * Merge reviewer passes.
28
+ * @param {string[]} texts - each pass's full report (with its pan-verdict block)
29
+ * @param {{phase?: string, quorum?: number}} [opts]
30
+ * @returns {{ok:true, passes, quorum, verdict, dropped, text}|{ok:false, error, reason}}
31
+ */
32
+ function voteVerdicts(texts, opts = {}) {
33
+ const parsed = [];
34
+ for (let i = 0; i < texts.length; i++) {
35
+ const p = V.parseVerdictText(String(texts[i] || ''), { agent: 'pan-reviewer', phase: opts.phase });
36
+ if (!p.ok) return { ok: false, error: 'unreadable_pass', reason: `pass ${i + 1} has no readable pan-verdict block (${p.error})` };
37
+ parsed.push(p.verdict);
38
+ }
39
+ if (parsed.length === 0) return { ok: false, error: 'no_passes', reason: 'review vote needs at least one pass' };
40
+ const quorum = Number.isInteger(opts.quorum) && opts.quorum > 0 ? opts.quorum : Math.ceil(parsed.length / 2);
41
+
42
+ // Cluster findings across passes; a pass counts once per cluster.
43
+ const clusters = [];
44
+ parsed.forEach((v, pass) => {
45
+ for (const f of v.findings) {
46
+ const c = clusters.find((x) => sameFinding(x.finding, f));
47
+ if (c) {
48
+ c.passes.add(pass);
49
+ if ((SEVERITY_RANK[f.severity] || 0) > (SEVERITY_RANK[c.finding.severity] || 0)) c.finding = { ...c.finding, severity: f.severity };
50
+ } else {
51
+ clusters.push({ finding: { ...f }, passes: new Set([pass]) });
52
+ }
53
+ }
54
+ });
55
+ const kept = clusters.filter((c) => c.passes.size >= quorum).map((c) => ({ ...c.finding, votes: c.passes.size }));
56
+ const dropped = clusters.filter((c) => c.passes.size < quorum).map((c) => ({ ...c.finding, votes: c.passes.size }));
57
+
58
+ const counted = kept.filter((f) => !f.pre_existing);
59
+ const errors = counted.some((f) => f.severity === 'critical' || f.severity === 'high');
60
+ const warnings = counted.some((f) => f.severity === 'medium' || f.severity === 'low');
61
+ const verdictWord = errors ? 'NEEDS_FIXES' : (warnings ? 'PASS_WITH_WARNINGS' : 'PASS');
62
+
63
+ const declined = [];
64
+ for (const v of parsed) for (const d of v.declined || []) if (!declined.some((x) => x.behaviour === d.behaviour)) declined.push(d);
65
+ const notChecked = [];
66
+ for (const v of parsed) for (const n of v.not_checked || []) if (!notChecked.some((x) => x.check === n.check)) notChecked.push(n);
67
+
68
+ const verdict = {
69
+ contract: V.VERDICT_CONTRACT, agent: 'pan-reviewer', phase: opts.phase || parsed[0].phase || null,
70
+ verdict: verdictWord, outcome: errors ? 'fail' : 'pass',
71
+ findings: kept.map(({ votes, ...f }) => ({ ...f, votes })),
72
+ ...(declined.length ? { declined } : {}),
73
+ ...(notChecked.length ? { not_checked: notChecked } : {}),
74
+ };
75
+ const lines = [
76
+ `## Code Review — Phase ${verdict.phase || '?'} (${parsed.length} passes, quorum ${quorum})`,
77
+ '',
78
+ `### Verdict`,
79
+ verdictWord,
80
+ '',
81
+ `Kept ${kept.length} finding(s) at least ${quorum} pass(es) reported; dropped ${dropped.length} only fewer passes raised.`,
82
+ ...kept.map((f) => `- ${f.severity.toUpperCase()} ${f.where || '(no location)'}: ${f.summary} (${f.votes}/${parsed.length})`),
83
+ ...(dropped.length ? ['', 'Dropped (below quorum):', ...dropped.map((f) => `- ${f.severity} ${f.where || '(no location)'}: ${f.summary} (${f.votes}/${parsed.length})`)] : []),
84
+ '',
85
+ `${FENCE}pan-verdict`,
86
+ JSON.stringify(verdict),
87
+ FENCE,
88
+ '',
89
+ ];
90
+ return { ok: true, passes: parsed.length, quorum, verdict, dropped, text: lines.join('\n') };
91
+ }
92
+
93
+ function cmdReviewVote(cwd, opts, raw) {
94
+ const files = opts.files || [];
95
+ if (!files.length) error('Usage: review vote [--phase N] [--quorum K] [--out <file>] <pass-file>...');
96
+ const texts = files.map((f) => {
97
+ try { return fs.readFileSync(path.isAbsolute(f) ? f : path.join(cwd, f), 'utf8'); } catch { return ''; }
98
+ });
99
+ const r = voteVerdicts(texts, { phase: opts.phase, quorum: opts.quorum ? Number(opts.quorum) : undefined });
100
+ if (!r.ok) { output(r, raw, 'error', 1); return; }
101
+ if (opts.out) fs.writeFileSync(path.isAbsolute(opts.out) ? opts.out : path.join(cwd, opts.out), r.text);
102
+ output({ ok: true, passes: r.passes, quorum: r.quorum, kept: r.verdict.findings.length, dropped: r.dropped.length, verdict: r.verdict.verdict, out: opts.out || null }, raw, r.verdict.verdict);
103
+ }
104
+
105
+ module.exports = { LENSES, voteVerdicts, cmdReviewVote };
@@ -55,22 +55,8 @@ const { getExperimentManifest, PAN_EXPERIMENTS_ROOT_DEFAULT } = require('./exper
55
55
  // claude into `--output-format json` so the trailing usage envelope can be
56
56
  // parsed for cost/token metrics. Other runtimes are unchanged — token
57
57
  // metering for codex/gemini/opencode is deferred (no equivalent flag).
58
- const RUNTIME_RUNNERS = Object.freeze({
59
- claude: {
60
- bin: 'claude',
61
- buildArgs: (prompt, opts) => {
62
- const args = ['-p', '--dangerously-skip-permissions'];
63
- if (opts && opts.captureMetrics) args.push('--output-format', 'json');
64
- args.push(prompt);
65
- return args;
66
- },
67
- shell: 'win32',
68
- },
69
- codex: { bin: 'codex', buildArgs: (prompt) => ['exec', prompt], shell: 'win32' },
70
- gemini: { bin: 'gemini', buildArgs: (prompt) => ['-p', '--yolo', prompt], shell: 'win32' },
71
- opencode: { bin: 'opencode', buildArgs: (prompt) => [prompt], shell: 'win32' },
72
- copilot: null,
73
- });
58
+ // The adapters live in hosts.cjs, the one host table (program A0).
59
+ const RUNTIME_RUNNERS = require('./hosts.cjs').runnerTable();
74
60
 
75
61
  // ── Stop reasons (enum-ish) ─────────────────────────────────────────────────
76
62