alphacouncil-agent 0.9.2 → 1.0.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 (493) hide show
  1. package/.claude/commands/alpha.md +22 -3
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +4 -4
  5. package/.grok/commands/alpha.md +22 -3
  6. package/.opencode/command/alpha.md +22 -3
  7. package/AGENTS.md +42 -4
  8. package/CHANGELOG.md +164 -0
  9. package/CLAUDE.md +59 -7
  10. package/README.ja.md +25 -11
  11. package/README.md +44 -12
  12. package/README.zh-CN.md +37 -12
  13. package/commands/alpha.md +22 -3
  14. package/data/authored/core-seats.mjs +808 -0
  15. package/data/authored/growth-seats.mjs +581 -0
  16. package/data/authored/index.mjs +114 -0
  17. package/data/authored/quant-seats.mjs +564 -0
  18. package/data/authored/value-seats.mjs +663 -0
  19. package/data/build-profile.v1.json +2 -1
  20. package/data/council-evaluation-protocol.v1.json +7 -4
  21. package/data/master-selector-method-locales.v1.mjs +296 -0
  22. package/data/persona-v3-ai-assisted-solo-profile.v1.json +2 -2
  23. package/data/persona-v3-build-specs.v1.mjs +60 -3
  24. package/docs/INSTALL.md +43 -6
  25. package/docs/personas.md +2 -1
  26. package/docs/plans/v1.0-roadmap.md +487 -0
  27. package/docs/releases/v0.9.3.md +86 -0
  28. package/docs/releases/v0.9.4.md +79 -0
  29. package/docs/releases/v0.9.5.md +96 -0
  30. package/docs/releases/v1.0.0.md +164 -0
  31. package/docs/report-contract.md +75 -2
  32. package/knowledge/ai-assisted-solo/experiments/runs/a.json +13 -13
  33. package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
  34. package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
  35. package/knowledge/ai-assisted-solo/experiments/runs/d13.json +126 -126
  36. package/knowledge/ai-assisted-solo/experiments/runs/d26.json +274 -254
  37. package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +129 -129
  38. package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +277 -257
  39. package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +278 -258
  40. package/knowledge/ai-assisted-solo/experiments/simulation-input.json +7278 -556
  41. package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +22 -22
  42. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/{change_value_bridge.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  43. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/{power_map.ai-review.json → owner_earnings_yield.ai-review.json} +46 -46
  44. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/{compute_power_bridge.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  45. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/{timeline_reverse_valuation.ai-review.json → price_implied_cash_yield.ai-review.json} +46 -46
  46. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/{crowding_cost_stress.ai-review.json → nominal_earnings_yield_gap.ai-review.json} +49 -49
  47. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/{factor_decomposer.ai-review.json → real_earnings_yield_gap.ai-review.json} +49 -49
  48. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/expected_return_over_long_bond.ai-review.json +277 -0
  49. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/fundamental_expected_return.ai-review.json +277 -0
  50. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/{incremental_returns.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  51. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/{owner_earnings_rebuilder.ai-review.json → owner_earnings_yield.ai-review.json} +46 -46
  52. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_burry/reviews/{mispricing_carry.ai-review.json → net_current_asset_surplus_per_share.ai-review.json} +49 -49
  53. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_burry/reviews/{capital_structure_reader.ai-review.json → net_current_asset_value_per_share.ai-review.json} +46 -46
  54. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/{cost_adoption_curve.ai-review.json → current_cash_yield.ai-review.json} +46 -46
  55. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/{capture_valuation_bridge.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  56. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/{debt_driver_stress.ai-review.json → policy_real_rate.ai-review.json} +49 -49
  57. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/{regime_classifier.ai-review.json → real_curve_slope.ai-review.json} +49 -49
  58. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/{reverse_valuation.ai-review.json → implied_equity_risk_premium.ai-review.json} +49 -49
  59. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/{story_dcf.ai-review.json → premium_versus_long_run_average.ai-review.json} +49 -49
  60. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/{liquidity_revision_map.ai-review.json → liquidity_curve_impulse.ai-review.json} +49 -49
  61. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/{inflection_payoff.ai-review.json → policy_real_rate.ai-review.json} +49 -49
  62. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/{opportunity_cost.ai-review.json → five_year_opportunity_cost.ai-review.json} +46 -46
  63. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/{user_value_evidence.ai-review.json → margin_retention.ai-review.json} +46 -46
  64. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/{research_productivity.ai-review.json → margin_retention.ai-review.json} +46 -46
  65. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/{scuttlebutt_graph.ai-review.json → reinvestment_spread.ai-review.json} +49 -49
  66. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/{borrow_catalyst.ai-review.json → cash_conversion_gap.ai-review.json} +49 -49
  67. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/{accounting_rebuilder.ai-review.json → cash_cover_of_leverage.ai-review.json} +46 -46
  68. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/{asset_floor.ai-review.json → defensive_hurdle_base.ai-review.json} +46 -46
  69. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/{normalized_earnings.ai-review.json → half_normalised_earnings_yield.ai-review.json} +46 -46
  70. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/{penetration_liquidity.ai-review.json → cash_quality_gap.ai-review.json} +49 -49
  71. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/{promoter_governance.ai-review.json → real_structural_growth.ai-review.json} +49 -49
  72. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/{recovery_waterfall.ai-review.json → downside_asset_coverage.ai-review.json} +46 -46
  73. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/{cash_catalyst_compare.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  74. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/{promise_integrity_ledger.ai-review.json → cash_backed_decade_return.ai-review.json} +46 -46
  75. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/{ten_year_return_bridge.ai-review.json → decade_return_over_long_bond.ai-review.json} +49 -49
  76. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/{story_numbers_check.ai-review.json → growth_justified_capitalisation.ai-review.json} +46 -46
  77. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/{category_classifier.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  78. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/{cycle_temperature.ai-review.json → credit_cycle_position.ai-review.json} +49 -49
  79. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/{implied_expectations.ai-review.json → equity_over_corporate_debt.ai-review.json} +49 -49
  80. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/{failure_path_graph.ai-review.json → debt_service_cushion.ai-review.json} +49 -49
  81. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/{incentive_map.ai-review.json → reported_earnings_cash_gap.ai-review.json} +49 -49
  82. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/{surface_builder.ai-review.json → normalised_skew.ai-review.json} +46 -46
  83. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/{greeks_payoff.ai-review.json → round_trip_cost.ai-review.json} +46 -46
  84. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/{downside_floor.ai-review.json → downside_floor_coverage.ai-review.json} +46 -46
  85. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/{discrete_payoff.ai-review.json → market_capitalisation.ai-review.json} +46 -46
  86. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/{oos_cost_test.ai-review.json → absolute_session_move.ai-review.json} +42 -42
  87. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/{leakage_audit.ai-review.json → session_move_over_implied.ai-review.json} +46 -46
  88. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/{realized_vol_forecast.ai-review.json → round_trip_cost.ai-review.json} +46 -46
  89. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/{net_edge_sizer.ai-review.json → volatility_per_unit_of_width.ai-review.json} +46 -46
  90. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/{reversal_monitor.ai-review.json → credit_cycle_position.ai-review.json} +49 -49
  91. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/{reflexivity_graph.ai-review.json → financing_burden.ai-review.json} +46 -46
  92. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/{tail_friction.ai-review.json → debt_service_cushion.ai-review.json} +49 -49
  93. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/{payoff_ruin.ai-review.json → normalised_skew.ai-review.json} +46 -46
  94. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/{edge_recalculator.ai-review.json → edge_per_unit_of_volatility.ai-review.json} +46 -46
  95. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/{fractional_kelly.ai-review.json → net_edge.ai-review.json} +49 -49
  96. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/review-manifest.json +288 -270
  97. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-semantic-adjudications/index.json +2 -2
  98. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-semantic-extractions/index.json +2 -2
  99. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-semantic-skeptic-reviews/index.json +2 -2
  100. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-source-prereviews/index.json +6 -2
  101. package/knowledge/solo-test/masters/master_ackman/decision_policy.json +122 -11
  102. package/knowledge/solo-test/masters/master_ackman/doctrine.jsonl +2 -2
  103. package/knowledge/solo-test/masters/master_ackman/manifest.json +27 -10
  104. package/knowledge/solo-test/masters/master_ackman/provisional-index.json +3 -3
  105. package/knowledge/solo-test/masters/master_ackman/sources.jsonl +1 -1
  106. package/knowledge/solo-test/masters/master_ackman/tools.json +56 -30
  107. package/knowledge/solo-test/masters/master_aschenbrenner/decision_policy.json +101 -10
  108. package/knowledge/solo-test/masters/master_aschenbrenner/doctrine.jsonl +2 -2
  109. package/knowledge/solo-test/masters/master_aschenbrenner/manifest.json +26 -8
  110. package/knowledge/solo-test/masters/master_aschenbrenner/provisional-index.json +3 -3
  111. package/knowledge/solo-test/masters/master_aschenbrenner/sources.jsonl +1 -1
  112. package/knowledge/solo-test/masters/master_aschenbrenner/tools.json +56 -30
  113. package/knowledge/solo-test/masters/master_asness/decision_policy.json +89 -10
  114. package/knowledge/solo-test/masters/master_asness/doctrine.jsonl +2 -2
  115. package/knowledge/solo-test/masters/master_asness/manifest.json +25 -8
  116. package/knowledge/solo-test/masters/master_asness/provisional-index.json +3 -3
  117. package/knowledge/solo-test/masters/master_asness/sources.jsonl +1 -1
  118. package/knowledge/solo-test/masters/master_asness/tools.json +56 -30
  119. package/knowledge/solo-test/masters/master_bogle/counterfactuals.jsonl +0 -0
  120. package/knowledge/solo-test/masters/master_bogle/decision_cases.jsonl +0 -0
  121. package/knowledge/solo-test/masters/master_bogle/decision_policy.json +170 -0
  122. package/knowledge/solo-test/masters/master_bogle/doctrine.jsonl +14 -0
  123. package/knowledge/solo-test/masters/master_bogle/evaluation/calibration_cases.jsonl +0 -0
  124. package/knowledge/solo-test/masters/master_bogle/evaluation/experiments.json +5 -0
  125. package/knowledge/solo-test/masters/master_bogle/evaluation/golden_cases.jsonl +0 -0
  126. package/knowledge/solo-test/masters/master_bogle/evaluation/pairwise_cases.jsonl +0 -0
  127. package/knowledge/solo-test/masters/master_bogle/failures.jsonl +0 -0
  128. package/knowledge/solo-test/masters/master_bogle/manifest.json +141 -0
  129. package/knowledge/solo-test/masters/master_bogle/memory_policy.json +5 -0
  130. package/knowledge/solo-test/masters/master_bogle/provisional-index.json +41 -0
  131. package/knowledge/solo-test/masters/master_bogle/research_policy.json +11 -0
  132. package/knowledge/solo-test/masters/master_bogle/sources.jsonl +1 -0
  133. package/knowledge/solo-test/masters/master_bogle/tools.json +120 -0
  134. package/knowledge/solo-test/masters/master_bogle/voice.en.md +7 -0
  135. package/knowledge/solo-test/masters/master_bogle/voice.zh.md +7 -0
  136. package/knowledge/solo-test/masters/master_buffett/decision_policy.json +102 -11
  137. package/knowledge/solo-test/masters/master_buffett/doctrine.jsonl +2 -2
  138. package/knowledge/solo-test/masters/master_buffett/manifest.json +24 -8
  139. package/knowledge/solo-test/masters/master_buffett/provisional-index.json +3 -3
  140. package/knowledge/solo-test/masters/master_buffett/sources.jsonl +1 -1
  141. package/knowledge/solo-test/masters/master_buffett/tools.json +55 -29
  142. package/knowledge/solo-test/masters/master_burry/decision_policy.json +102 -12
  143. package/knowledge/solo-test/masters/master_burry/doctrine.jsonl +2 -2
  144. package/knowledge/solo-test/masters/master_burry/manifest.json +26 -10
  145. package/knowledge/solo-test/masters/master_burry/provisional-index.json +3 -3
  146. package/knowledge/solo-test/masters/master_burry/sources.jsonl +1 -1
  147. package/knowledge/solo-test/masters/master_burry/tools.json +56 -30
  148. package/knowledge/solo-test/masters/master_cathie_wood/decision_policy.json +74 -12
  149. package/knowledge/solo-test/masters/master_cathie_wood/doctrine.jsonl +2 -2
  150. package/knowledge/solo-test/masters/master_cathie_wood/manifest.json +26 -10
  151. package/knowledge/solo-test/masters/master_cathie_wood/provisional-index.json +3 -3
  152. package/knowledge/solo-test/masters/master_cathie_wood/sources.jsonl +1 -1
  153. package/knowledge/solo-test/masters/master_cathie_wood/tools.json +56 -30
  154. package/knowledge/solo-test/masters/master_dalio/decision_policy.json +123 -16
  155. package/knowledge/solo-test/masters/master_dalio/doctrine.jsonl +2 -2
  156. package/knowledge/solo-test/masters/master_dalio/manifest.json +24 -8
  157. package/knowledge/solo-test/masters/master_dalio/provisional-index.json +3 -3
  158. package/knowledge/solo-test/masters/master_dalio/sources.jsonl +1 -1
  159. package/knowledge/solo-test/masters/master_dalio/tools.json +56 -30
  160. package/knowledge/solo-test/masters/master_damodaran/decision_policy.json +58 -10
  161. package/knowledge/solo-test/masters/master_damodaran/doctrine.jsonl +2 -2
  162. package/knowledge/solo-test/masters/master_damodaran/manifest.json +23 -8
  163. package/knowledge/solo-test/masters/master_damodaran/provisional-index.json +3 -3
  164. package/knowledge/solo-test/masters/master_damodaran/sources.jsonl +1 -1
  165. package/knowledge/solo-test/masters/master_damodaran/tools.json +54 -28
  166. package/knowledge/solo-test/masters/master_druckenmiller/decision_policy.json +89 -10
  167. package/knowledge/solo-test/masters/master_druckenmiller/doctrine.jsonl +2 -2
  168. package/knowledge/solo-test/masters/master_druckenmiller/manifest.json +25 -8
  169. package/knowledge/solo-test/masters/master_druckenmiller/provisional-index.json +3 -3
  170. package/knowledge/solo-test/masters/master_druckenmiller/sources.jsonl +1 -1
  171. package/knowledge/solo-test/masters/master_druckenmiller/tools.json +55 -29
  172. package/knowledge/solo-test/masters/master_duan_yongping/decision_policy.json +138 -13
  173. package/knowledge/solo-test/masters/master_duan_yongping/doctrine.jsonl +2 -2
  174. package/knowledge/solo-test/masters/master_duan_yongping/manifest.json +31 -11
  175. package/knowledge/solo-test/masters/master_duan_yongping/provisional-index.json +3 -3
  176. package/knowledge/solo-test/masters/master_duan_yongping/sources.jsonl +1 -1
  177. package/knowledge/solo-test/masters/master_duan_yongping/tools.json +82 -30
  178. package/knowledge/solo-test/masters/master_fisher/decision_policy.json +102 -11
  179. package/knowledge/solo-test/masters/master_fisher/doctrine.jsonl +2 -2
  180. package/knowledge/solo-test/masters/master_fisher/manifest.json +27 -10
  181. package/knowledge/solo-test/masters/master_fisher/provisional-index.json +3 -3
  182. package/knowledge/solo-test/masters/master_fisher/sources.jsonl +1 -1
  183. package/knowledge/solo-test/masters/master_fisher/tools.json +58 -32
  184. package/knowledge/solo-test/masters/master_forensic_short/decision_policy.json +93 -14
  185. package/knowledge/solo-test/masters/master_forensic_short/doctrine.jsonl +2 -2
  186. package/knowledge/solo-test/masters/master_forensic_short/manifest.json +20 -8
  187. package/knowledge/solo-test/masters/master_forensic_short/provisional-index.json +3 -3
  188. package/knowledge/solo-test/masters/master_forensic_short/sources.jsonl +1 -1
  189. package/knowledge/solo-test/masters/master_forensic_short/tools.json +53 -27
  190. package/knowledge/solo-test/masters/master_graham/decision_policy.json +103 -12
  191. package/knowledge/solo-test/masters/master_graham/doctrine.jsonl +2 -2
  192. package/knowledge/solo-test/masters/master_graham/manifest.json +26 -8
  193. package/knowledge/solo-test/masters/master_graham/provisional-index.json +3 -3
  194. package/knowledge/solo-test/masters/master_graham/sources.jsonl +1 -1
  195. package/knowledge/solo-test/masters/master_graham/tools.json +67 -28
  196. package/knowledge/solo-test/masters/master_jhunjhunwala/decision_policy.json +77 -12
  197. package/knowledge/solo-test/masters/master_jhunjhunwala/doctrine.jsonl +2 -2
  198. package/knowledge/solo-test/masters/master_jhunjhunwala/manifest.json +25 -10
  199. package/knowledge/solo-test/masters/master_jhunjhunwala/provisional-index.json +3 -3
  200. package/knowledge/solo-test/masters/master_jhunjhunwala/sources.jsonl +1 -1
  201. package/knowledge/solo-test/masters/master_jhunjhunwala/tools.json +54 -28
  202. package/knowledge/solo-test/masters/master_klarman/decision_policy.json +105 -14
  203. package/knowledge/solo-test/masters/master_klarman/doctrine.jsonl +2 -2
  204. package/knowledge/solo-test/masters/master_klarman/manifest.json +22 -6
  205. package/knowledge/solo-test/masters/master_klarman/provisional-index.json +3 -3
  206. package/knowledge/solo-test/masters/master_klarman/sources.jsonl +1 -1
  207. package/knowledge/solo-test/masters/master_klarman/tools.json +55 -29
  208. package/knowledge/solo-test/masters/master_li_lu/decision_policy.json +101 -10
  209. package/knowledge/solo-test/masters/master_li_lu/doctrine.jsonl +2 -2
  210. package/knowledge/solo-test/masters/master_li_lu/manifest.json +27 -10
  211. package/knowledge/solo-test/masters/master_li_lu/provisional-index.json +3 -3
  212. package/knowledge/solo-test/masters/master_li_lu/sources.jsonl +1 -1
  213. package/knowledge/solo-test/masters/master_li_lu/tools.json +58 -32
  214. package/knowledge/solo-test/masters/master_lynch/decision_policy.json +127 -16
  215. package/knowledge/solo-test/masters/master_lynch/doctrine.jsonl +2 -2
  216. package/knowledge/solo-test/masters/master_lynch/manifest.json +28 -10
  217. package/knowledge/solo-test/masters/master_lynch/provisional-index.json +3 -3
  218. package/knowledge/solo-test/masters/master_lynch/sources.jsonl +1 -1
  219. package/knowledge/solo-test/masters/master_lynch/tools.json +67 -28
  220. package/knowledge/solo-test/masters/master_marks/decision_policy.json +71 -9
  221. package/knowledge/solo-test/masters/master_marks/doctrine.jsonl +2 -2
  222. package/knowledge/solo-test/masters/master_marks/manifest.json +25 -10
  223. package/knowledge/solo-test/masters/master_marks/provisional-index.json +3 -3
  224. package/knowledge/solo-test/masters/master_marks/sources.jsonl +1 -1
  225. package/knowledge/solo-test/masters/master_marks/tools.json +54 -28
  226. package/knowledge/solo-test/masters/master_munger/decision_policy.json +141 -16
  227. package/knowledge/solo-test/masters/master_munger/doctrine.jsonl +2 -2
  228. package/knowledge/solo-test/masters/master_munger/manifest.json +24 -10
  229. package/knowledge/solo-test/masters/master_munger/provisional-index.json +3 -3
  230. package/knowledge/solo-test/masters/master_munger/sources.jsonl +1 -1
  231. package/knowledge/solo-test/masters/master_munger/tools.json +52 -26
  232. package/knowledge/solo-test/masters/master_natenberg/decision_policy.json +59 -11
  233. package/knowledge/solo-test/masters/master_natenberg/doctrine.jsonl +2 -2
  234. package/knowledge/solo-test/masters/master_natenberg/manifest.json +23 -10
  235. package/knowledge/solo-test/masters/master_natenberg/provisional-index.json +3 -3
  236. package/knowledge/solo-test/masters/master_natenberg/sources.jsonl +1 -1
  237. package/knowledge/solo-test/masters/master_natenberg/tools.json +50 -24
  238. package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +125 -14
  239. package/knowledge/solo-test/masters/master_pabrai/doctrine.jsonl +2 -2
  240. package/knowledge/solo-test/masters/master_pabrai/manifest.json +22 -6
  241. package/knowledge/solo-test/masters/master_pabrai/provisional-index.json +3 -3
  242. package/knowledge/solo-test/masters/master_pabrai/sources.jsonl +1 -1
  243. package/knowledge/solo-test/masters/master_pabrai/tools.json +55 -29
  244. package/knowledge/solo-test/masters/master_simons/decision_policy.json +59 -11
  245. package/knowledge/solo-test/masters/master_simons/doctrine.jsonl +2 -2
  246. package/knowledge/solo-test/masters/master_simons/manifest.json +22 -8
  247. package/knowledge/solo-test/masters/master_simons/provisional-index.json +3 -3
  248. package/knowledge/solo-test/masters/master_simons/sources.jsonl +1 -1
  249. package/knowledge/solo-test/masters/master_simons/tools.json +43 -30
  250. package/knowledge/solo-test/masters/master_sinclair/decision_policy.json +59 -11
  251. package/knowledge/solo-test/masters/master_sinclair/doctrine.jsonl +2 -2
  252. package/knowledge/solo-test/masters/master_sinclair/manifest.json +22 -10
  253. package/knowledge/solo-test/masters/master_sinclair/provisional-index.json +3 -3
  254. package/knowledge/solo-test/masters/master_sinclair/sources.jsonl +1 -1
  255. package/knowledge/solo-test/masters/master_sinclair/tools.json +54 -28
  256. package/knowledge/solo-test/masters/master_soros/decision_policy.json +98 -10
  257. package/knowledge/solo-test/masters/master_soros/doctrine.jsonl +2 -2
  258. package/knowledge/solo-test/masters/master_soros/manifest.json +24 -11
  259. package/knowledge/solo-test/masters/master_soros/provisional-index.json +3 -3
  260. package/knowledge/solo-test/masters/master_soros/sources.jsonl +1 -1
  261. package/knowledge/solo-test/masters/master_soros/tools.json +54 -28
  262. package/knowledge/solo-test/masters/master_taleb/decision_policy.json +124 -13
  263. package/knowledge/solo-test/masters/master_taleb/doctrine.jsonl +2 -2
  264. package/knowledge/solo-test/masters/master_taleb/manifest.json +23 -9
  265. package/knowledge/solo-test/masters/master_taleb/provisional-index.json +3 -3
  266. package/knowledge/solo-test/masters/master_taleb/sources.jsonl +1 -1
  267. package/knowledge/solo-test/masters/master_taleb/tools.json +50 -24
  268. package/knowledge/solo-test/masters/master_thorp/decision_policy.json +61 -13
  269. package/knowledge/solo-test/masters/master_thorp/doctrine.jsonl +2 -2
  270. package/knowledge/solo-test/masters/master_thorp/manifest.json +25 -10
  271. package/knowledge/solo-test/masters/master_thorp/provisional-index.json +3 -3
  272. package/knowledge/solo-test/masters/master_thorp/sources.jsonl +1 -1
  273. package/knowledge/solo-test/masters/master_thorp/tools.json +56 -30
  274. package/knowledge/solo-test/persona-v3-solo-test-formulas/compilation-manifest.json +290 -264
  275. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/components/tools.json +58 -32
  276. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  277. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/owner_earnings_yield.derived-proxy-evidence.json +76 -0
  278. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/components/tools.json +58 -32
  279. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  280. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/price_implied_cash_yield.derived-proxy-evidence.json +76 -0
  281. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/components/tools.json +58 -32
  282. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/nominal_earnings_yield_gap.derived-proxy-evidence.json +76 -0
  283. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/real_earnings_yield_gap.derived-proxy-evidence.json +76 -0
  284. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_bogle/components/tools.json +120 -0
  285. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_bogle/provisional-derivations/expected_return_over_long_bond.derived-proxy-evidence.json +77 -0
  286. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_bogle/provisional-derivations/fundamental_expected_return.derived-proxy-evidence.json +76 -0
  287. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/components/tools.json +57 -31
  288. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  289. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/owner_earnings_yield.derived-proxy-evidence.json +76 -0
  290. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/components/tools.json +58 -32
  291. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/net_current_asset_surplus_per_share.derived-proxy-evidence.json +77 -0
  292. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/net_current_asset_value_per_share.derived-proxy-evidence.json +76 -0
  293. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/components/tools.json +58 -32
  294. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/current_cash_yield.derived-proxy-evidence.json +76 -0
  295. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  296. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/components/tools.json +58 -32
  297. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/policy_real_rate.derived-proxy-evidence.json +76 -0
  298. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/real_curve_slope.derived-proxy-evidence.json +76 -0
  299. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/components/tools.json +56 -30
  300. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/provisional-derivations/implied_equity_risk_premium.derived-proxy-evidence.json +76 -0
  301. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/provisional-derivations/{reverse_valuation.derived-proxy-evidence.json → premium_versus_long_run_average.derived-proxy-evidence.json} +29 -16
  302. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/components/tools.json +57 -31
  303. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/liquidity_curve_impulse.derived-proxy-evidence.json +76 -0
  304. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/policy_real_rate.derived-proxy-evidence.json +76 -0
  305. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/components/tools.json +84 -32
  306. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/five_year_opportunity_cost.derived-proxy-evidence.json +102 -0
  307. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/margin_retention.derived-proxy-evidence.json +76 -0
  308. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/components/tools.json +60 -34
  309. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/margin_retention.derived-proxy-evidence.json +76 -0
  310. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/reinvestment_spread.derived-proxy-evidence.json +76 -0
  311. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/components/tools.json +55 -29
  312. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/provisional-derivations/{accounting_rebuilder.derived-proxy-evidence.json → cash_conversion_gap.derived-proxy-evidence.json} +28 -15
  313. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/provisional-derivations/cash_cover_of_leverage.derived-proxy-evidence.json +76 -0
  314. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/components/tools.json +69 -30
  315. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/defensive_hurdle_base.derived-proxy-evidence.json +89 -0
  316. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/half_normalised_earnings_yield.derived-proxy-evidence.json +76 -0
  317. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/components/tools.json +56 -30
  318. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/cash_quality_gap.derived-proxy-evidence.json +76 -0
  319. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/real_structural_growth.derived-proxy-evidence.json +76 -0
  320. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/components/tools.json +57 -31
  321. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/downside_asset_coverage.derived-proxy-evidence.json +76 -0
  322. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  323. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/components/tools.json +60 -34
  324. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/cash_backed_decade_return.derived-proxy-evidence.json +76 -0
  325. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/decade_return_over_long_bond.derived-proxy-evidence.json +76 -0
  326. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/components/tools.json +69 -30
  327. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/growth_justified_capitalisation.derived-proxy-evidence.json +89 -0
  328. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  329. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/components/tools.json +56 -30
  330. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/credit_cycle_position.derived-proxy-evidence.json +76 -0
  331. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/equity_over_corporate_debt.derived-proxy-evidence.json +76 -0
  332. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/components/tools.json +54 -28
  333. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/debt_service_cushion.derived-proxy-evidence.json +76 -0
  334. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/reported_earnings_cash_gap.derived-proxy-evidence.json +76 -0
  335. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_natenberg/components/tools.json +52 -26
  336. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_natenberg/provisional-derivations/{surface_builder.derived-proxy-evidence.json → normalised_skew.derived-proxy-evidence.json} +27 -14
  337. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_natenberg/provisional-derivations/round_trip_cost.derived-proxy-evidence.json +76 -0
  338. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/components/tools.json +57 -31
  339. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/downside_floor_coverage.derived-proxy-evidence.json +76 -0
  340. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
  341. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/components/tools.json +45 -32
  342. package/knowledge/solo-test/persona-v3-solo-test-formulas/{master_natenberg/provisional-derivations/greeks_payoff.derived-proxy-evidence.json → master_simons/provisional-derivations/absolute_session_move.derived-proxy-evidence.json} +17 -17
  343. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/provisional-derivations/session_move_over_implied.derived-proxy-evidence.json +77 -0
  344. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/components/tools.json +56 -30
  345. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/round_trip_cost.derived-proxy-evidence.json +76 -0
  346. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/volatility_per_unit_of_width.derived-proxy-evidence.json +76 -0
  347. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/components/tools.json +56 -30
  348. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/credit_cycle_position.derived-proxy-evidence.json +76 -0
  349. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/financing_burden.derived-proxy-evidence.json +76 -0
  350. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/components/tools.json +52 -26
  351. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/provisional-derivations/debt_service_cushion.derived-proxy-evidence.json +76 -0
  352. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/provisional-derivations/{tail_friction.derived-proxy-evidence.json → normalised_skew.derived-proxy-evidence.json} +27 -14
  353. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/components/tools.json +58 -32
  354. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/edge_per_unit_of_volatility.derived-proxy-evidence.json +77 -0
  355. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/net_edge.derived-proxy-evidence.json +76 -0
  356. package/mcp/lib/breadth.mjs +122 -0
  357. package/mcp/lib/codex.mjs +3 -3
  358. package/mcp/lib/constants.mjs +76 -25
  359. package/mcp/lib/council-options.mjs +95 -40
  360. package/mcp/lib/council-selection.mjs +95 -9
  361. package/mcp/lib/damodaran.mjs +119 -0
  362. package/mcp/lib/fred.mjs +246 -0
  363. package/mcp/lib/fund-holdings-parsers.mjs +297 -0
  364. package/mcp/lib/fundamentals.mjs +801 -0
  365. package/mcp/lib/funds.mjs +750 -0
  366. package/mcp/lib/gates.mjs +149 -15
  367. package/mcp/lib/grounding.mjs +188 -36
  368. package/mcp/lib/index-aggregate.mjs +718 -0
  369. package/mcp/lib/insider-ownership.mjs +149 -0
  370. package/mcp/lib/instrument-facts.mjs +557 -0
  371. package/mcp/lib/instruments.mjs +191 -0
  372. package/mcp/lib/lang.mjs +109 -2
  373. package/mcp/lib/markdown.mjs +620 -138
  374. package/mcp/lib/master-catalog.mjs +32 -3
  375. package/mcp/lib/orchestrator.mjs +1121 -290
  376. package/mcp/lib/packets.mjs +227 -21
  377. package/mcp/lib/personas/engine.mjs +44 -31
  378. package/mcp/lib/personas-v3/compiler.mjs +2 -0
  379. package/mcp/lib/personas-v3/deterministic-executor.mjs +13 -2
  380. package/mcp/lib/personas-v3/ga-external-evidence.mjs +3 -2
  381. package/mcp/lib/personas-v3/ga-gate.mjs +2 -1
  382. package/mcp/lib/personas-v3/ga-package-evidence.mjs +5 -4
  383. package/mcp/lib/personas-v3/grounding-adapter.mjs +309 -2
  384. package/mcp/lib/personas-v3/loader.mjs +10 -7
  385. package/mcp/lib/personas-v3/production-root.mjs +5 -4
  386. package/mcp/lib/personas-v3/release-formula-evidence.mjs +14 -7
  387. package/mcp/lib/personas-v3/release-source-evidence.mjs +2 -1
  388. package/mcp/lib/personas-v3/releases.mjs +5 -4
  389. package/mcp/lib/personas-v3/runtime-release-integrity.mjs +6 -5
  390. package/mcp/lib/personas-v3/semantic-source-adjudication.mjs +2 -1
  391. package/mcp/lib/personas-v3/semantic-source-extraction.mjs +2 -1
  392. package/mcp/lib/personas-v3/semantic-source-skeptic-review.mjs +2 -1
  393. package/mcp/lib/personas-v3/source-acquisition.mjs +10 -3
  394. package/mcp/lib/personas-v3/source-review-operations.mjs +3 -2
  395. package/mcp/lib/personas-v3/staging.mjs +22 -3
  396. package/mcp/lib/prompts.mjs +119 -2
  397. package/mcp/lib/quotes.mjs +5 -0
  398. package/mcp/lib/rpc.mjs +101 -42
  399. package/mcp/lib/run-store.mjs +28 -3
  400. package/mcp/lib/sec.mjs +32 -0
  401. package/mcp/lib/tables.mjs +11 -0
  402. package/mcp/lib/voice.mjs +122 -0
  403. package/mcp/lib/xbrl-aggregates.mjs +153 -0
  404. package/mcp/lib/xbrl-series.mjs +339 -0
  405. package/mcp/lib/xlsx.mjs +155 -0
  406. package/package.json +5 -5
  407. package/personas/masters/masters-value/bogle.md +145 -0
  408. package/schemas/council-evaluation-protocol-v1.schema.json +2 -2
  409. package/schemas/persona-v3-ai-assisted-solo-profile-v1.schema.json +2 -2
  410. package/schemas/persona-v3-ai-machine-simulation-v1.schema.json +1 -1
  411. package/schemas/persona-v3-build-spec-v1.schema.json +3 -3
  412. package/schemas/persona-v3-release-formula-review-evidence-v1.schema.json +3 -3
  413. package/schemas/persona-v3-release-manifest-v1.schema.json +6 -6
  414. package/schemas/persona-v3-release-source-review-evidence-v1.schema.json +3 -3
  415. package/schemas/persona-v3-tool-graph-v1.schema.json +1 -1
  416. package/schemas/persona-v3.schema.json +37 -2
  417. package/schemas/source-review-batch-v1.schema.json +9 -9
  418. package/scripts/assemble-persona-v3-release.mjs +3 -2
  419. package/scripts/build-persona-v3-solo-test-packs.mjs +6 -8
  420. package/scripts/check-persona-v3-ga.mjs +1 -1
  421. package/scripts/check-persona-v3-production-candidates.mjs +1 -1
  422. package/scripts/compile-persona-v3-formulas.mjs +19 -18
  423. package/scripts/doctor.mjs +11 -9
  424. package/scripts/export-persona-source-review-batch.mjs +1 -1
  425. package/scripts/generate-persona-v3-drafts.mjs +2 -2
  426. package/scripts/lib/build-profile.mjs +24 -0
  427. package/scripts/lib/council-evaluation-protocol.mjs +7 -6
  428. package/scripts/lib/host-capabilities.mjs +2 -1
  429. package/scripts/lib/packaged-host-parity.mjs +63 -8
  430. package/scripts/lib/persona-v3-ai-assisted-solo-status.mjs +6 -4
  431. package/scripts/lib/persona-v3-ai-formula-review.mjs +6 -5
  432. package/scripts/lib/persona-v3-ai-machine-simulations.mjs +32 -4
  433. package/scripts/lib/persona-v3-build-specs.mjs +3 -2
  434. package/scripts/lib/persona-v3-draft-packs.mjs +2 -2
  435. package/scripts/lib/persona-v3-formula-pipeline.mjs +11 -10
  436. package/scripts/lib/persona-v3-production-candidates.mjs +10 -9
  437. package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +182 -16
  438. package/scripts/lib/persona-v3-solo-test-packs.mjs +143 -16
  439. package/scripts/package-smoke.mjs +32 -12
  440. package/scripts/report-persona-corpus-gaps.mjs +4 -1
  441. package/scripts/review-persona-v3-solo-formulas.mjs +12 -8
  442. package/scripts/run-tests.mjs +23 -2
  443. package/scripts/scaffold-persona-v3-staging.mjs +1 -1
  444. package/skills/agent-skills-governance/SKILL.md +8 -2
  445. package/skills/alphacouncil-agent/SKILL.md +60 -9
  446. package/skills/public-equity-investing/SKILL.md +17 -0
  447. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/change_value_bridge.derived-proxy-evidence.json +0 -64
  448. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/power_map.derived-proxy-evidence.json +0 -64
  449. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/compute_power_bridge.derived-proxy-evidence.json +0 -64
  450. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/timeline_reverse_valuation.derived-proxy-evidence.json +0 -64
  451. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/crowding_cost_stress.derived-proxy-evidence.json +0 -64
  452. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/factor_decomposer.derived-proxy-evidence.json +0 -64
  453. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/incremental_returns.derived-proxy-evidence.json +0 -64
  454. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/owner_earnings_rebuilder.derived-proxy-evidence.json +0 -64
  455. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/capital_structure_reader.derived-proxy-evidence.json +0 -64
  456. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/mispricing_carry.derived-proxy-evidence.json +0 -64
  457. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/capture_valuation_bridge.derived-proxy-evidence.json +0 -64
  458. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/cost_adoption_curve.derived-proxy-evidence.json +0 -64
  459. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/debt_driver_stress.derived-proxy-evidence.json +0 -64
  460. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/regime_classifier.derived-proxy-evidence.json +0 -64
  461. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/provisional-derivations/story_dcf.derived-proxy-evidence.json +0 -64
  462. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/inflection_payoff.derived-proxy-evidence.json +0 -64
  463. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/liquidity_revision_map.derived-proxy-evidence.json +0 -64
  464. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/opportunity_cost.derived-proxy-evidence.json +0 -64
  465. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/user_value_evidence.derived-proxy-evidence.json +0 -64
  466. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/research_productivity.derived-proxy-evidence.json +0 -64
  467. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/scuttlebutt_graph.derived-proxy-evidence.json +0 -64
  468. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/provisional-derivations/borrow_catalyst.derived-proxy-evidence.json +0 -64
  469. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/asset_floor.derived-proxy-evidence.json +0 -64
  470. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/normalized_earnings.derived-proxy-evidence.json +0 -64
  471. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/penetration_liquidity.derived-proxy-evidence.json +0 -64
  472. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/promoter_governance.derived-proxy-evidence.json +0 -64
  473. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/cash_catalyst_compare.derived-proxy-evidence.json +0 -64
  474. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/recovery_waterfall.derived-proxy-evidence.json +0 -64
  475. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/promise_integrity_ledger.derived-proxy-evidence.json +0 -64
  476. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/ten_year_return_bridge.derived-proxy-evidence.json +0 -64
  477. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/category_classifier.derived-proxy-evidence.json +0 -64
  478. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/story_numbers_check.derived-proxy-evidence.json +0 -64
  479. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/cycle_temperature.derived-proxy-evidence.json +0 -64
  480. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/implied_expectations.derived-proxy-evidence.json +0 -64
  481. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/failure_path_graph.derived-proxy-evidence.json +0 -64
  482. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/incentive_map.derived-proxy-evidence.json +0 -64
  483. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/discrete_payoff.derived-proxy-evidence.json +0 -64
  484. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/downside_floor.derived-proxy-evidence.json +0 -64
  485. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/provisional-derivations/leakage_audit.derived-proxy-evidence.json +0 -64
  486. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/provisional-derivations/oos_cost_test.derived-proxy-evidence.json +0 -64
  487. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/net_edge_sizer.derived-proxy-evidence.json +0 -64
  488. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/realized_vol_forecast.derived-proxy-evidence.json +0 -64
  489. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/reflexivity_graph.derived-proxy-evidence.json +0 -64
  490. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/reversal_monitor.derived-proxy-evidence.json +0 -64
  491. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/provisional-derivations/payoff_ruin.derived-proxy-evidence.json +0 -64
  492. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/edge_recalculator.derived-proxy-evidence.json +0 -64
  493. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/fractional_kelly.derived-proxy-evidence.json +0 -64
@@ -1,17 +1,17 @@
1
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { COUNCIL_MODES, DEBATE_ROLES, DEFAULT_TASKS, LIMITS, OUTPUT_MODES, QUICK_TASKS } from "./constants.mjs";
4
4
  import { invalidParams } from "./errors.mjs";
5
5
  import { readJson, writeJson } from "./fsutil.mjs";
6
6
  import { registry } from "./personas/registry.mjs";
7
- import { isChineseLanguage, resolveLanguage } from "./lang.mjs";
7
+ import { assertReaderLanguage, isChineseLanguage, localized, resolveLanguage } from "./lang.mjs";
8
8
  import { cleanLog } from "./text.mjs";
9
- import { completenessStatus, verificationStatus } from "./gates.mjs";
9
+ import { completenessStatus, masterSeatIncomplete, verificationStatus } from "./gates.mjs";
10
10
  import { agentState, appendEvent, artifactPaths, existingDebate, runPath, runId, safeSymbol, saveRun, taskState, today, updateAgent, updateTask, writeSourceManifest, writeStatus } from "./run-store.mjs";
11
11
  import { writeAllAgentsMarkdown, writeAnalystMarkdownFiles, writeArtifactIndex, writeFinalArtifacts } from "./markdown.mjs";
12
- import { debateFromCodex, debateQnaGate, dryDebate, dryPacket, extractJson, firstFailedDebateResult, managerFallback, mergeDebateRounds, normalizeDebate, normalizeMasterOpinion, normalizePacket, rawRecordText } from "./packets.mjs";
12
+ import { debateFailurePacket, debateFromCodex, debateQnaGate, dryDebate, dryPacket, extractJson, firstFailedDebateResult, managerFallback, mergeDebateRounds, normalizeDebate, normalizeMasterOpinion, normalizeMasterVoice, normalizePacket, rawRecordText } from "./packets.mjs";
13
13
  import { mapLimit, runCodex } from "./codex.mjs";
14
- import { debatePrompt, masterPrompt, selectedMasters, taskPrompt } from "./prompts.mjs";
14
+ import { debatePrompt, masterPrompt, masterVoicePrompt, selectedMasters, taskPrompt } from "./prompts.mjs";
15
15
  import { resolveSeatWeights } from "./weights.mjs";
16
16
  import { completedMasterOpinion, declinedMasterOpinion, planMasterSeats, reconcileMasterOpinion } from "./personas/engine.mjs";
17
17
  import { gatherGrounding } from "./grounding.mjs";
@@ -28,40 +28,67 @@ function plannedTasks(args = {}) {
28
28
  return DEFAULT_TASKS;
29
29
  }
30
30
 
31
- function quickTiming(args, startedAt) {
32
- if (councilMode(args) !== "quick") {
33
- return { council_mode: "full", debate_format: "three_round_cross_exam", time_budget_ms: null, deadline_at: null };
34
- }
31
+ function councilTiming(args, startedAt) {
32
+ const mode = councilMode(args);
35
33
  const requested = Number(args.total_timeout_ms);
34
+ const hardMaximum = mode === "quick" ? LIMITS.QUICK_HARD_MAX_MS : LIMITS.FULL_HARD_MAX_MS;
35
+ const defaultBudget = mode === "quick" ? LIMITS.QUICK_TOTAL_MS : LIMITS.FULL_TOTAL_MS;
36
36
  const timeBudgetMs = Number.isFinite(requested) && requested > 0
37
- ? Math.min(requested, LIMITS.QUICK_HARD_MAX_MS)
38
- : LIMITS.QUICK_TOTAL_MS;
37
+ ? Math.min(requested, hardMaximum)
38
+ : defaultBudget;
39
39
  const deadlineAt = args.queued_run?.deadline_at
40
40
  || new Date(Date.parse(startedAt) + timeBudgetMs).toISOString();
41
41
  return {
42
- council_mode: "quick",
43
- debate_format: "single_round_parallel",
42
+ council_mode: mode,
43
+ debate_format: mode === "quick" ? "single_round_parallel" : "three_round_cross_exam_parallel_per_round",
44
44
  time_budget_ms: timeBudgetMs,
45
45
  deadline_at: deadlineAt,
46
+ deadline_enforced: true,
47
+ };
48
+ }
49
+
50
+ function visibleCouncilTiming(args) {
51
+ const mode = councilMode(args);
52
+ return {
53
+ council_mode: mode,
54
+ debate_format: mode === "quick" ? "single_round_parallel" : "host_managed_visible_debate",
55
+ time_budget_ms: null,
56
+ deadline_at: null,
57
+ deadline_enforced: false,
46
58
  };
47
59
  }
48
60
 
49
- function remainingQuickBudget(run, capMs) {
50
- if (run.council_mode !== "quick" || !run.deadline_at) return capMs;
61
+ function remainingCouncilBudget(run, capMs) {
62
+ if (!run.deadline_at) return capMs;
63
+ const configuredReserve = run.council_mode === "quick"
64
+ ? LIMITS.QUICK_FINALIZE_RESERVE_MS
65
+ : LIMITS.FULL_FINALIZE_RESERVE_MS;
51
66
  const reserve = Math.min(
52
- LIMITS.QUICK_FINALIZE_RESERVE_MS,
53
- Math.max(100, Math.floor(Number(run.time_budget_ms || LIMITS.QUICK_TOTAL_MS) * 0.1)),
67
+ configuredReserve,
68
+ Math.max(100, Math.floor(Number(run.time_budget_ms || LIMITS.FULL_TOTAL_MS) * 0.1)),
54
69
  );
55
- const usable = Date.parse(run.deadline_at) - Date.now() - reserve;
70
+ const killGrace = councilKillGrace(run);
71
+ // runCodex may need one SIGKILL grace after its timeout. Budget that grace before
72
+ // launching a child so the queue-to-persistence deadline remains the outer boundary.
73
+ const usable = Date.parse(run.deadline_at) - Date.now() - reserve - killGrace;
56
74
  return Math.max(0, Math.min(capMs, usable));
57
75
  }
58
76
 
59
- function deadlineResult() {
77
+ function councilKillGrace(run) {
78
+ const total = Math.max(1_000, Number(run?.time_budget_ms || LIMITS.FULL_TOTAL_MS));
79
+ // A fixed five-second grace would consume tiny operator-supplied budgets before the
80
+ // first worker. Production budgets retain the full grace; small budgets scale it down.
81
+ // The exact same value is passed to runCodex, so forced settlement remains inside the
82
+ // queue-to-persistence deadline rather than becoming an unaccounted overrun.
83
+ return Math.min(LIMITS.SIGKILL_GRACE_MS, Math.max(50, Math.floor(total * 0.02)));
84
+ }
85
+
86
+ function deadlineResult(run) {
60
87
  return {
61
88
  ok: false,
62
89
  code: null,
63
90
  text: "",
64
- stderr: "quick council global deadline exhausted",
91
+ stderr: `${run?.council_mode || "council"} global deadline exhausted`,
65
92
  stdout: "",
66
93
  timedOut: true,
67
94
  deadline_exhausted: true,
@@ -169,7 +196,10 @@ export function visibleRun(args) {
169
196
  const dir = runPath(id);
170
197
  mkdirSync(dir, { recursive: true });
171
198
  const startedAt = new Date().toISOString();
172
- const timing = quickTiming(args, startedAt);
199
+ // External host threads are outside this MCP process, so the plugin cannot stop them or
200
+ // make a truthful queue-to-persistence SLA claim. Only analyze_symbol gets the enforced
201
+ // thirty-minute full-council deadline.
202
+ const timing = visibleCouncilTiming(args);
173
203
  const run = {
174
204
  run_id: id,
175
205
  symbol,
@@ -188,6 +218,18 @@ export function visibleRun(args) {
188
218
  tasks,
189
219
  task_status: Object.fromEntries(tasks.map((task) => [task, { task, status: "pending" }])),
190
220
  agent_status: Object.fromEntries(DEBATE_ROLES.map((role) => [role, { role, status: "pending" }])),
221
+ // Visible hosts call record_visible_decision once per role and round. Keep those
222
+ // packets separate: overwriting bull_researcher.json three times destroys the audit
223
+ // chain and makes a one-round shortcut indistinguishable from a full cross-exam.
224
+ visible_debate: {
225
+ contract: "role_round_audit_v1",
226
+ rounds_expected: councilMode(args) === "quick" ? 1 : 3,
227
+ rounds: { bull_researcher: {}, bear_researcher: {} },
228
+ qna_gate: {
229
+ status: councilMode(args) === "quick" ? "not_run" : "pending",
230
+ errors: [],
231
+ },
232
+ },
191
233
  packets: [],
192
234
  // The receipt resolves the user's exact choice once. This frozen list, not a dynamic
193
235
  // roster lookup, is the run truth and every seat is part of the completeness gate.
@@ -220,9 +262,19 @@ export function visibleRun(args) {
220
262
  export function visibleAgentSpecs(run, userPrompt = "") {
221
263
  const evidence_agents = run.tasks.map((task) => ({
222
264
  role: task,
223
- title: isChineseLanguage(run.language) ? `AlphaCouncil Agent ${run.symbol} ${task} 证据子代理` : `AlphaCouncil Agent ${run.symbol} ${task} evidence subagent`,
265
+ title: localized(run.language, {
266
+ en: `AlphaCouncil Agent ${run.symbol} ${task} evidence subagent`,
267
+ zh: `AlphaCouncil Agent ${run.symbol} ${task} 证据子代理`,
268
+ ja: `AlphaCouncil Agent ${run.symbol} ${task} 証拠サブエージェント`,
269
+ ko: `AlphaCouncil Agent ${run.symbol} ${task} 증거 하위 에이전트`,
270
+ }),
224
271
  prompt: taskPrompt(task, run.symbol, run.as_of, userPrompt, run.language, run.grounding),
225
- output_contract: isChineseLanguage(run.language) ? "只返回一个 JSON evidence packet。" : `Return one JSON evidence packet with reader-facing fields in ${run.language}.`,
272
+ output_contract: localized(run.language, {
273
+ en: `Return one JSON evidence packet with reader-facing fields in ${run.language}.`,
274
+ zh: "只返回一个 JSON evidence packet。",
275
+ ja: "読者向けフィールドを日本語にした JSON evidence packet を1つだけ返してください。",
276
+ ko: "독자용 필드를 한국어로 작성한 JSON evidence packet 하나만 반환하십시오.",
277
+ }),
226
278
  }));
227
279
  const debate_agents = DEBATE_ROLES.map((role) => ({
228
280
  role,
@@ -230,15 +282,17 @@ export function visibleAgentSpecs(run, userPrompt = "") {
230
282
  prompt_template: [
231
283
  debatePrompt(role, run),
232
284
  "",
233
- isChineseLanguage(run.language) ? "主线程必须先粘贴已完成的 Evidence JSON,再运行这个可见代理。" : "The main thread must paste the completed Evidence JSON before running this visible agent.",
234
- role === "bear_researcher" ? (isChineseLanguage(run.language) ? "主线程还必须粘贴 Bull argument JSON。" : "The main thread must also paste Bull argument JSON.") : "",
235
- role === "portfolio_manager" ? (isChineseLanguage(run.language) ? "主线程还必须粘贴 Bull 和 Bear argument JSON。" : "The main thread must also paste Bull and Bear argument JSON.") : "",
285
+ localized(run.language, {
286
+ en: "The main thread must paste the completed Evidence JSON before running this visible agent.", zh: "主线程必须先粘贴已完成的 Evidence JSON,再运行这个可见代理。", ja: "メインスレッドは、この可視エージェントを実行する前に完成済みの Evidence JSON を貼り付ける必要があります。", ko: "메인 스레드는 표시형 에이전트를 실행하기 전에 완료된 Evidence JSON 붙여 넣어야 합니다.",
287
+ }),
288
+ role === "bear_researcher" ? localized(run.language, { en: "The main thread must also paste Bull argument JSON.", zh: "主线程还必须粘贴 Bull argument JSON。", ja: "メインスレッドは Bull argument JSON も貼り付ける必要があります。", ko: "메인 스레드는 Bull argument JSON도 붙여 넣어야 합니다." }) : "",
289
+ role === "portfolio_manager" ? localized(run.language, { en: "The main thread must also paste Bull and Bear argument JSON.", zh: "主线程还必须粘贴 Bull 和 Bear argument JSON。", ja: "メインスレッドは Bull と Bear の argument JSON も貼り付ける必要があります。", ko: "메인 스레드는 Bull 및 Bear argument JSON도 붙여 넣어야 합니다." }) : "",
236
290
  ].filter(Boolean).join("\n"),
237
- output_contract: isChineseLanguage(run.language) ? "只返回一个 JSON debate packet。" : `Return one JSON debate packet with reader-facing fields in ${run.language}.`,
291
+ output_contract: localized(run.language, { en: `Return one JSON debate packet with reader-facing fields in ${run.language}.`, zh: "只返回一个 JSON debate packet。", ja: "読者向けフィールドを日本語にした JSON debate packet を1つだけ返してください。", ko: "독자용 필드를 한국어로 작성한 JSON debate packet 하나만 반환하십시오." }),
238
292
  }));
239
- // The deterministic pass runs first and settles, for free, every seat whose method cannot
240
- // reach this security. Spawning an agent for a lens that has already declined is how the
241
- // previous design produced ten confident essays over a screen that computed nothing.
293
+ // The deterministic pass freezes every physical v3 stance before language generation.
294
+ // Visible v3 workers below may explain that frozen result after evidence completes, but
295
+ // cannot turn an out_of_scope method into a directional vote.
242
296
  const plan = planMasterSeats(run, selectedMasters(run));
243
297
  run.master_decisions = plan.decisions;
244
298
  run.fact_pack_hash = plan.shared_fact_pack_hash;
@@ -253,35 +307,58 @@ export function visibleAgentSpecs(run, userPrompt = "") {
253
307
  "",
254
308
  decision ? deterministicVerdictBlock(decision, zh) : "",
255
309
  "",
256
- zh
257
- ? "主线程必须先粘贴已完成的 Evidence JSON,再运行这个大师议席;大师在证据之后、辩论之前运行。"
258
- : "The main thread must paste the completed Evidence JSON first. Masters run after the evidence stage and before the debate.",
310
+ localized(run.language, {
311
+ en: "The main thread must paste the completed Evidence JSON first. Masters run after the evidence stage and before the debate.",
312
+ zh: "主线程必须先粘贴已完成的 Evidence JSON,再运行这个大师议席;大师在证据之后、辩论之前运行。",
313
+ ja: "メインスレッドは先に完成済みの Evidence JSON を貼り付ける必要があります。メソッド席は証拠段階の後、討論の前に実行します。",
314
+ ko: "메인 스레드는 먼저 완료된 Evidence JSON을 붙여 넣어야 합니다. 방법론 좌석은 증거 단계 이후, 토론 이전에 실행합니다.",
315
+ }),
259
316
  ].filter(Boolean).join("\n"),
260
- output_contract: zh
261
- ? "只返回一个 JSON master opinion。"
262
- : `Return one JSON master opinion with reader-facing fields in ${run.language}.`,
317
+ output_contract: localized(run.language, {
318
+ en: `Return one JSON master opinion with reader-facing fields in ${run.language}.`,
319
+ zh: "只返回一个 JSON master opinion。",
320
+ ja: "読者向けフィールドを日本語にした JSON master opinion を1つだけ返してください。",
321
+ ko: "독자용 필드를 한국어로 작성한 JSON master opinion 하나만 반환하십시오.",
322
+ }),
263
323
  }));
264
- // A declined seat is settled here rather than skipped: it is written straight into the
265
- // run as an out_of_scope opinion, so the completeness gate is satisfied and no agent is
266
- // ever spawned for a method that cannot look.
324
+ // Persist the deterministic fallback before the visible worker runs. It remains auditable
325
+ // if the host fails, but a full visible PM now waits for the returned explanation worker.
267
326
  if (plan.declined.length) {
268
327
  const byId = new Map((run.master_opinions || []).map((o) => [o.master, o]));
269
328
  for (const item of plan.declined) {
270
329
  if (!byId.has(item.id)) {
330
+ const fallback = declinedMasterOpinion(run, item);
271
331
  byId.set(item.id, attachMasterRuntimeProvenance(
272
332
  run,
273
333
  item.id,
274
- declinedMasterOpinion(run, item),
334
+ {
335
+ ...fallback,
336
+ voice_statement: fallback.voice_statement || fallback.summary || fallback.verdict,
337
+ voice_status: fallback.voice_status || "deterministic_fallback",
338
+ statement_origin: fallback.statement_origin || "deterministic_scope_fallback",
339
+ },
275
340
  item.engine,
276
341
  ));
277
342
  }
343
+ // A declined seat already carries a readable deterministic scope statement and a frozen
344
+ // out_of_scope stance no worker may change, so the explanation worker could not alter
345
+ // the record -- it only cost one sequential model turn per seat on a host with no
346
+ // fan-out. On a full bench that was 26 extra turns, which roughly doubled visible
347
+ // wall-clock. Seats that actually reached a decision still get their worker below.
348
+ const requiresVisibleVoice = false;
349
+ const alreadyVoiced = run.master_status?.[item.id]?.status === "completed"
350
+ && byId.get(item.id)?.voice_status === "completed";
351
+ const completed = alreadyVoiced || !requiresVisibleVoice;
278
352
  run.master_status[item.id] = {
279
353
  ...(run.master_status[item.id] || {}),
280
354
  master: item.id,
281
- status: "completed",
355
+ status: completed ? "completed" : "waiting",
282
356
  engine: item.engine || "v2_method_model",
283
357
  deterministic_decline: true,
284
- completed_at: new Date().toISOString(),
358
+ voice_required: requiresVisibleVoice,
359
+ completed_at: completed
360
+ ? (run.master_status[item.id]?.completed_at || new Date().toISOString())
361
+ : null,
285
362
  };
286
363
  }
287
364
  run.master_opinions = selectedMasters(run).map((id) => byId.get(id)).filter(Boolean);
@@ -297,13 +374,16 @@ export function visibleAgentSpecs(run, userPrompt = "") {
297
374
  item.engine,
298
375
  ));
299
376
  }
377
+ const alreadyVoiced = run.master_status?.[item.id]?.status === "completed"
378
+ && byId.get(item.id)?.voice_status === "completed";
300
379
  run.master_status[item.id] = {
301
380
  ...(run.master_status[item.id] || {}),
302
381
  master: item.id,
303
- status: "completed",
382
+ status: alreadyVoiced ? "completed" : "waiting",
304
383
  engine: item.engine,
305
384
  deterministic_execution: true,
306
- completed_at: new Date().toISOString(),
385
+ voice_required: true,
386
+ completed_at: alreadyVoiced ? run.master_status[item.id].completed_at : null,
307
387
  };
308
388
  }
309
389
  run.master_opinions = selectedMasters(run).map((id) => byId.get(id)).filter(Boolean);
@@ -318,10 +398,43 @@ export function visibleAgentSpecs(run, userPrompt = "") {
318
398
  updated_at: new Date().toISOString(),
319
399
  };
320
400
  }
401
+ const frozenById = new Map((run.master_opinions || []).map((opinion) => [opinion.master, opinion]));
402
+ const v3VoiceAgents = plan.completed
403
+ .filter((item) => item.engine === "v3_method_runtime")
404
+ .filter((item) => run.master_status?.[item.id]?.status !== "completed")
405
+ .map((item) => {
406
+ const frozenOpinion = frozenById.get(item.id);
407
+ return {
408
+ role: item.id,
409
+ engine: "v3_method_runtime",
410
+ worker_kind: "visible_method_voice",
411
+ frozen_stance: frozenOpinion?.stance || "out_of_scope",
412
+ title: `AlphaCouncil Agent ${run.symbol} ${item.id} method-seat explanation`,
413
+ prompt_template: [
414
+ masterVoicePrompt(item.id, run, frozenOpinion),
415
+ localized(run.language, {
416
+ en: "The main thread MUST append the completed Evidence JSON before launching this visible worker. Use it only to explain or challenge analyst interpretation; never change the frozen stance or add a fact.",
417
+ zh: "主线程必须在启动这个可见方法席前附上已完成的 Evidence JSON。它只能用于解释或质疑分析师解读;不得改变冻结立场,也不得新增事实。",
418
+ ja: "メインスレッドは、この可視メソッド席を起動する前に完成済み Evidence JSON を追加してください。分析担当の解釈を説明・検討する目的だけに使い、凍結済みスタンスの変更や事実追加は禁止です。",
419
+ ko: "메인 스레드는 이 표시형 방법론 좌석을 시작하기 전에 완료된 Evidence JSON을 추가해야 합니다. 분석가 해석을 설명·검토하는 데만 사용하고 동결된 입장을 바꾸거나 사실을 추가하지 마십시오.",
420
+ }),
421
+ ].join("\n\n"),
422
+ output_contract: localized(run.language, {
423
+ en: "Return one JSON method voice: master, acknowledged_stance, statement, key_findings, disagreements, what_would_change_my_mind, source_ids, confidence. acknowledged_stance must equal frozen_stance.",
424
+ zh: "只返回一个 JSON 方法席陈词:master、acknowledged_stance、statement、key_findings、disagreements、what_would_change_my_mind、source_ids、confidence;acknowledged_stance 必须等于冻结立场。",
425
+ ja: "master、acknowledged_stance、statement、key_findings、disagreements、what_would_change_my_mind、source_ids、confidence を持つ JSON を1つだけ返し、acknowledged_stance は凍結済みスタンスと一致させてください。",
426
+ ko: "master, acknowledged_stance, statement, key_findings, disagreements, what_would_change_my_mind, source_ids, confidence를 가진 JSON 하나만 반환하고 acknowledged_stance는 동결된 입장과 같아야 합니다.",
427
+ }),
428
+ };
429
+ });
430
+ master_agents.push(...v3VoiceAgents);
431
+ const orderedMasterAgents = selectedMasters(run)
432
+ .map((id) => master_agents.find((agent) => agent.role === id))
433
+ .filter(Boolean);
321
434
  saveRun(run);
322
435
  return {
323
436
  evidence_agents,
324
- master_agents,
437
+ master_agents: orderedMasterAgents,
325
438
  debate_agents,
326
439
  // Recorded, not hidden: a reader must be able to tell a method that judged from a method
327
440
  // that could not look, and neither from a seat that was never offered.
@@ -395,18 +508,90 @@ export function recordMasterOpinion(args) {
395
508
  if (!allowed.includes(args.master)) {
396
509
  throw invalidParams(`master ${args.master} was not selected for this run. Selected: ${allowed.join(", ") || "none"}`);
397
510
  }
511
+ const missingEvidence = (run.tasks || []).filter((task) => taskState(run, task).status !== "completed");
512
+ if (missingEvidence.length) {
513
+ throw invalidParams("record_master_opinion rejected: every planned evidence packet must complete first.", {
514
+ reason: "VISIBLE_MASTER_EVIDENCE_INCOMPLETE",
515
+ run_id: run.run_id,
516
+ missing_evidence: missingEvidence,
517
+ });
518
+ }
398
519
  const dir = runPath(run.run_id);
399
- const normalized = normalizeMasterOpinion(
400
- { ...(args.packet || {}), thread_id: args.thread_id },
401
- args.master,
402
- run,
403
- rawRecordText(args.packet),
404
- );
405
- // A narrated stance that disagrees with the arithmetic does not get to win quietly. The
406
- // deterministic verdict stands and the disagreement is preserved on the record.
407
- const reconciled = reconcileMasterOpinion(run, args.master, normalized);
408
- const opinion = attachMasterRuntimeProvenance(run, args.master, reconciled.opinion, reconciled.engine);
409
- const { overridden } = reconciled;
520
+ const frozenOpinion = (run.master_opinions || []).find((item) => item.master === args.master);
521
+ const v3Voice = frozenOpinion?.engine === "v3_method_runtime"
522
+ || run.master_runtime_provenance?.[args.master]?.engine === "v3_method_runtime";
523
+ let opinion;
524
+ let overridden = false;
525
+ if (v3Voice) {
526
+ if (!frozenOpinion) throw invalidParams(`v3 master ${args.master} has no frozen deterministic opinion.`);
527
+ if (args.packet?.master !== args.master || args.packet?.acknowledged_stance !== frozenOpinion.stance) {
528
+ throw invalidParams("visible v3 method voice must acknowledge the exact frozen master and stance.", {
529
+ reason: "VISIBLE_MASTER_FROZEN_STANCE_MISMATCH",
530
+ run_id: run.run_id,
531
+ master: args.master,
532
+ expected_stance: frozenOpinion.stance,
533
+ supplied_master: args.packet?.master || null,
534
+ supplied_stance: args.packet?.acknowledged_stance || null,
535
+ });
536
+ }
537
+ const voice = normalizeMasterVoice(
538
+ args.packet,
539
+ args.master,
540
+ run,
541
+ frozenOpinion,
542
+ rawRecordText(args.packet),
543
+ );
544
+ assertVisibleReaderLanguage([
545
+ voice.statement,
546
+ ...(voice.key_findings || []),
547
+ ...(voice.disagreements || []),
548
+ ...(voice.what_would_change_my_mind || []),
549
+ ].filter(Boolean).join("\n"), run, `visible master voice ${args.master}`);
550
+ opinion = attachMasterRuntimeProvenance(run, args.master, {
551
+ ...frozenOpinion,
552
+ deterministic_summary: frozenOpinion.deterministic_summary || frozenOpinion.summary,
553
+ summary: voice.statement,
554
+ voice_statement: voice.statement,
555
+ voice_status: "completed",
556
+ voice_language: run.language,
557
+ statement_origin: "visible_method_voice_worker",
558
+ key_findings: voice.key_findings.length ? voice.key_findings : frozenOpinion.key_findings,
559
+ disagreements: voice.disagreements,
560
+ what_would_change_my_mind: voice.what_would_change_my_mind.length
561
+ ? voice.what_would_change_my_mind
562
+ : frozenOpinion.what_would_change_my_mind,
563
+ source_ids: [...new Set([...(frozenOpinion.source_ids || []), ...voice.source_ids])],
564
+ confidence: voice.confidence,
565
+ thread_id: args.thread_id,
566
+ dedicated_worker: {
567
+ status: "completed",
568
+ language: run.language,
569
+ execution_mode: "visible_host_thread",
570
+ thread_id: args.thread_id,
571
+ },
572
+ }, "v3_method_runtime");
573
+ } else {
574
+ const normalized = normalizeMasterOpinion(
575
+ { ...(args.packet || {}), thread_id: args.thread_id },
576
+ args.master,
577
+ run,
578
+ rawRecordText(args.packet),
579
+ );
580
+ assertVisibleReaderLanguage(masterReaderText(normalized), run, `visible master ${args.master}`);
581
+ // A narrated stance that disagrees with the arithmetic does not get to win quietly. The
582
+ // deterministic verdict stands and the disagreement is preserved on the record.
583
+ const reconciled = reconcileMasterOpinion(run, args.master, normalized);
584
+ opinion = attachMasterRuntimeProvenance(run, args.master, {
585
+ ...reconciled.opinion,
586
+ voice_statement: reconciled.opinion.voice_statement
587
+ || reconciled.opinion.summary
588
+ || reconciled.opinion.verdict,
589
+ voice_status: "completed",
590
+ voice_language: run.language,
591
+ statement_origin: "visible_legacy_method_worker",
592
+ }, reconciled.engine);
593
+ overridden = reconciled.overridden;
594
+ }
410
595
  const byId = new Map((run.master_opinions || []).map((item) => [item.master, item]));
411
596
  byId.set(args.master, opinion);
412
597
  run.master_opinions = allowed.map((id) => byId.get(id)).filter(Boolean);
@@ -453,6 +638,7 @@ export function recordVisiblePacket(args) {
453
638
  thread_title: args.thread_title,
454
639
  execution_mode: "visible_host_threads",
455
640
  }, task, run.symbol, run.as_of, rawRecordText(args.packet));
641
+ assertVisibleReaderLanguage(visibleEvidenceReaderText(packet), run, `visible evidence ${task}`);
456
642
  const byTask = new Map(run.packets.map((item) => [item.task, item]));
457
643
  byTask.set(task, packet);
458
644
  run.packets = run.tasks.map((item) => byTask.get(item)).filter(Boolean);
@@ -472,70 +658,380 @@ export function recordVisiblePacket(args) {
472
658
  return run;
473
659
  }
474
660
 
475
- export function recordVisibleDecision(args) {
476
- const run = readJson(join(runPath(args.run_id), "evidence.json"));
477
- if (run.execution_mode !== "visible_host_threads") {
478
- throw invalidParams("record_visible_decision requires a run created by plan_visible_run.");
661
+ function visibleDebateState(run) {
662
+ const expected = run.council_mode === "quick" ? 1 : 3;
663
+ run.visible_debate = run.visible_debate && typeof run.visible_debate === "object"
664
+ ? run.visible_debate
665
+ : {};
666
+ run.visible_debate.contract = "role_round_audit_v1";
667
+ run.visible_debate.rounds_expected = expected;
668
+ run.visible_debate.rounds = run.visible_debate.rounds && typeof run.visible_debate.rounds === "object"
669
+ ? run.visible_debate.rounds
670
+ : {};
671
+ for (const role of ["bull_researcher", "bear_researcher"]) {
672
+ run.visible_debate.rounds[role] = run.visible_debate.rounds[role]
673
+ && typeof run.visible_debate.rounds[role] === "object"
674
+ ? run.visible_debate.rounds[role]
675
+ : {};
676
+ }
677
+ run.visible_debate.qna_gate = run.visible_debate.qna_gate && typeof run.visible_debate.qna_gate === "object"
678
+ ? run.visible_debate.qna_gate
679
+ : { status: expected === 1 ? "not_run" : "pending", errors: [] };
680
+ return run.visible_debate;
681
+ }
682
+
683
+ function visibleRoundEntry(state, role, round) {
684
+ return state?.rounds?.[role]?.[String(round)] || null;
685
+ }
686
+
687
+ function visibleRoundPacket(state, role, round) {
688
+ return visibleRoundEntry(state, role, round)?.packet || null;
689
+ }
690
+
691
+ function visibleRoundNumbers(state, role) {
692
+ return Object.keys(state?.rounds?.[role] || {})
693
+ .map(Number)
694
+ .filter(Number.isInteger)
695
+ .sort((a, b) => a - b);
696
+ }
697
+
698
+ function visibleDecisionContentHash(packet) {
699
+ const content = JSON.parse(JSON.stringify(packet || {}));
700
+ delete content.raw_text;
701
+ delete content.thread_id;
702
+ delete content.thread_title;
703
+ delete content.execution_mode;
704
+ return sha256(content);
705
+ }
706
+
707
+ function roundGateArgs(state, role, round, candidate) {
708
+ const get = (candidateRole, candidateRound) => (
709
+ candidateRole === role && candidateRound === round
710
+ ? candidate
711
+ : visibleRoundPacket(state, candidateRole, candidateRound)
712
+ );
713
+ return {
714
+ bullR2: get("bull_researcher", 2),
715
+ bearR2: get("bear_researcher", 2),
716
+ bullR3: get("bull_researcher", 3),
717
+ bearR3: get("bear_researcher", 3),
718
+ };
719
+ }
720
+
721
+ function rejectVisibleDecision(run, reason, message, data = {}) {
722
+ throw invalidParams(message, { reason, run_id: run.run_id, ...data });
723
+ }
724
+
725
+ function assertVisibleRoundOrder(run, state, role, round) {
726
+ if (round === 1) return;
727
+ const previous = round - 1;
728
+ const missing = ["bull_researcher", "bear_researcher"]
729
+ .filter((side) => !visibleRoundEntry(state, side, previous))
730
+ .map((side) => `${side}:${previous}`);
731
+ if (missing.length) {
732
+ rejectVisibleDecision(
733
+ run,
734
+ "VISIBLE_DEBATE_ROUND_OUT_OF_ORDER",
735
+ `Cannot record ${role} round ${round} before both round ${previous} packets are recorded.`,
736
+ { role, round, missing_prerequisite_rounds: missing },
737
+ );
738
+ }
739
+ }
740
+
741
+ function assertVisibleRoundQna(run, state, role, round, packet) {
742
+ if (run.council_mode === "quick" || round === 1) return;
743
+ const gate = debateQnaGate(roundGateArgs(state, role, round, packet));
744
+ const ownErrors = gate.errors.filter((error) => error.startsWith(`${role} round ${round}`));
745
+ if (ownErrors.length) {
746
+ rejectVisibleDecision(
747
+ run,
748
+ "VISIBLE_DEBATE_QNA_INVALID",
749
+ `Rejected ${role} round ${round}: ${ownErrors.join("; ")}`,
750
+ { role, round, qna_gate: { status: "failed", errors: ownErrors } },
751
+ );
752
+ }
753
+ const other = role === "bull_researcher" ? "bear_researcher" : "bull_researcher";
754
+ if (round === 3 && visibleRoundEntry(state, other, 3) && gate.status !== "passed") {
755
+ rejectVisibleDecision(
756
+ run,
757
+ "VISIBLE_DEBATE_QNA_INVALID",
758
+ `Rejected ${role} round 3 because the completed cross-exam failed the exact Q&A gate.`,
759
+ { role, round, qna_gate: gate },
760
+ );
479
761
  }
762
+ }
763
+
764
+ function visibleRoundAgentPatch(run, state, role) {
765
+ const dir = runPath(run.run_id);
766
+ const rounds = visibleRoundNumbers(state, role);
767
+ const latest = rounds.length ? visibleRoundEntry(state, role, rounds.at(-1)) : null;
768
+ return {
769
+ rounds_completed: rounds,
770
+ last_completed_round: rounds.at(-1) || null,
771
+ round_status: rounds.length ? "completed" : "pending",
772
+ qna_gate: state.qna_gate.status,
773
+ thread_id: latest?.thread_id,
774
+ thread_title: latest?.thread_title,
775
+ output: join(dir, `${role}.json`),
776
+ };
777
+ }
778
+
779
+ function recordVisibleDebateRound(run, args) {
480
780
  const role = args.role;
481
- if (!DEBATE_ROLES.includes(role)) throw invalidParams(`Unknown decision role: ${role}`);
781
+ const missingEvidence = (run.tasks || []).filter((task) => taskState(run, task).status !== "completed");
782
+ const missingMasters = selectedMasters(run).filter((master) => masterSeatIncomplete(run, master));
783
+ if (missingEvidence.length || missingMasters.length) {
784
+ rejectVisibleDecision(
785
+ run,
786
+ "VISIBLE_DEBATE_PREREQUISITES_INCOMPLETE",
787
+ "Bull/Bear debate rejected: complete every evidence packet and returned method-seat worker first.",
788
+ { missing_evidence: missingEvidence, missing_masters: missingMasters },
789
+ );
790
+ }
791
+ const expected = run.council_mode === "quick" ? [1] : [1, 2, 3];
792
+ if (!Number.isInteger(args.round) || !expected.includes(args.round)) {
793
+ rejectVisibleDecision(
794
+ run,
795
+ "VISIBLE_DEBATE_ROUND_REQUIRED",
796
+ `${role} requires an explicit round=${expected.join("|")}.`,
797
+ { role, supplied_round: args.round ?? null, allowed_rounds: expected },
798
+ );
799
+ }
800
+ const round = args.round;
801
+ const dir = runPath(run.run_id);
802
+ const state = visibleDebateState(run);
803
+ const packet = {
804
+ ...normalizeDebate({
805
+ ...(args.packet || {}),
806
+ thread_id: args.thread_id,
807
+ thread_title: args.thread_title,
808
+ execution_mode: "visible_host_threads",
809
+ }, role, run, rawRecordText(args.packet)),
810
+ round,
811
+ // A caller records exactly one round. Nested supplied rounds would create a second,
812
+ // unaudited history inside the packet and are therefore discarded.
813
+ debate_rounds: [],
814
+ };
815
+ assertVisibleReaderLanguage(debateReaderText(packet), run, `visible debate ${role} round ${round}`);
816
+ const contentHash = visibleDecisionContentHash(packet);
817
+ const existing = visibleRoundEntry(state, role, round);
818
+ if (existing) {
819
+ if (existing.content_hash !== contentHash) {
820
+ rejectVisibleDecision(
821
+ run,
822
+ "VISIBLE_DEBATE_ROUND_CONFLICT",
823
+ `Conflicting replay for ${role} round ${round}.`,
824
+ { role, round, existing_content_hash: existing.content_hash, submitted_content_hash: contentHash },
825
+ );
826
+ }
827
+ return { run, decision: existing.packet, idempotent_replay: true };
828
+ }
829
+
830
+ assertVisibleRoundOrder(run, state, role, round);
831
+ assertVisibleRoundQna(run, state, role, round, packet);
832
+
833
+ const completedAt = new Date().toISOString();
834
+ const roundFile = join(dir, `${role}.round-${round}.json`);
835
+ state.rounds[role][String(round)] = {
836
+ round,
837
+ role,
838
+ content_hash: contentHash,
839
+ packet,
840
+ thread_id: args.thread_id,
841
+ thread_title: args.thread_title,
842
+ output: roundFile,
843
+ completed_at: completedAt,
844
+ };
845
+ writeJson(roundFile, packet);
846
+ const merged = mergeDebateRounds(visibleRoundNumbers(state, role)
847
+ .map((item) => visibleRoundPacket(state, role, item)));
848
+ writeJson(join(dir, `${role}.json`), merged);
849
+
850
+ const quick = run.council_mode === "quick";
851
+ const hasBothFinalRounds = ["bull_researcher", "bear_researcher"]
852
+ .every((side) => visibleRoundEntry(state, side, quick ? 1 : 3));
853
+ if (quick) {
854
+ state.qna_gate = { status: "not_run", errors: [], checked_at: completedAt };
855
+ updateAgent(run, role, "completed", {
856
+ ...visibleRoundAgentPatch(run, state, role),
857
+ completed_at: completedAt,
858
+ });
859
+ } else if (hasBothFinalRounds) {
860
+ const qnaGate = debateQnaGate(roundGateArgs(state, role, round, packet));
861
+ // The candidate was validated before persistence. This guard protects future state
862
+ // migrations or a manually corrupted run file from manufacturing a passed audit.
863
+ if (qnaGate.status !== "passed") {
864
+ delete state.rounds[role][String(round)];
865
+ rejectVisibleDecision(
866
+ run,
867
+ "VISIBLE_DEBATE_QNA_INVALID",
868
+ "The completed visible cross-exam failed the exact Q&A gate.",
869
+ { role, round, qna_gate: qnaGate },
870
+ );
871
+ }
872
+ state.qna_gate = { ...qnaGate, checked_at: completedAt };
873
+ for (const side of ["bull_researcher", "bear_researcher"]) {
874
+ updateAgent(run, side, "completed", {
875
+ ...visibleRoundAgentPatch(run, state, side),
876
+ completed_at: completedAt,
877
+ });
878
+ }
879
+ appendEvent(run, "debate_qna_gate", state.qna_gate);
880
+ } else {
881
+ updateAgent(run, role, "waiting", visibleRoundAgentPatch(run, state, role));
882
+ }
883
+
884
+ run.status = "running";
885
+ run.phase = "visible_debate";
886
+ run.completed_at = null;
887
+ saveRun(run);
888
+ appendEvent(run, "visible_debate_round_recorded", {
889
+ role,
890
+ round,
891
+ content_hash: contentHash,
892
+ output: roundFile,
893
+ qna_gate: state.qna_gate.status,
894
+ });
895
+ writeAnalystMarkdownFiles(run, existingDebate(dir));
896
+ writeArtifactIndex(run, existingDebate(dir));
897
+ writeAllAgentsMarkdown(run, existingDebate(dir));
898
+ return { run, decision: packet, idempotent_replay: false };
899
+ }
900
+
901
+ function visiblePmPrerequisites(run, state) {
902
+ const missingEvidence = (run.tasks || [])
903
+ .filter((task) => taskState(run, task).status !== "completed");
904
+ const missingMasters = selectedMasters(run).filter((master) => masterSeatIncomplete(run, master));
905
+ const expected = run.council_mode === "quick" ? [1] : [1, 2, 3];
906
+ const missingRounds = ["bull_researcher", "bear_researcher"].flatMap((role) => (
907
+ expected.filter((round) => !visibleRoundEntry(state, role, round)).map((round) => `${role}:${round}`)
908
+ ));
909
+ const missingSides = ["bull_researcher", "bear_researcher"]
910
+ .filter((role) => agentState(run, role).status !== "completed");
911
+ const qnaRequired = run.council_mode !== "quick";
912
+ return {
913
+ missing_evidence: missingEvidence,
914
+ missing_masters: missingMasters,
915
+ missing_debate_rounds: missingRounds,
916
+ missing_debate_sides: missingSides,
917
+ qna_gate: state.qna_gate,
918
+ passed: missingEvidence.length === 0
919
+ && missingMasters.length === 0
920
+ && missingRounds.length === 0
921
+ && missingSides.length === 0
922
+ && (!qnaRequired || state.qna_gate.status === "passed"),
923
+ };
924
+ }
925
+
926
+ function recordVisiblePortfolioManager(run, args) {
927
+ if (args.round !== undefined && args.round !== null) {
928
+ rejectVisibleDecision(
929
+ run,
930
+ "VISIBLE_PM_ROUND_FORBIDDEN",
931
+ "portfolio_manager is the final decision and does not accept a debate round.",
932
+ { supplied_round: args.round },
933
+ );
934
+ }
482
935
  const dir = runPath(run.run_id);
936
+ const state = visibleDebateState(run);
483
937
  const packet = normalizeDebate({
484
938
  ...(args.packet || {}),
485
939
  thread_id: args.thread_id,
486
940
  thread_title: args.thread_title,
487
941
  execution_mode: "visible_host_threads",
488
- }, role, run, rawRecordText(args.packet));
489
- const file = role === "portfolio_manager" ? "manager_synthesis.json" : `${role}.json`;
490
- writeJson(join(dir, file), packet);
491
- // Mark the agent completed BEFORE evaluating the gates. The completeness gate now
492
- // counts portfolio_manager, so reading it first would report every finished run as
493
- // incomplete -- the PM would never be recorded by the time it was checked.
494
- updateAgent(run, role, "completed", {
495
- completed_at: new Date().toISOString(),
942
+ }, "portfolio_manager", run, rawRecordText(args.packet));
943
+ assertVisibleReaderLanguage(debateReaderText(packet), run, "visible portfolio_manager decision");
944
+ const contentHash = visibleDecisionContentHash(packet);
945
+ if (state.portfolio_manager) {
946
+ if (state.portfolio_manager.content_hash !== contentHash) {
947
+ rejectVisibleDecision(run, "VISIBLE_PM_CONFLICT", "Conflicting portfolio_manager replay.", {
948
+ existing_content_hash: state.portfolio_manager.content_hash,
949
+ submitted_content_hash: contentHash,
950
+ });
951
+ }
952
+ const handoffPath = join(dir, "user_response.md");
953
+ const reportPath = join(dir, "final_report.md");
954
+ const qualityPath = join(dir, "report_quality.json");
955
+ return {
956
+ run,
957
+ decision: state.portfolio_manager.packet,
958
+ idempotent_replay: true,
959
+ user_response_markdown: existsSync(handoffPath) ? readFileSync(handoffPath, "utf8") : "",
960
+ final_report_markdown: existsSync(reportPath) ? readFileSync(reportPath, "utf8") : "",
961
+ report_quality: existsSync(qualityPath) ? readJson(qualityPath) : null,
962
+ artifacts: artifactPaths(run),
963
+ };
964
+ }
965
+
966
+ const prerequisites = visiblePmPrerequisites(run, state);
967
+ if (!prerequisites.passed) {
968
+ rejectVisibleDecision(
969
+ run,
970
+ "VISIBLE_PM_PREREQUISITES_INCOMPLETE",
971
+ "portfolio_manager rejected: complete evidence, every selected master, all required bull/bear rounds, and the exact Q&A gate are mandatory first.",
972
+ prerequisites,
973
+ );
974
+ }
975
+
976
+ const completedAt = new Date().toISOString();
977
+ const file = join(dir, "manager_synthesis.json");
978
+ state.portfolio_manager = {
979
+ content_hash: contentHash,
980
+ packet,
496
981
  thread_id: args.thread_id,
497
982
  thread_title: args.thread_title,
498
- output: join(dir, file),
983
+ output: file,
984
+ completed_at: completedAt,
985
+ };
986
+ writeJson(file, packet);
987
+ writeJson(join(dir, "decision.json"), packet);
988
+ updateAgent(run, "portfolio_manager", "completed", {
989
+ completed_at: completedAt,
990
+ thread_id: args.thread_id,
991
+ thread_title: args.thread_title,
992
+ output: file,
993
+ debate_qna_gate: state.qna_gate.status,
499
994
  });
500
- let finalArtifacts = {};
501
- if (role === "portfolio_manager") {
502
- const gate = verificationStatus(run);
503
- const completeness = completenessStatus(run);
504
- writeJson(join(dir, "decision.json"), packet);
505
- if (completeness.completeness === "incomplete") {
506
- run.status = "incomplete";
507
- run.phase = "incomplete";
508
- appendEvent(run, "incomplete", {
509
- missing_evidence: completeness.missing_evidence,
510
- missing_debate: completeness.missing_debate,
511
- missing_masters: completeness.missing_masters,
512
- });
513
- } else if (gate.verification === "needs_verification") {
514
- run.status = "needs_verification";
515
- run.phase = "needs_verification";
516
- appendEvent(run, "needs_verification", { missing: gate.missing_claim_source_ids.length });
517
- } else {
518
- run.status = "complete";
519
- run.phase = "complete";
520
- }
521
- run.completed_at = new Date().toISOString();
522
- } else {
523
- run.status = "running";
524
- run.phase = "visible_debate";
995
+
996
+ const gate = verificationStatus(run);
997
+ const completeness = completenessStatus(run);
998
+ if (completeness.completeness === "incomplete") {
999
+ // Defensive only: visiblePmPrerequisites rejects all known incomplete states before
1000
+ // this point. Never create a final report if a future completeness field disagrees.
1001
+ rejectVisibleDecision(run, "VISIBLE_PM_PREREQUISITES_INCOMPLETE", "portfolio_manager completeness gate failed.", {
1002
+ missing_evidence: completeness.missing_evidence,
1003
+ missing_debate: completeness.missing_debate,
1004
+ missing_masters: completeness.missing_masters,
1005
+ });
525
1006
  }
526
- saveRun(run);
527
- writeJson(join(dir, "evidence.json"), run);
528
- if (role === "portfolio_manager") {
529
- finalArtifacts = writeFinalArtifacts(run, existingDebate(dir));
530
- writeJson(join(dir, "evidence.json"), run);
531
- if (run.status === "complete") appendEvent(run, "run_complete", { decision: packet.rating, winner: packet.winner });
1007
+ if (gate.verification === "needs_verification") {
1008
+ run.status = "needs_verification";
1009
+ run.phase = "needs_verification";
1010
+ appendEvent(run, "needs_verification", { missing: gate.missing_claim_source_ids.length });
532
1011
  } else {
533
- writeAnalystMarkdownFiles(run, existingDebate(dir));
534
- writeArtifactIndex(run, existingDebate(dir));
1012
+ run.status = "complete";
1013
+ run.phase = "complete";
535
1014
  }
1015
+ run.completed_at = completedAt;
1016
+ saveRun(run);
1017
+ const finalArtifacts = writeFinalArtifacts(run, existingDebate(dir));
1018
+ saveRun(run);
1019
+ if (run.status === "complete") appendEvent(run, "run_complete", { decision: packet.rating, winner: packet.winner });
536
1020
  writeStatus(run);
537
1021
  writeAllAgentsMarkdown(run, existingDebate(dir));
538
- return { run, decision: packet, ...finalArtifacts };
1022
+ return { run, decision: packet, idempotent_replay: false, ...finalArtifacts };
1023
+ }
1024
+
1025
+ export function recordVisibleDecision(args) {
1026
+ const run = readJson(join(runPath(args.run_id), "evidence.json"));
1027
+ if (run.execution_mode !== "visible_host_threads") {
1028
+ throw invalidParams("record_visible_decision requires a run created by plan_visible_run.");
1029
+ }
1030
+ const role = args.role;
1031
+ if (!DEBATE_ROLES.includes(role)) throw invalidParams(`Unknown decision role: ${role}`);
1032
+ return role === "portfolio_manager"
1033
+ ? recordVisiblePortfolioManager(run, args)
1034
+ : recordVisibleDebateRound(run, args);
539
1035
  }
540
1036
 
541
1037
  export function isDryRun(args = {}) {
@@ -550,30 +1046,51 @@ export function isDryRun(args = {}) {
550
1046
  * but is not a sourced claim and must never enter evidence.json or downstream debate.
551
1047
  */
552
1048
  export function workerFailureArtifacts({ task, symbol, asOfDate, language, timeoutMs, result, failureKind, parseError }) {
553
- const chinese = isChineseLanguage(language);
1049
+ const copy = localized(language, {
1050
+ en: {
1051
+ parse: `Evidence worker ${task} returned output that violated the JSON contract; it produced no evidence usable for an investment decision.`,
1052
+ language: `Evidence worker ${task} returned reader-facing content in the wrong language; it produced no evidence usable for an investment decision.`,
1053
+ failed: `Evidence worker ${task} timed out or failed; it produced no evidence usable for an investment decision.`,
1054
+ inspect: `Inspect the separate ${task} failure diagnostic and retry; do not use its partial transcript as evidence.`,
1055
+ },
1056
+ zh: {
1057
+ parse: `证据席位 ${task} 的输出不符合 JSON 契约;未生成可用于投资判断的证据。`,
1058
+ language: `证据席位 ${task} 返回了错误语言的读者内容;未生成可用于投资判断的证据。`,
1059
+ failed: `证据席位 ${task} 执行超时或失败;未生成可用于投资判断的证据。`,
1060
+ inspect: `检查 ${task} 的独立失败诊断并重试;不得用该席位的部分对话补齐证据。`,
1061
+ },
1062
+ ja: {
1063
+ parse: `証拠席 ${task} の出力は JSON 契約に違反しており、投資判断に使用できる証拠は生成されませんでした。`,
1064
+ language: `証拠席 ${task} は指定と異なる言語の読者向け内容を返したため、投資判断に使用できる証拠は生成されませんでした。`,
1065
+ failed: `証拠席 ${task} はタイムアウトまたは失敗し、投資判断に使用できる証拠は生成されませんでした。`,
1066
+ inspect: `${task} の分離された失敗診断を確認して再実行してください。部分的な対話を証拠として使用してはいけません。`,
1067
+ },
1068
+ ko: {
1069
+ parse: `증거 좌석 ${task}의 출력이 JSON 계약을 위반해 투자 판단에 사용할 수 있는 증거를 생성하지 못했습니다.`,
1070
+ language: `증거 좌석 ${task}가 지정과 다른 언어의 독자용 내용을 반환해 투자 판단에 사용할 수 있는 증거를 생성하지 못했습니다.`,
1071
+ failed: `증거 좌석 ${task}가 시간 초과 또는 실패로 투자 판단에 사용할 수 있는 증거를 생성하지 못했습니다.`,
1072
+ inspect: `${task}의 분리된 실패 진단을 확인한 뒤 다시 실행하십시오. 부분 대화를 증거로 사용해서는 안 됩니다.`,
1073
+ },
1074
+ });
554
1075
  const timedOut = result?.timedOut === true;
555
1076
  const parseFailed = failureKind === "parse_failed";
556
- const status = parseFailed ? "parse_failed" : (timedOut ? "timed_out" : "failed");
1077
+ const languageMismatch = failureKind === "reader_language_mismatch";
1078
+ const outputContractFailed = parseFailed || languageMismatch;
1079
+ const status = outputContractFailed ? failureKind : (timedOut ? "timed_out" : "failed");
557
1080
  const exitLabel = Number.isInteger(result?.code) ? `exit code ${result.code}` : "worker error";
558
1081
  const parseMessage = cleanLog(parseError?.message || parseError || "subagent did not return valid JSON", 1_000);
559
- const reason = parseFailed ? parseMessage : (timedOut ? `timeout after ${timeoutMs}ms` : exitLabel);
1082
+ const reason = outputContractFailed ? parseMessage : (timedOut ? `timeout after ${timeoutMs}ms` : exitLabel);
560
1083
  const rawOutput = String(result?.text || "");
561
- const positionMatch = parseFailed ? /\bposition\s+(\d+)\b/i.exec(parseMessage) : null;
1084
+ const positionMatch = outputContractFailed ? /\bposition\s+(\d+)\b/i.exec(parseMessage) : null;
562
1085
  const parsePosition = positionMatch ? Number(positionMatch[1]) : null;
563
1086
  const contextStart = Number.isInteger(parsePosition) ? Math.max(0, parsePosition - 500) : 0;
564
1087
  const contextEnd = Number.isInteger(parsePosition) ? Math.min(rawOutput.length, parsePosition + 500) : 0;
565
1088
  const packet = normalizePacket({
566
1089
  summary: parseFailed
567
- ? (chinese
568
- ? `证据席位 ${task} 的输出不符合 JSON 契约;未生成可用于投资判断的证据。`
569
- : `Evidence worker ${task} returned output that violated the JSON contract; it produced no evidence usable for an investment decision.`)
570
- : (chinese
571
- ? `证据席位 ${task} 执行超时或失败;未生成可用于投资判断的证据。`
572
- : `Evidence worker ${task} timed out or failed; it produced no evidence usable for an investment decision.`),
1090
+ ? copy.parse
1091
+ : languageMismatch ? copy.language : copy.failed,
573
1092
  claims: [],
574
- open_questions: [chinese
575
- ? `检查 ${task} 的独立失败诊断并重试;不得用该席位的部分对话补齐证据。`
576
- : `Inspect the separate ${task} failure diagnostic and retry; do not use its partial transcript as evidence.`],
1093
+ open_questions: [copy.inspect],
577
1094
  confidence: "low",
578
1095
  }, task, symbol, asOfDate, "");
579
1096
  const diagnostic = {
@@ -586,11 +1103,11 @@ export function workerFailureArtifacts({ task, symbol, asOfDate, language, timeo
586
1103
  timeout_ms: timeoutMs,
587
1104
  exit_code: Number.isInteger(result?.code) ? result.code : null,
588
1105
  reason,
589
- diagnostic_excerpt: cleanLog(parseFailed
1106
+ diagnostic_excerpt: cleanLog(outputContractFailed
590
1107
  ? (rawOutput || result?.stderr || result?.stdout || reason)
591
1108
  : (result?.stderr || result?.stdout || rawOutput || reason)),
592
- ...(parseFailed ? {
593
- parse_error: parseMessage,
1109
+ ...(outputContractFailed ? {
1110
+ ...(parseFailed ? { parse_error: parseMessage } : { reader_language_error: parseMessage }),
594
1111
  parse_position: Number.isInteger(parsePosition) ? parsePosition : null,
595
1112
  parse_context: Number.isInteger(parsePosition)
596
1113
  ? cleanLog(rawOutput.slice(contextStart, contextEnd), 1_000)
@@ -603,6 +1120,68 @@ export function workerFailureArtifacts({ task, symbol, asOfDate, language, timeo
603
1120
  return { packet, diagnostic };
604
1121
  }
605
1122
 
1123
+ function evidenceReaderText(packet) {
1124
+ return [
1125
+ packet?.summary,
1126
+ ...(packet?.claims || []).flatMap((claim) => [claim?.claim, claim?.evidence]),
1127
+ ...(packet?.open_questions || []),
1128
+ ].filter(Boolean).join("\n");
1129
+ }
1130
+
1131
+ function readerStrings(value, skipKeys = new Set()) {
1132
+ if (typeof value === "string") return value.trim() ? [value] : [];
1133
+ if (Array.isArray(value)) return value.flatMap((item) => readerStrings(item, skipKeys));
1134
+ if (!value || typeof value !== "object") return [];
1135
+ return Object.entries(value).flatMap(([key, item]) => (
1136
+ skipKeys.has(key) ? [] : readerStrings(item, skipKeys)
1137
+ ));
1138
+ }
1139
+
1140
+ function visibleEvidenceReaderText(packet) {
1141
+ const machine = new Set([
1142
+ "task", "symbol", "as_of", "source_ids", "id", "url", "published_at", "retrieved_at",
1143
+ "confidence", "information_richness", "thread_id", "execution_mode", "raw_text",
1144
+ ]);
1145
+ return [
1146
+ ...readerStrings(packet, machine),
1147
+ ].filter(Boolean).join("\n");
1148
+ }
1149
+
1150
+ function masterReaderText(opinion) {
1151
+ const machine = new Set([
1152
+ "master", "symbol", "as_of", "stance", "source_ids", "confidence", "thread_id", "raw_text",
1153
+ "engine", "voice_status", "voice_language", "dedicated_worker", "runtime_provenance",
1154
+ ]);
1155
+ return readerStrings(opinion, machine).join("\n");
1156
+ }
1157
+
1158
+ function debateReaderText(packet) {
1159
+ const machine = new Set([
1160
+ "role", "symbol", "as_of", "decision_available", "rating", "winner", "source_ids", "confidence",
1161
+ "thread_id", "execution_mode", "raw_text", "round", "debate_rounds",
1162
+ ]);
1163
+ return readerStrings(packet, machine).join("\n");
1164
+ }
1165
+
1166
+ function assertVisibleReaderLanguage(text, run, label) {
1167
+ try {
1168
+ return assertReaderLanguage(text, run.language, label);
1169
+ } catch (error) {
1170
+ if (error?.code !== "READER_LANGUAGE_MISMATCH") throw error;
1171
+ throw invalidParams(error.message, {
1172
+ reason: "READER_LANGUAGE_MISMATCH",
1173
+ label,
1174
+ ...error.data,
1175
+ });
1176
+ }
1177
+ }
1178
+
1179
+ function outputFailureKind(error) {
1180
+ return error?.code === "READER_LANGUAGE_MISMATCH"
1181
+ ? "reader_language_mismatch"
1182
+ : "parse_failed";
1183
+ }
1184
+
606
1185
  /**
607
1186
  * A production headless run must not silently become a prompt-only council because its
608
1187
  * caller omitted `grounding`. Visible runs already collect it in rpc.mjs; this gives the
@@ -669,7 +1248,7 @@ export function queueHeadlessRun(args) {
669
1248
  const dir = runPath(id);
670
1249
  mkdirSync(dir, { recursive: true });
671
1250
  const startedAt = new Date().toISOString();
672
- const timing = quickTiming(args, startedAt);
1251
+ const timing = councilTiming(args, startedAt);
673
1252
  const run = {
674
1253
  run_id: id,
675
1254
  symbol,
@@ -723,21 +1302,32 @@ export async function collectEvidence(args) {
723
1302
  const language = resolveLanguage(args);
724
1303
  const frozen = frozenMasterSelection(args);
725
1304
  const startedAt = args.queued_run?.started_at || new Date().toISOString();
726
- const timing = quickTiming(args, startedAt);
1305
+ const timing = councilTiming(args, startedAt);
727
1306
  const requestedTimeoutMs = Number.isFinite(args.timeout_ms) ? args.timeout_ms : LIMITS.CODEX_TIMEOUT_MS;
728
1307
  const timeoutMs = timing.council_mode === "quick"
729
1308
  ? Math.min(requestedTimeoutMs, LIMITS.QUICK_EVIDENCE_MS)
730
- : requestedTimeoutMs;
731
- const defaultConcurrency = timing.council_mode === "quick" ? QUICK_TASKS.length : LIMITS.CONCURRENCY_DEFAULT;
732
- const maxConcurrency = Math.max(LIMITS.CONCURRENCY_MIN, Math.min(LIMITS.CONCURRENCY_MAX, Number(args.max_concurrency || defaultConcurrency)));
1309
+ : Math.min(requestedTimeoutMs, LIMITS.FULL_EVIDENCE_MS);
1310
+ const defaultConcurrency = timing.council_mode === "quick"
1311
+ ? QUICK_TASKS.length
1312
+ : Math.min(tasks.length, LIMITS.FULL_EVIDENCE_CONCURRENCY);
1313
+ // The evidence topology is contractual: every planned evidence seat starts in the same
1314
+ // wave. A legacy low max_concurrency override may not silently turn the full fan-out back
1315
+ // into the old three-wave, ~30-minute evidence stage. Higher values have no effect once
1316
+ // every task can launch.
1317
+ const requestedConcurrency = Number(args.max_concurrency || defaultConcurrency);
1318
+ const maxConcurrency = Math.max(
1319
+ defaultConcurrency,
1320
+ Math.min(LIMITS.CONCURRENCY_MAX, Number.isFinite(requestedConcurrency) ? requestedConcurrency : defaultConcurrency),
1321
+ );
733
1322
  const grounding = await groundingForHeadlessRun({
734
1323
  symbol,
735
1324
  asOf: asOfDate,
736
1325
  grounding: args.grounding,
737
1326
  dryRun,
738
- timeoutMs: timing.council_mode === "quick"
739
- ? remainingQuickBudget(timing, LIMITS.QUICK_GROUNDING_MS)
740
- : undefined,
1327
+ timeoutMs: remainingCouncilBudget(
1328
+ timing,
1329
+ timing.council_mode === "quick" ? LIMITS.QUICK_GROUNDING_MS : LIMITS.FULL_GROUNDING_MS,
1330
+ ),
741
1331
  });
742
1332
  const dir = runPath(id);
743
1333
  mkdirSync(dir, { recursive: true });
@@ -847,15 +1437,15 @@ export async function collectEvidence(args) {
847
1437
  return packet;
848
1438
  }
849
1439
  const workerStartedAt = Date.now();
850
- const runAttempt = async (workerPrompt, budgetMs, attempt) => {
851
- const allowedMs = remainingQuickBudget(run, budgetMs);
852
- if (allowedMs <= 0) return { ...deadlineResult(), budget_ms: 0 };
1440
+ const runAttempt = async (workerPrompt, budgetMs, attempt, { search = true } = {}) => {
1441
+ const allowedMs = remainingCouncilBudget(run, budgetMs);
1442
+ if (allowedMs <= 0) return { ...deadlineResult(run), budget_ms: 0 };
853
1443
  const result = await runCodex(workerPrompt, allowedMs, ({ pid, output }) => {
854
1444
  updateTask(run, task, "running", { pid, output, attempts: attempt });
855
1445
  }, ({ pid, output, elapsed_ms }) => {
856
1446
  updateTask(run, task, "running", { pid, output, attempts: attempt });
857
1447
  appendEvent(run, "task_heartbeat", { task, pid, output, elapsed_ms, attempt });
858
- });
1448
+ }, { search, sigkillGraceMs: councilKillGrace(run) });
859
1449
  return { ...result, budget_ms: allowedMs };
860
1450
  };
861
1451
  const commitFailure = ({ failedResult, budgetMs, attempts, failureKind, parseError, retryDiagnostic }) => {
@@ -882,8 +1472,8 @@ export async function collectEvidence(args) {
882
1472
  ...(retryDiagnostic ? { retry_diagnostic: retryDiagnostic } : {}),
883
1473
  attempts,
884
1474
  deadline_exhausted: failedResult.deadline_exhausted === true,
885
- error: failureKind === "parse_failed"
886
- ? "parse_failed"
1475
+ error: ["parse_failed", "reader_language_mismatch"].includes(failureKind)
1476
+ ? failureKind
887
1477
  : (failedResult.deadline_exhausted ? "global_deadline" : failedResult.timedOut ? "timeout" : `exit code ${failedResult.code}`),
888
1478
  });
889
1479
  return failure.packet;
@@ -896,10 +1486,12 @@ export async function collectEvidence(args) {
896
1486
  let packet;
897
1487
  try {
898
1488
  packet = normalizePacket(extractJson(result.text), task, symbol, asOfDate, result.text);
1489
+ assertReaderLanguage(evidenceReaderText(packet), language, `evidence worker ${task}`);
899
1490
  commitPacket(packet);
900
1491
  updateTask(run, task, "completed", { completed_at: new Date().toISOString(), output: join(dir, `${task}.json`), attempts: 1 });
901
1492
  return packet;
902
1493
  } catch (firstParseError) {
1494
+ const firstFailureKind = outputFailureKind(firstParseError);
903
1495
  const firstFailure = workerFailureArtifacts({
904
1496
  task,
905
1497
  symbol,
@@ -907,19 +1499,23 @@ export async function collectEvidence(args) {
907
1499
  language,
908
1500
  timeoutMs,
909
1501
  result,
910
- failureKind: "parse_failed",
1502
+ failureKind: firstFailureKind,
911
1503
  parseError: firstParseError,
912
1504
  });
913
1505
  const retryDiagnostic = join(dir, `${task}.attempt-1.failure.json`);
914
1506
  writeJson(retryDiagnostic, firstFailure.diagnostic, { mode: 0o600 });
915
1507
  const elapsedMs = Date.now() - workerStartedAt;
916
- const retryTimeoutMs = Math.min(timeoutMs - elapsedMs, remainingQuickBudget(run, timeoutMs));
1508
+ const retryTimeoutMs = Math.min(
1509
+ LIMITS.PARSE_REPAIR_MS,
1510
+ timeoutMs - elapsedMs,
1511
+ remainingCouncilBudget(run, LIMITS.PARSE_REPAIR_MS),
1512
+ );
917
1513
  if (retryTimeoutMs <= 0) {
918
1514
  return commitFailure({
919
1515
  failedResult: result,
920
1516
  budgetMs: result.budget_ms ?? timeoutMs,
921
1517
  attempts: 1,
922
- failureKind: "parse_failed",
1518
+ failureKind: firstFailureKind,
923
1519
  parseError: firstParseError,
924
1520
  retryDiagnostic,
925
1521
  });
@@ -929,13 +1525,22 @@ export async function collectEvidence(args) {
929
1525
  task,
930
1526
  attempt: 2,
931
1527
  max_attempts: 2,
932
- reason: "parse_failed",
1528
+ reason: firstFailureKind,
933
1529
  retry_diagnostic: retryDiagnostic,
934
1530
  remaining_ms: retryTimeoutMs,
935
1531
  });
936
1532
  updateTask(run, task, "running", { attempts: 2, retry_diagnostic: retryDiagnostic });
937
- const retryPrompt = `${prompt}\n\nTRANSPORT RETRY ONLY: Your previous final response violated the required JSON transport contract. Return exactly one JSON object matching the schema above, with no prose, Markdown fence, second object, or trailing text. Do not infer or repair facts from the prior malformed response; perform the same source-bounded task again.`;
938
- result = await runAttempt(retryPrompt, retryTimeoutMs, 2);
1533
+ const malformed = String(result.text || "").slice(0, LIMITS.PARSE_REPAIR_INPUT_CHARS);
1534
+ const retryPrompt = [
1535
+ "PARSE-ONLY TRANSPORT REPAIR. Do not search, browse, fetch, add facts, or redo the research.",
1536
+ `Target task: ${task}; symbol: ${symbol}; as_of: ${asOfDate}; reader language: ${language}.`,
1537
+ firstFailureKind === "reader_language_mismatch"
1538
+ ? "Translate only the reader-facing strings in the supplied valid JSON into the requested language. Preserve the evidence packet schema, claims, numbers, source IDs, URLs, dates, explicit gaps and uncertainty. Return exactly one JSON object and nothing else."
1539
+ : "Convert only the supplied malformed worker output into exactly one valid JSON object matching the evidence packet schema. Preserve claims, numbers, source IDs, URLs, dates, explicit gaps and uncertainty. If a field cannot be recovered, use an empty value and record the loss in open_questions. Return JSON only.",
1540
+ `Write every reader-facing value in ${language}. Translation is permitted only to repair the language mismatch; do not alter facts, numbers, dates, source IDs or uncertainty.`,
1541
+ `Malformed worker output:\n${malformed}`,
1542
+ ].join("\n\n");
1543
+ result = await runAttempt(retryPrompt, retryTimeoutMs, 2, { search: false });
939
1544
  if (!result.ok) {
940
1545
  return commitFailure({
941
1546
  failedResult: result,
@@ -946,6 +1551,7 @@ export async function collectEvidence(args) {
946
1551
  }
947
1552
  try {
948
1553
  packet = normalizePacket(extractJson(result.text), task, symbol, asOfDate, result.text);
1554
+ assertReaderLanguage(evidenceReaderText(packet), language, `evidence worker ${task} repair`);
949
1555
  commitPacket(packet);
950
1556
  updateTask(run, task, "completed", {
951
1557
  completed_at: new Date().toISOString(),
@@ -955,11 +1561,12 @@ export async function collectEvidence(args) {
955
1561
  });
956
1562
  return packet;
957
1563
  } catch (secondParseError) {
1564
+ const secondFailureKind = outputFailureKind(secondParseError);
958
1565
  return commitFailure({
959
1566
  failedResult: result,
960
1567
  budgetMs: result.budget_ms ?? retryTimeoutMs,
961
1568
  attempts: 2,
962
- failureKind: "parse_failed",
1569
+ failureKind: secondFailureKind,
963
1570
  parseError: secondParseError,
964
1571
  retryDiagnostic,
965
1572
  });
@@ -1022,11 +1629,14 @@ export async function runHeadlessMasters(run, args = {}) {
1022
1629
  const requestedTimeoutMs = Number.isFinite(args.timeout_ms) ? args.timeout_ms : LIMITS.CODEX_TIMEOUT_MS;
1023
1630
  const timeoutMs = run.council_mode === "quick"
1024
1631
  ? Math.min(requestedTimeoutMs, LIMITS.QUICK_MASTER_MS)
1025
- : requestedTimeoutMs;
1026
- const defaultConcurrency = run.council_mode === "quick" ? 4 : LIMITS.CONCURRENCY_DEFAULT;
1632
+ : Math.min(requestedTimeoutMs, LIMITS.FULL_MASTER_MS);
1633
+ const defaultConcurrency = run.council_mode === "quick"
1634
+ ? 4
1635
+ : Math.min(selected.length, LIMITS.FULL_MASTER_CONCURRENCY);
1636
+ const requestedConcurrency = Number(args.max_concurrency || defaultConcurrency);
1027
1637
  const maxConcurrency = Math.max(
1028
- LIMITS.CONCURRENCY_MIN,
1029
- Math.min(LIMITS.CONCURRENCY_MAX, Number(args.max_concurrency || defaultConcurrency)),
1638
+ defaultConcurrency,
1639
+ Math.min(LIMITS.CONCURRENCY_MAX, Number.isFinite(requestedConcurrency) ? requestedConcurrency : defaultConcurrency),
1030
1640
  );
1031
1641
  const plan = planMasterSeats(run, selected);
1032
1642
  const byId = new Map((run.master_opinions || []).map((opinion) => [opinion.master, opinion]));
@@ -1045,40 +1655,6 @@ export async function runHeadlessMasters(run, args = {}) {
1045
1655
  blocked: plan.blocked.length,
1046
1656
  });
1047
1657
 
1048
- for (const item of plan.declined) {
1049
- const opinion = attachMasterRuntimeProvenance(
1050
- run,
1051
- item.id,
1052
- declinedMasterOpinion(run, item),
1053
- item.engine,
1054
- );
1055
- byId.set(item.id, opinion);
1056
- writeJson(join(dir, `${item.id}.json`), opinion);
1057
- updateMasterStatus(run, item.id, "completed", {
1058
- engine: item.engine || opinion.engine,
1059
- deterministic_decline: true,
1060
- completed_at: new Date().toISOString(),
1061
- });
1062
- }
1063
-
1064
- for (const item of plan.completed) {
1065
- const opinion = attachMasterRuntimeProvenance(
1066
- run,
1067
- item.id,
1068
- completedMasterOpinion(run, item),
1069
- item.engine,
1070
- );
1071
- byId.set(item.id, opinion);
1072
- writeJson(join(dir, `${item.id}.json`), opinion);
1073
- updateMasterStatus(run, item.id, "completed", {
1074
- engine: item.engine,
1075
- deterministic_execution: true,
1076
- policy_execution_hash: opinion.policy_execution_hash,
1077
- frozen_decision_hash: opinion.frozen_decision_hash,
1078
- completed_at: new Date().toISOString(),
1079
- });
1080
- }
1081
-
1082
1658
  for (const item of plan.blocked) {
1083
1659
  updateMasterStatus(run, item.id, "failed", {
1084
1660
  engine: item.engine,
@@ -1087,48 +1663,155 @@ export async function runHeadlessMasters(run, args = {}) {
1087
1663
  });
1088
1664
  }
1089
1665
 
1090
- const outcomes = await mapLimit(plan.to_run, maxConcurrency, async ({ id, decision, engine }) => {
1091
- const prompt = [
1092
- masterPrompt(id, run),
1093
- decision ? deterministicVerdictBlock(decision, isChineseLanguage(run.language)) : "",
1094
- ].filter(Boolean).join("\n\n");
1095
- updateMasterStatus(run, id, "running", { started_at: new Date().toISOString() });
1666
+ const workerItems = [
1667
+ ...plan.declined.map((item) => ({
1668
+ ...item,
1669
+ frozenOpinion: attachMasterRuntimeProvenance(run, item.id, declinedMasterOpinion(run, item), item.engine),
1670
+ deterministic_decline: true,
1671
+ })),
1672
+ ...plan.completed.map((item) => ({
1673
+ ...item,
1674
+ frozenOpinion: attachMasterRuntimeProvenance(run, item.id, completedMasterOpinion(run, item), item.engine),
1675
+ deterministic_execution: true,
1676
+ })),
1677
+ ...plan.to_run,
1678
+ ];
1679
+ for (const item of workerItems) {
1680
+ if (!item.frozenOpinion) continue;
1681
+ writeJson(join(dir, `${item.id}.deterministic.json`), item.frozenOpinion);
1682
+ }
1683
+
1684
+ const outcomes = await mapLimit(workerItems, maxConcurrency, async ({ id, decision, engine, frozenOpinion, deterministic_decline, deterministic_execution }) => {
1685
+ const prompt = frozenOpinion
1686
+ ? masterVoicePrompt(id, run, frozenOpinion)
1687
+ : [
1688
+ masterPrompt(id, run),
1689
+ decision ? deterministicVerdictBlock(decision, isChineseLanguage(run.language)) : "",
1690
+ ].filter(Boolean).join("\n\n");
1691
+ updateMasterStatus(run, id, "running", {
1692
+ started_at: new Date().toISOString(),
1693
+ worker_kind: frozenOpinion ? "dedicated_method_voice" : "dedicated_method_judgment",
1694
+ deterministic_decline: deterministic_decline || undefined,
1695
+ deterministic_execution: deterministic_execution || undefined,
1696
+ });
1096
1697
 
1097
1698
  if (run.dry_run) {
1699
+ if (frozenOpinion) {
1700
+ return {
1701
+ id,
1702
+ engine,
1703
+ opinion: {
1704
+ ...frozenOpinion,
1705
+ deterministic_summary: frozenOpinion.summary,
1706
+ voice_statement: frozenOpinion.summary,
1707
+ voice_status: "dry_run",
1708
+ dedicated_worker: { status: "dry_run", language: run.language, execution_mode: "dry_run" },
1709
+ },
1710
+ };
1711
+ }
1098
1712
  const normalized = normalizeMasterOpinion({
1099
1713
  verdict: "DRY_RUN",
1100
1714
  stance: "out_of_scope",
1101
- summary: "Dry run: the master prompt was planned but no model judgment was executed.",
1715
+ summary: "Dry run: the dedicated method worker was planned but no model judgment was executed.",
1102
1716
  what_would_change_my_mind: ["Run without dry_run to obtain a method judgment."],
1103
1717
  confidence: "low",
1104
1718
  }, id, run, prompt);
1105
1719
  const reconciled = reconcileMasterOpinion(run, id, normalized);
1106
1720
  const resolvedEngine = engine || reconciled.engine || (decision ? "v2_method_model" : "v1_prompt");
1107
- return { id, opinion: attachMasterRuntimeProvenance(run, id, reconciled.opinion, resolvedEngine), engine: resolvedEngine };
1108
- }
1109
-
1110
- const allowedMs = remainingQuickBudget(run, timeoutMs);
1111
- const result = allowedMs <= 0
1112
- ? deadlineResult()
1113
- : await runCodex(prompt, allowedMs, ({ pid, output }) => {
1114
- updateMasterStatus(run, id, "running", { pid, output });
1115
- }, ({ pid, output, elapsed_ms }) => {
1116
- updateMasterStatus(run, id, "running", { pid, output, elapsed_ms });
1117
- });
1118
- if (!result.ok) {
1119
1721
  return {
1120
1722
  id,
1121
- error: result.timedOut ? "timeout" : `exit code ${result.code}`,
1122
- raw: cleanLog(result.stderr || result.stdout || result.text || "master execution failed"),
1723
+ opinion: attachMasterRuntimeProvenance(run, id, {
1724
+ ...reconciled.opinion,
1725
+ voice_status: "dry_run",
1726
+ dedicated_worker: { status: "dry_run", language: run.language, execution_mode: "dry_run" },
1727
+ }, resolvedEngine),
1728
+ engine: resolvedEngine,
1123
1729
  };
1124
1730
  }
1125
- try {
1731
+
1732
+ let pid = null;
1733
+ const execute = async (workerPrompt, budgetMs, attempt) => {
1734
+ const allowedMs = remainingCouncilBudget(run, budgetMs);
1735
+ if (allowedMs <= 0) return { ...deadlineResult(run), budget_ms: 0 };
1736
+ const result = await runCodex(workerPrompt, allowedMs, ({ pid: workerPid, output }) => {
1737
+ pid = workerPid;
1738
+ updateMasterStatus(run, id, "running", { pid: workerPid, output, attempts: attempt });
1739
+ }, ({ pid: workerPid, output, elapsed_ms }) => {
1740
+ updateMasterStatus(run, id, "running", { pid: workerPid, output, elapsed_ms, attempts: attempt });
1741
+ }, { search: false, sigkillGraceMs: councilKillGrace(run) });
1742
+ return { ...result, budget_ms: allowedMs };
1743
+ };
1744
+ const parse = (result) => {
1745
+ if (frozenOpinion) {
1746
+ const voice = normalizeMasterVoice(extractJson(result.text), id, run, frozenOpinion, result.text);
1747
+ assertReaderLanguage([
1748
+ voice.statement,
1749
+ ...(voice.key_findings || []),
1750
+ ...(voice.disagreements || []),
1751
+ ...(voice.what_would_change_my_mind || []),
1752
+ ].filter(Boolean).join("\n"), run.language, `master voice ${id}`);
1753
+ return attachMasterRuntimeProvenance(run, id, {
1754
+ ...frozenOpinion,
1755
+ deterministic_summary: frozenOpinion.summary,
1756
+ summary: voice.statement,
1757
+ voice_statement: voice.statement,
1758
+ voice_status: "completed",
1759
+ voice_language: run.language,
1760
+ key_findings: voice.key_findings.length ? voice.key_findings : frozenOpinion.key_findings,
1761
+ disagreements: voice.disagreements,
1762
+ what_would_change_my_mind: voice.what_would_change_my_mind.length
1763
+ ? voice.what_would_change_my_mind
1764
+ : frozenOpinion.what_would_change_my_mind,
1765
+ source_ids: [...new Set([...(frozenOpinion.source_ids || []), ...voice.source_ids])],
1766
+ confidence: voice.confidence,
1767
+ dedicated_worker: { status: "completed", pid, language: run.language, execution_mode: "codex_exec" },
1768
+ }, engine);
1769
+ }
1126
1770
  const normalized = normalizeMasterOpinion(extractJson(result.text), id, run, result.text);
1771
+ assertReaderLanguage([
1772
+ normalized.summary,
1773
+ normalized.verdict,
1774
+ ...(normalized.key_findings || []),
1775
+ ...(normalized.disagreements || []),
1776
+ ...(normalized.what_would_change_my_mind || []),
1777
+ ].filter(Boolean).join("\n"), run.language, `legacy master voice ${id}`);
1127
1778
  const reconciled = reconcileMasterOpinion(run, id, normalized);
1128
1779
  const resolvedEngine = engine || reconciled.engine || (decision ? "v2_method_model" : "v1_prompt");
1129
- return { id, opinion: attachMasterRuntimeProvenance(run, id, reconciled.opinion, resolvedEngine), engine: resolvedEngine };
1130
- } catch (error) {
1131
- return { id, error: "parse_failed", raw: cleanLog(result.text || String(error?.message || error)) };
1780
+ return attachMasterRuntimeProvenance(run, id, {
1781
+ ...reconciled.opinion,
1782
+ voice_statement: reconciled.opinion.summary || reconciled.opinion.verdict,
1783
+ voice_status: "completed",
1784
+ voice_language: run.language,
1785
+ dedicated_worker: { status: "completed", pid, language: run.language, execution_mode: "codex_exec" },
1786
+ }, resolvedEngine);
1787
+ };
1788
+
1789
+ let result = await execute(prompt, timeoutMs, 1);
1790
+ if (!result.ok) {
1791
+ return { id, error: result.deadline_exhausted ? "global_deadline" : result.timedOut ? "timeout" : `exit code ${result.code}`, raw: cleanLog(result.stderr || result.stdout || "method worker failed") };
1792
+ }
1793
+ try {
1794
+ return { id, opinion: parse(result), engine };
1795
+ } catch (firstParseError) {
1796
+ const firstFailureKind = outputFailureKind(firstParseError);
1797
+ const repairBudget = Math.min(LIMITS.PARSE_REPAIR_MS, remainingCouncilBudget(run, LIMITS.PARSE_REPAIR_MS));
1798
+ if (repairBudget <= 0) return { id, error: firstFailureKind, raw: cleanLog(firstParseError?.message || result.text) };
1799
+ const repairPrompt = [
1800
+ "PARSE-ONLY TRANSPORT REPAIR. Do not browse, search, add facts or change the frozen method stance.",
1801
+ `Master ID: ${id}; required acknowledged stance: ${frozenOpinion?.stance || "use the original stance"}; output language: ${run.language}.`,
1802
+ frozenOpinion
1803
+ ? "Return one JSON object with master, acknowledged_stance, statement, key_findings, disagreements, what_would_change_my_mind, source_ids and confidence."
1804
+ : "Return one JSON object matching the master_opinion schema from the original prompt.",
1805
+ `Write every reader-facing value in ${run.language}. Translation is allowed only to repair language; preserve the frozen stance, facts, numbers and source IDs.`,
1806
+ `Malformed output:\n${String(result.text || "").slice(0, LIMITS.PARSE_REPAIR_INPUT_CHARS)}`,
1807
+ ].join("\n\n");
1808
+ result = await execute(repairPrompt, repairBudget, 2);
1809
+ if (!result.ok) return { id, error: result.deadline_exhausted ? "global_deadline" : result.timedOut ? "timeout" : `exit code ${result.code}`, raw: cleanLog(result.stderr || "method repair failed") };
1810
+ try {
1811
+ return { id, opinion: parse(result), engine };
1812
+ } catch (secondParseError) {
1813
+ return { id, error: outputFailureKind(secondParseError), raw: cleanLog(secondParseError?.message || result.text) };
1814
+ }
1132
1815
  }
1133
1816
  }, (error, { id }) => ({
1134
1817
  id,
@@ -1138,13 +1821,37 @@ export async function runHeadlessMasters(run, args = {}) {
1138
1821
 
1139
1822
  for (const outcome of outcomes) {
1140
1823
  if (!outcome.opinion) {
1141
- updateMasterStatus(run, outcome.id, "failed", { error: outcome.error, output: outcome.raw });
1824
+ const diagnosticPath = join(dir, `${outcome.id}.failure.json`);
1825
+ writeJson(diagnosticPath, {
1826
+ master: outcome.id,
1827
+ failure_kind: outcome.error,
1828
+ diagnostic: outcome.raw,
1829
+ public_summary: outcome.error === "reader_language_mismatch"
1830
+ ? localized(run.language, {
1831
+ en: "The dedicated method worker returned reader-facing content in the wrong language; no method-seat statement is available.",
1832
+ zh: "专属方法席 worker 返回了错误语言的读者内容;没有可用的方法席发言。",
1833
+ ja: "専用メソッド席ワーカーは指定と異なる言語の読者向け内容を返したため、利用可能なメソッド席の発言はありません。",
1834
+ ko: "전용 방법론 좌석 워커가 지정과 다른 언어의 독자용 내용을 반환해 사용할 수 있는 방법론 좌석 발언이 없습니다.",
1835
+ })
1836
+ : localized(run.language, {
1837
+ en: "The dedicated method worker did not complete; no method-seat statement is available.",
1838
+ zh: "专属方法席 worker 未完成;没有可用的方法席发言。",
1839
+ ja: "専用メソッド席ワーカーが完了せず、利用可能なメソッド席の発言はありません。",
1840
+ ko: "전용 방법론 좌석 워커가 완료되지 않아 사용할 수 있는 방법론 좌석 발언이 없습니다.",
1841
+ }),
1842
+ }, { mode: 0o600 });
1843
+ updateMasterStatus(run, outcome.id, "failed", { error: outcome.error, diagnostic: diagnosticPath });
1142
1844
  continue;
1143
1845
  }
1144
1846
  byId.set(outcome.id, outcome.opinion);
1145
1847
  writeJson(join(dir, `${outcome.id}.json`), outcome.opinion);
1146
1848
  updateMasterStatus(run, outcome.id, "completed", {
1147
1849
  engine: outcome.opinion.engine || outcome.engine,
1850
+ worker_kind: outcome.opinion.dedicated_worker?.execution_mode === "dry_run"
1851
+ ? "dedicated_method_voice_dry_run"
1852
+ : "dedicated_method_worker",
1853
+ worker_pid: outcome.opinion.dedicated_worker?.pid || null,
1854
+ voice_status: outcome.opinion.voice_status || "completed",
1148
1855
  completed_at: new Date().toISOString(),
1149
1856
  output: join(dir, `${outcome.id}.json`),
1150
1857
  });
@@ -1162,50 +1869,112 @@ export async function runHeadlessMasters(run, args = {}) {
1162
1869
  export async function runDebateRole(run, role, context, timeoutMs) {
1163
1870
  const prompt = debatePrompt(role, run, context);
1164
1871
  updateAgent(run, role, "running", { started_at: new Date().toISOString(), round: context.round });
1165
- const result = timeoutMs <= 0
1166
- ? deadlineResult()
1872
+ let result = timeoutMs <= 0
1873
+ ? deadlineResult(run)
1167
1874
  : await runCodex(prompt, timeoutMs, ({ pid, output }) => {
1168
1875
  updateAgent(run, role, "running", { pid, output, round: context.round });
1169
1876
  }, ({ pid, output, elapsed_ms }) => {
1170
1877
  updateAgent(run, role, "running", { pid, output, round: context.round });
1171
1878
  appendEvent(run, "agent_heartbeat", { role, round: context.round, pid, output, elapsed_ms });
1172
- });
1173
- const packet = debateFromCodex(result, role, run, prompt);
1879
+ }, { search: false, sigkillGraceMs: councilKillGrace(run) });
1880
+ const enforceLanguage = (candidate) => {
1881
+ if (candidate?.failure_kind) return candidate;
1882
+ try {
1883
+ assertReaderLanguage([
1884
+ candidate?.summary,
1885
+ ...(candidate?.long_thesis || []),
1886
+ ...(candidate?.short_thesis || []),
1887
+ ...(candidate?.catalysts || []),
1888
+ ...(candidate?.risks || []),
1889
+ candidate?.position,
1890
+ ...(candidate?.invalidation || []),
1891
+ ...(candidate?.questions || []),
1892
+ ...(candidate?.questions_answered || []).flatMap((item) => [item?.question, item?.answer]),
1893
+ candidate?.report_markdown,
1894
+ ].filter(Boolean).join("\n"), run.language, `${role} debate output`);
1895
+ return candidate;
1896
+ } catch (error) {
1897
+ return debateFailurePacket(role, run, "reader_language_mismatch");
1898
+ }
1899
+ };
1900
+ let packet = enforceLanguage(debateFromCodex(result, role, run, prompt));
1901
+ if (result.ok && ["parse_failed", "reader_language_mismatch"].includes(packet.failure_kind)) {
1902
+ const repairBudget = Math.min(LIMITS.PARSE_REPAIR_MS, remainingCouncilBudget(run, LIMITS.PARSE_REPAIR_MS));
1903
+ if (repairBudget > 0) {
1904
+ const repairReason = packet.failure_kind;
1905
+ appendEvent(run, "agent_parse_repair", { role, round: context.round, budget_ms: repairBudget, reason: repairReason });
1906
+ const repairPrompt = [
1907
+ "PARSE-ONLY TRANSPORT REPAIR. Do not search, browse, add facts or redo the analysis.",
1908
+ `Role: ${role}; symbol: ${run.symbol}; as_of: ${run.as_of}; reader language: ${run.language}; round: ${context.round || "final"}.`,
1909
+ repairReason === "reader_language_mismatch"
1910
+ ? "Translate only the reader-facing strings in the supplied valid debate-packet JSON. Preserve exact round-2 questions, exact round-3 question bindings, facts, numbers, source IDs and uncertainty. Return one JSON object only."
1911
+ : "Convert only the supplied malformed output into one valid debate-packet JSON object. Preserve exact round-2 questions and exact round-3 question bindings when present. Return JSON only.",
1912
+ `Write every reader-facing value in ${run.language}. Translation is allowed only to repair language; preserve facts, numbers, source IDs and exact Q&A bindings.`,
1913
+ `Malformed output:\n${String(result.text || "").slice(0, LIMITS.PARSE_REPAIR_INPUT_CHARS)}`,
1914
+ ].join("\n\n");
1915
+ result = await runCodex(repairPrompt, repairBudget, ({ pid, output }) => {
1916
+ updateAgent(run, role, "running", { pid, output, round: context.round, attempts: 2 });
1917
+ }, () => {}, { search: false, sigkillGraceMs: councilKillGrace(run) });
1918
+ packet = enforceLanguage(debateFromCodex(result, role, run, repairPrompt));
1919
+ }
1920
+ }
1174
1921
  const roundCompletedAt = new Date().toISOString();
1175
- // A role can run three times. Leaving it marked `running` after one awaited invocation
1176
- // returned made sequential rounds look concurrent in status.json. Record the dependency
1177
- // boundary explicitly while reserving `completed` for the merged three-round artifact.
1178
- updateAgent(run, role, "waiting", {
1179
- round: context.round,
1180
- round_status: "completed",
1181
- last_completed_round: context.round,
1182
- round_completed_at: roundCompletedAt,
1183
- pid: null,
1184
- output: null,
1185
- });
1186
- appendEvent(run, "agent_round_completed", {
1187
- role,
1188
- round: context.round,
1189
- ok: result.ok,
1190
- timed_out: result.timedOut === true,
1191
- verdict: packet.verdict,
1192
- question_count: packet.questions.length,
1193
- answered_count: packet.questions_answered.length,
1194
- });
1922
+ if (Number.isInteger(context.round)) {
1923
+ // A role can run three times. Leaving it marked `running` after one awaited invocation
1924
+ // returned made sequential rounds look concurrent in status.json. Record the dependency
1925
+ // boundary explicitly while reserving `completed` for the merged three-round artifact.
1926
+ updateAgent(run, role, "waiting", {
1927
+ round: context.round,
1928
+ round_status: "completed",
1929
+ last_completed_round: context.round,
1930
+ round_completed_at: roundCompletedAt,
1931
+ pid: null,
1932
+ output: null,
1933
+ });
1934
+ appendEvent(run, "agent_round_completed", {
1935
+ role,
1936
+ round: context.round,
1937
+ ok: result.ok,
1938
+ timed_out: result.timedOut === true,
1939
+ verdict: packet.verdict,
1940
+ failure_kind: packet.failure_kind,
1941
+ question_count: packet.questions.length,
1942
+ answered_count: packet.questions_answered.length,
1943
+ });
1944
+ } else {
1945
+ updateAgent(run, role, "waiting", {
1946
+ synthesis_status: "completed",
1947
+ synthesis_completed_at: roundCompletedAt,
1948
+ pid: null,
1949
+ output: null,
1950
+ });
1951
+ appendEvent(run, "agent_role_completed", {
1952
+ role,
1953
+ ok: result.ok,
1954
+ timed_out: result.timedOut === true,
1955
+ verdict: packet.verdict,
1956
+ failure_kind: packet.failure_kind,
1957
+ });
1958
+ }
1195
1959
  return { packet, result };
1196
1960
  }
1197
1961
 
1198
1962
  function debateFailure(step) {
1199
- if (step.result.ok && step.packet.verdict !== "PARSE_FAILED") return undefined;
1963
+ if (!step.result.ok) {
1964
+ if (step.result.deadline_exhausted) return "global_deadline";
1965
+ if (step.result.timedOut) return "timeout";
1966
+ if (Number.isInteger(step.result.code)) return `exit code ${step.result.code}`;
1967
+ return "unexpected_error";
1968
+ }
1969
+ if (step.packet.failure_kind) return step.packet.failure_kind;
1200
1970
  if (step.packet.verdict === "PARSE_FAILED") return "parse_failed";
1201
- if (step.result.deadline_exhausted) return "global_deadline";
1202
- return step.result.timedOut ? "timeout" : `exit code ${step.result.code ?? "unknown"}`;
1971
+ return undefined;
1203
1972
  }
1204
1973
 
1205
1974
  async function synthesizeQuickDecision(run, args, timeoutMs, outputMode) {
1206
1975
  const dir = runPath(run.run_id);
1207
1976
  appendEvent(run, "debate_round", { round: 1, format: "single_round_parallel" });
1208
- const sideBudget = remainingQuickBudget(run, timeoutMs);
1977
+ const sideBudget = remainingCouncilBudget(run, timeoutMs);
1209
1978
  const [bullOutcome, bearOutcome] = await Promise.allSettled([
1210
1979
  runDebateRole(run, "bull_researcher", { round: 1, brief: "quick long case" }, sideBudget),
1211
1980
  runDebateRole(run, "bear_researcher", { round: 1, brief: "quick short case" }, sideBudget),
@@ -1249,21 +2018,11 @@ async function synthesizeQuickDecision(run, args, timeoutMs, outputMode) {
1249
2018
  });
1250
2019
  writeAllAgentsMarkdown(run, { bull, bear });
1251
2020
 
1252
- const managerPrompt = debatePrompt("portfolio_manager", run, { bull, bear, outputMode });
1253
- updateAgent(run, "portfolio_manager", "running", { started_at: new Date().toISOString() });
1254
- const managerBudget = remainingQuickBudget(run, timeoutMs);
1255
- const managerResult = managerBudget <= 0
1256
- ? deadlineResult()
1257
- : await runCodex(managerPrompt, managerBudget, ({ pid, output }) => {
1258
- updateAgent(run, "portfolio_manager", "running", { pid, output });
1259
- }, ({ pid, output, elapsed_ms }) => {
1260
- updateAgent(run, "portfolio_manager", "running", { pid, output });
1261
- appendEvent(run, "agent_heartbeat", { role: "portfolio_manager", pid, output, elapsed_ms });
1262
- });
1263
- const manager = managerResult.ok
1264
- ? debateFromCodex(managerResult, "portfolio_manager", run, managerPrompt)
1265
- : managerFallback(run, args.prompt || "");
1266
- const managerOk = managerResult.ok && manager.verdict !== "PARSE_FAILED";
2021
+ const managerBudget = remainingCouncilBudget(run, timeoutMs);
2022
+ const managerStep = await runDebateRole(run, "portfolio_manager", { bull, bear, outputMode }, managerBudget);
2023
+ const managerError = debateFailure(managerStep);
2024
+ const managerOk = !managerError;
2025
+ const manager = managerOk ? managerStep.packet : managerFallback(run, args.prompt || "");
1267
2026
  writeJson(join(dir, "manager_synthesis.json"), manager);
1268
2027
  writeJson(join(dir, "decision.json"), manager);
1269
2028
  updateAgent(run, "portfolio_manager", managerOk ? "completed" : "failed", {
@@ -1271,9 +2030,7 @@ async function synthesizeQuickDecision(run, args, timeoutMs, outputMode) {
1271
2030
  output: join(dir, "manager_synthesis.json"),
1272
2031
  error: managerOk
1273
2032
  ? undefined
1274
- : manager.verdict === "PARSE_FAILED"
1275
- ? "parse_failed"
1276
- : managerResult.deadline_exhausted ? "global_deadline" : managerResult.timedOut ? "timeout" : `exit code ${managerResult.code ?? "unknown"}`,
2033
+ : managerError,
1277
2034
  });
1278
2035
 
1279
2036
  const gate = verificationStatus(run);
@@ -1340,6 +2097,47 @@ function finalizeBeforeDebate(run, args, reason) {
1340
2097
  return { bull: null, bear: null, manager, ...finalArtifacts };
1341
2098
  }
1342
2099
 
2100
+ function finalizeAfterDebateFailure(run, args, reason, bullRounds = [], bearRounds = []) {
2101
+ const dir = runPath(run.run_id);
2102
+ const bull = mergeDebateRounds(bullRounds.map((step) => step?.packet).filter(Boolean));
2103
+ const bear = mergeDebateRounds(bearRounds.map((step) => step?.packet).filter(Boolean));
2104
+ if (bull) writeJson(join(dir, "bull_researcher.json"), bull);
2105
+ if (bear) writeJson(join(dir, "bear_researcher.json"), bear);
2106
+ updateAgent(run, "bull_researcher", "failed", {
2107
+ error: reason,
2108
+ completed_at: new Date().toISOString(),
2109
+ output: bull ? join(dir, "bull_researcher.json") : undefined,
2110
+ });
2111
+ updateAgent(run, "bear_researcher", "failed", {
2112
+ error: reason,
2113
+ completed_at: new Date().toISOString(),
2114
+ output: bear ? join(dir, "bear_researcher.json") : undefined,
2115
+ });
2116
+ updateAgent(run, "portfolio_manager", "skipped", {
2117
+ error: reason,
2118
+ completed_at: new Date().toISOString(),
2119
+ });
2120
+ const manager = managerFallback(run, args.prompt || "");
2121
+ writeJson(join(dir, "manager_synthesis.json"), manager);
2122
+ writeJson(join(dir, "decision.json"), manager);
2123
+ run.phase = "incomplete";
2124
+ run.status = "incomplete";
2125
+ run.completed_at = new Date().toISOString();
2126
+ const completeness = completenessStatus(run);
2127
+ appendEvent(run, "incomplete", {
2128
+ reason,
2129
+ downstream_model_calls_skipped: true,
2130
+ missing_evidence: completeness.missing_evidence,
2131
+ missing_debate: completeness.missing_debate,
2132
+ missing_masters: completeness.missing_masters,
2133
+ });
2134
+ const finalArtifacts = writeFinalArtifacts(run, { bull, bear, manager });
2135
+ writeJson(join(dir, "evidence.json"), run);
2136
+ writeStatus(run);
2137
+ writeAllAgentsMarkdown(run, { bull, bear, manager });
2138
+ return { bull, bear, manager, ...finalArtifacts };
2139
+ }
2140
+
1343
2141
  export async function synthesizeDecision(run, args) {
1344
2142
  const dir = runPath(run.run_id);
1345
2143
  const requestedSynthesisMs = Number.isFinite(args.synthesis_timeout_ms)
@@ -1347,7 +2145,7 @@ export async function synthesizeDecision(run, args) {
1347
2145
  : Number(args.timeout_ms || LIMITS.CODEX_TIMEOUT_MS);
1348
2146
  const timeoutMs = run.council_mode === "quick"
1349
2147
  ? Math.min(requestedSynthesisMs, LIMITS.QUICK_SYNTHESIS_MS)
1350
- : requestedSynthesisMs;
2148
+ : Math.min(requestedSynthesisMs, LIMITS.FULL_DEBATE_MS);
1351
2149
  const outputMode = OUTPUT_MODES.includes(args.output_mode) ? args.output_mode : "public_equity";
1352
2150
  run.phase = "debate";
1353
2151
  run.status = "running";
@@ -1403,28 +2201,66 @@ export async function synthesizeDecision(run, args) {
1403
2201
  return synthesizeQuickDecision(run, args, timeoutMs, outputMode);
1404
2202
  }
1405
2203
 
1406
- // Three-round debate: R1 cases, R2 cross-rebuttal, R3 Q&A.
1407
- appendEvent(run, "debate_round", { round: 1 });
1408
- const bullR1 = await runDebateRole(run, "bull_researcher", { round: 1, brief: "long" }, timeoutMs);
1409
- const bearR1 = await runDebateRole(run, "bear_researcher", { round: 1, brief: "short", bull: bullR1.packet }, timeoutMs);
1410
-
1411
- appendEvent(run, "debate_round", { round: 2 });
1412
- const bullR2 = await runDebateRole(run, "bull_researcher", { round: 2, otherCaseR1: bearR1.packet }, timeoutMs);
1413
- const bearR2 = await runDebateRole(run, "bear_researcher", { round: 2, otherCaseR1: bullR1.packet }, timeoutMs);
1414
-
1415
- appendEvent(run, "debate_round", { round: 3 });
1416
- const bullR3 = await runDebateRole(run, "bull_researcher", {
1417
- round: 3,
1418
- otherCaseR1: bearR2.packet,
1419
- questionsYouAsked: bullR2.packet.questions,
1420
- questionsForYou: bearR2.packet.questions,
1421
- }, timeoutMs);
1422
- const bearR3 = await runDebateRole(run, "bear_researcher", {
1423
- round: 3,
1424
- otherCaseR1: bullR2.packet,
1425
- questionsYouAsked: bearR2.packet.questions,
1426
- questionsForYou: bullR2.packet.questions,
1427
- }, timeoutMs);
2204
+ const rejectedStep = (reason, role) => {
2205
+ const result = {
2206
+ ok: false, code: null, text: "", stderr: cleanLog(reason?.message || reason), stdout: "",
2207
+ timedOut: false, unexpected_error: true,
2208
+ };
2209
+ return { result, packet: debateFromCodex(result, role, run, "unexpected debate failure") };
2210
+ };
2211
+ const parallelRound = async (round, bullContext, bearContext) => {
2212
+ const budget = remainingCouncilBudget(run, timeoutMs);
2213
+ appendEvent(run, "debate_round", { round, format: "parallel_per_round", budget_ms: budget });
2214
+ const [bullOutcome, bearOutcome] = await Promise.allSettled([
2215
+ runDebateRole(run, "bull_researcher", { round, ...bullContext }, budget),
2216
+ runDebateRole(run, "bear_researcher", { round, ...bearContext }, budget),
2217
+ ]);
2218
+ return {
2219
+ bull: bullOutcome.status === "fulfilled" ? bullOutcome.value : rejectedStep(bullOutcome.reason, "bull_researcher"),
2220
+ bear: bearOutcome.status === "fulfilled" ? bearOutcome.value : rejectedStep(bearOutcome.reason, "bear_researcher"),
2221
+ };
2222
+ };
2223
+ const failedRound = (round) => [debateFailure(round.bull), debateFailure(round.bear)].filter(Boolean);
2224
+
2225
+ // Three-round debate with a strict inter-round barrier and parallel sides per round.
2226
+ const r1 = await parallelRound(1, { brief: "long" }, { brief: "short" });
2227
+ if (failedRound(r1).length) {
2228
+ return finalizeAfterDebateFailure(run, args, `debate_round_1_failed:${failedRound(r1).join(",")}`, [r1.bull], [r1.bear]);
2229
+ }
2230
+
2231
+ const r2 = await parallelRound(2,
2232
+ { otherCaseR1: r1.bear.packet },
2233
+ { otherCaseR1: r1.bull.packet });
2234
+ if (failedRound(r2).length) {
2235
+ return finalizeAfterDebateFailure(run, args, `debate_round_2_failed:${failedRound(r2).join(",")}`, [r1.bull, r2.bull], [r1.bear, r2.bear]);
2236
+ }
2237
+ const exactlyThreeQuestions = (packet) => Array.isArray(packet?.questions)
2238
+ && packet.questions.length === 3
2239
+ && packet.questions.every((question) => typeof question === "string" && question.trim());
2240
+ if (!exactlyThreeQuestions(r2.bull.packet) || !exactlyThreeQuestions(r2.bear.packet)) {
2241
+ appendEvent(run, "debate_qna_gate", { status: "failed", errors: ["round 2 did not produce exactly three questions per side"] });
2242
+ return finalizeAfterDebateFailure(run, args, "debate_round_2_questions_incomplete", [r1.bull, r2.bull], [r1.bear, r2.bear]);
2243
+ }
2244
+
2245
+ const r3 = await parallelRound(3, {
2246
+ otherCaseR1: r2.bear.packet,
2247
+ questionsYouAsked: r2.bull.packet.questions,
2248
+ questionsForYou: r2.bear.packet.questions,
2249
+ }, {
2250
+ otherCaseR1: r2.bull.packet,
2251
+ questionsYouAsked: r2.bear.packet.questions,
2252
+ questionsForYou: r2.bull.packet.questions,
2253
+ });
2254
+ if (failedRound(r3).length) {
2255
+ return finalizeAfterDebateFailure(run, args, `debate_round_3_failed:${failedRound(r3).join(",")}`, [r1.bull, r2.bull, r3.bull], [r1.bear, r2.bear, r3.bear]);
2256
+ }
2257
+
2258
+ const bullR1 = r1.bull;
2259
+ const bearR1 = r1.bear;
2260
+ const bullR2 = r2.bull;
2261
+ const bearR2 = r2.bear;
2262
+ const bullR3 = r3.bull;
2263
+ const bearR3 = r3.bear;
1428
2264
 
1429
2265
  const bull = mergeDebateRounds([bullR1.packet, bullR2.packet, bullR3.packet]);
1430
2266
  const bear = mergeDebateRounds([bearR1.packet, bearR2.packet, bearR3.packet]);
@@ -1435,6 +2271,9 @@ export async function synthesizeDecision(run, args) {
1435
2271
  bearR3: bearR3.packet,
1436
2272
  });
1437
2273
  appendEvent(run, "debate_qna_gate", qnaGate);
2274
+ if (qnaGate.status !== "passed") {
2275
+ return finalizeAfterDebateFailure(run, args, "debate_round_3_qna_incomplete", [bullR1, bullR2, bullR3], [bearR1, bearR2, bearR3]);
2276
+ }
1438
2277
  const bullQnaErrors = qnaGate.errors.filter((error) => error.startsWith("bull_researcher"));
1439
2278
  const bearQnaErrors = qnaGate.errors.filter((error) => error.startsWith("bear_researcher"));
1440
2279
  const bullTransportOk = [bullR1, bullR2, bullR3].every((step) => step.result.ok);
@@ -1472,24 +2311,17 @@ export async function synthesizeDecision(run, args) {
1472
2311
  });
1473
2312
  writeAllAgentsMarkdown(run, { bull, bear });
1474
2313
 
1475
- const managerPrompt = debatePrompt("portfolio_manager", run, { bull, bear, outputMode });
1476
- updateAgent(run, "portfolio_manager", "running", { started_at: new Date().toISOString() });
1477
- const managerResult = await runCodex(managerPrompt, timeoutMs, ({ pid, output }) => {
1478
- updateAgent(run, "portfolio_manager", "running", { pid, output });
1479
- }, ({ pid, output, elapsed_ms }) => {
1480
- updateAgent(run, "portfolio_manager", "running", { pid, output });
1481
- appendEvent(run, "agent_heartbeat", { role: "portfolio_manager", pid, output, elapsed_ms });
1482
- });
1483
- const manager = managerResult.ok
1484
- ? debateFromCodex(managerResult, "portfolio_manager", run, managerPrompt)
1485
- : managerFallback(run, args.prompt || "");
2314
+ const pmBudget = remainingCouncilBudget(run, Math.min(requestedSynthesisMs, LIMITS.FULL_PM_MS));
2315
+ const managerStep = await runDebateRole(run, "portfolio_manager", { bull, bear, outputMode }, pmBudget);
2316
+ const managerOk = !debateFailure(managerStep);
2317
+ const manager = managerOk ? managerStep.packet : managerFallback(run, args.prompt || "");
1486
2318
  const gate = verificationStatus(run);
1487
2319
  writeJson(join(dir, "manager_synthesis.json"), manager);
1488
2320
  writeJson(join(dir, "decision.json"), manager);
1489
- updateAgent(run, "portfolio_manager", managerResult.ok && manager.verdict !== "PARSE_FAILED" ? "completed" : "failed", {
2321
+ updateAgent(run, "portfolio_manager", managerOk ? "completed" : "failed", {
1490
2322
  completed_at: new Date().toISOString(),
1491
2323
  output: join(dir, "manager_synthesis.json"),
1492
- error: managerResult.ok ? undefined : (managerResult.timedOut ? "timeout" : `exit code ${managerResult.code}`),
2324
+ error: managerOk ? undefined : debateFailure(managerStep),
1493
2325
  });
1494
2326
  const completeness = completenessStatus(run);
1495
2327
  run.completed_at = new Date().toISOString();
@@ -1532,7 +2364,7 @@ export async function analyzeSymbol(args) {
1532
2364
  artifacts: debate.artifacts || artifactPaths(run),
1533
2365
  };
1534
2366
  }
1535
- if (run.council_mode === "quick" && remainingQuickBudget(run, 1) <= 0) {
2367
+ if (remainingCouncilBudget(run, 1) <= 0) {
1536
2368
  const debate = finalizeBeforeDebate(run, args, "global_deadline_before_masters");
1537
2369
  return {
1538
2370
  run,
@@ -1546,8 +2378,7 @@ export async function analyzeSymbol(args) {
1546
2378
  }
1547
2379
  await runHeadlessMasters(run, args);
1548
2380
  gate = completenessStatus(run);
1549
- if (gate.missing_masters.length > 0
1550
- || (run.council_mode === "quick" && remainingQuickBudget(run, 1) <= 0)) {
2381
+ if (gate.missing_masters.length > 0 || remainingCouncilBudget(run, 1) <= 0) {
1551
2382
  const debate = finalizeBeforeDebate(run, args,
1552
2383
  gate.missing_masters.length ? "master_gate_failed" : "global_deadline_before_debate");
1553
2384
  return {