alphacouncil-agent 0.5.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 (139) hide show
  1. package/.claude/agents/alphacouncil-bear_researcher.md +39 -0
  2. package/.claude/agents/alphacouncil-bull_researcher.md +40 -0
  3. package/.claude/agents/alphacouncil-earnings_deep_dive.md +49 -0
  4. package/.claude/agents/alphacouncil-forward_expectations.md +48 -0
  5. package/.claude/agents/alphacouncil-ib_event_analysis.md +40 -0
  6. package/.claude/agents/alphacouncil-insider_sec.md +39 -0
  7. package/.claude/agents/alphacouncil-macro_regime.md +31 -0
  8. package/.claude/agents/alphacouncil-market_data.md +37 -0
  9. package/.claude/agents/alphacouncil-market_narrative.md +45 -0
  10. package/.claude/agents/alphacouncil-news_industry_management.md +49 -0
  11. package/.claude/agents/alphacouncil-portfolio_manager.md +22 -0
  12. package/.claude/agents/alphacouncil-quant_factor.md +38 -0
  13. package/.claude/agents/alphacouncil-social_pulse.md +49 -0
  14. package/.claude/agents/alphacouncil-valuation_long_short.md +45 -0
  15. package/.claude-plugin/marketplace.json +30 -0
  16. package/.claude-plugin/plugin.json +28 -0
  17. package/.codex-plugin/plugin.json +32 -0
  18. package/.grok/agents/alphacouncil-bear_researcher.md +41 -0
  19. package/.grok/agents/alphacouncil-bull_researcher.md +42 -0
  20. package/.grok/agents/alphacouncil-earnings_deep_dive.md +50 -0
  21. package/.grok/agents/alphacouncil-forward_expectations.md +49 -0
  22. package/.grok/agents/alphacouncil-ib_event_analysis.md +41 -0
  23. package/.grok/agents/alphacouncil-insider_sec.md +40 -0
  24. package/.grok/agents/alphacouncil-macro_regime.md +32 -0
  25. package/.grok/agents/alphacouncil-market_data.md +38 -0
  26. package/.grok/agents/alphacouncil-market_narrative.md +46 -0
  27. package/.grok/agents/alphacouncil-news_industry_management.md +50 -0
  28. package/.grok/agents/alphacouncil-portfolio_manager.md +24 -0
  29. package/.grok/agents/alphacouncil-quant_factor.md +39 -0
  30. package/.grok/agents/alphacouncil-social_pulse.md +50 -0
  31. package/.grok/agents/alphacouncil-valuation_long_short.md +46 -0
  32. package/.grok/config.toml +4 -0
  33. package/.mcp.json +11 -0
  34. package/.opencode/agent/alphacouncil-bear_researcher.md +45 -0
  35. package/.opencode/agent/alphacouncil-bull_researcher.md +46 -0
  36. package/.opencode/agent/alphacouncil-earnings_deep_dive.md +54 -0
  37. package/.opencode/agent/alphacouncil-forward_expectations.md +53 -0
  38. package/.opencode/agent/alphacouncil-ib_event_analysis.md +45 -0
  39. package/.opencode/agent/alphacouncil-insider_sec.md +44 -0
  40. package/.opencode/agent/alphacouncil-macro_regime.md +36 -0
  41. package/.opencode/agent/alphacouncil-market_data.md +42 -0
  42. package/.opencode/agent/alphacouncil-market_narrative.md +50 -0
  43. package/.opencode/agent/alphacouncil-news_industry_management.md +54 -0
  44. package/.opencode/agent/alphacouncil-portfolio_manager.md +28 -0
  45. package/.opencode/agent/alphacouncil-quant_factor.md +43 -0
  46. package/.opencode/agent/alphacouncil-social_pulse.md +54 -0
  47. package/.opencode/agent/alphacouncil-valuation_long_short.md +50 -0
  48. package/AGENTS.md +96 -0
  49. package/CHANGELOG.md +112 -0
  50. package/CLAUDE.md +144 -0
  51. package/LICENSE +21 -0
  52. package/README.ja.md +282 -0
  53. package/README.md +361 -0
  54. package/README.zh-CN.md +282 -0
  55. package/data/industry-map.json +74 -0
  56. package/docs/attribution.md +99 -0
  57. package/docs/personas.md +59 -0
  58. package/docs/report-contract.md +110 -0
  59. package/mcp/lib/codex.mjs +152 -0
  60. package/mcp/lib/constants.mjs +139 -0
  61. package/mcp/lib/council-options.mjs +111 -0
  62. package/mcp/lib/errors.mjs +46 -0
  63. package/mcp/lib/feeds.mjs +140 -0
  64. package/mcp/lib/fsutil.mjs +69 -0
  65. package/mcp/lib/gates.mjs +208 -0
  66. package/mcp/lib/grounding.mjs +252 -0
  67. package/mcp/lib/headings.mjs +65 -0
  68. package/mcp/lib/industry.mjs +208 -0
  69. package/mcp/lib/lang.mjs +24 -0
  70. package/mcp/lib/macro.mjs +167 -0
  71. package/mcp/lib/markdown.mjs +337 -0
  72. package/mcp/lib/markets-kr-jp.mjs +180 -0
  73. package/mcp/lib/markets.mjs +261 -0
  74. package/mcp/lib/narrative.mjs +235 -0
  75. package/mcp/lib/options.mjs +216 -0
  76. package/mcp/lib/orchestrator.mjs +541 -0
  77. package/mcp/lib/output-modes.mjs +144 -0
  78. package/mcp/lib/packets.mjs +256 -0
  79. package/mcp/lib/personas/frontmatter.mjs +80 -0
  80. package/mcp/lib/personas/registry.mjs +215 -0
  81. package/mcp/lib/preflight.mjs +174 -0
  82. package/mcp/lib/prompts.mjs +151 -0
  83. package/mcp/lib/quotes.mjs +108 -0
  84. package/mcp/lib/rpc.mjs +737 -0
  85. package/mcp/lib/run-store.mjs +140 -0
  86. package/mcp/lib/screen.mjs +283 -0
  87. package/mcp/lib/sec.mjs +131 -0
  88. package/mcp/lib/social.mjs +261 -0
  89. package/mcp/lib/tables.mjs +150 -0
  90. package/mcp/lib/text.mjs +27 -0
  91. package/mcp/lib/weights.mjs +150 -0
  92. package/mcp/server.mjs +92 -0
  93. package/opencode.json +20 -0
  94. package/package.json +61 -0
  95. package/personas/_shared/_debate_base.md +46 -0
  96. package/personas/_shared/_evidence_base.md +56 -0
  97. package/personas/_shared/_master_base.md +77 -0
  98. package/personas/analysts/earnings_deep_dive.md +118 -0
  99. package/personas/analysts/forward_expectations.md +115 -0
  100. package/personas/analysts/ib_event_analysis.md +100 -0
  101. package/personas/analysts/insider_sec.md +98 -0
  102. package/personas/analysts/macro_regime.md +66 -0
  103. package/personas/analysts/market_data.md +94 -0
  104. package/personas/analysts/market_narrative.md +94 -0
  105. package/personas/analysts/news_industry_management.md +117 -0
  106. package/personas/analysts/quant_factor.md +97 -0
  107. package/personas/analysts/social_pulse.md +102 -0
  108. package/personas/analysts/valuation_long_short.md +109 -0
  109. package/personas/debate/bear_researcher.md +96 -0
  110. package/personas/debate/bull_researcher.md +98 -0
  111. package/personas/debate/portfolio_manager.md +64 -0
  112. package/personas/masters/masters-adversarial/burry.md +137 -0
  113. package/personas/masters/masters-adversarial/dalio.md +139 -0
  114. package/personas/masters/masters-adversarial/druckenmiller.md +144 -0
  115. package/personas/masters/masters-adversarial/short_seller.md +145 -0
  116. package/personas/masters/masters-adversarial/soros.md +137 -0
  117. package/personas/masters/masters-modern/aschenbrenner.md +147 -0
  118. package/personas/masters/masters-options/natenberg.md +168 -0
  119. package/personas/masters/masters-options/sinclair.md +166 -0
  120. package/personas/masters/masters-options/taleb.md +163 -0
  121. package/personas/masters/masters-quant/asness.md +142 -0
  122. package/personas/masters/masters-quant/simons.md +143 -0
  123. package/personas/masters/masters-quant/thorp.md +143 -0
  124. package/personas/masters/masters-value/buffett.md +123 -0
  125. package/personas/masters/masters-value/duan_yongping.md +142 -0
  126. package/personas/masters/masters-value/li_lu.md +146 -0
  127. package/personas/masters/masters-value/munger.md +117 -0
  128. package/personas/masters/masters-value-classic/fisher.md +140 -0
  129. package/personas/masters/masters-value-classic/graham.md +137 -0
  130. package/personas/masters/masters-value-classic/klarman.md +138 -0
  131. package/personas/masters/masters-value-classic/lynch.md +133 -0
  132. package/personas/masters/masters-value-classic/marks.md +145 -0
  133. package/personas/verifiers/rederivation.md +97 -0
  134. package/personas/verifiers/refuter.md +99 -0
  135. package/personas/verifiers/source_fidelity.md +91 -0
  136. package/skills/agent-skills-governance/SKILL.md +42 -0
  137. package/skills/alphacouncil-agent/SKILL.md +259 -0
  138. package/skills/investment-banking/SKILL.md +70 -0
  139. package/skills/public-equity-investing/SKILL.md +74 -0
@@ -0,0 +1,144 @@
1
+ import { OUTPUT_MODES } from "./constants.mjs";
2
+ import { isChineseLanguage } from "./lang.mjs";
3
+
4
+ /**
5
+ * The output shapes the portfolio manager can target.
6
+ *
7
+ * best_for/effect used to be Chinese-only strings that outputModeInstruction interpolated
8
+ * straight into English PM prompts, so an English run received two lines of Chinese
9
+ * instructions. They are per-language now.
10
+ *
11
+ * The former product_design / creative_production / sales modes described themselves as
12
+ * "not for investment summary" and have been removed. Unknown values still fall back to
13
+ * public_equity, so a host that passes an old name degrades instead of erroring.
14
+ */
15
+ export function summaryModes(language = "English") {
16
+ const zh = isChineseLanguage(language);
17
+ const pick = (pair) => (zh ? pair.zh : pair.en);
18
+ return MODES.map((mode) => ({
19
+ mode: mode.mode,
20
+ best_for: pick(mode.best_for),
21
+ effect: pick(mode.effect),
22
+ fit: mode.fit,
23
+ }));
24
+ }
25
+
26
+ const MODES = [
27
+ {
28
+ mode: "chat",
29
+ best_for: {
30
+ zh: "默认最终答复、快速判断、直接复制到聊天窗口。",
31
+ en: "Default final reply, quick judgment, paste straight into chat.",
32
+ },
33
+ effect: {
34
+ zh: "聊天里给不冗长但完整的交付摘要;完整报告必须写入 final_report.md,并覆盖新闻、财报、前瞻、估值、风险和文件位置。",
35
+ en: "A short but complete handoff in chat; the full report still goes to final_report.md and must cover news, earnings, forward thresholds, valuation, risks, and file locations.",
36
+ },
37
+ fit: "best_default",
38
+ },
39
+ {
40
+ mode: "documents",
41
+ best_for: {
42
+ zh: "正式投资备忘录、IC memo、可继续编辑的 Word/Google Docs 文档。",
43
+ en: "Formal investment memos, IC memos, editable Word/Google Docs deliverables.",
44
+ },
45
+ effect: {
46
+ zh: "更像严肃 memo:封面信息、执行摘要、论证、来源附录、风险和反证条件。",
47
+ en: "Reads like a serious memo: cover details, executive summary, argument, source appendix, risks, and invalidation conditions.",
48
+ },
49
+ fit: "best_formal_memo",
50
+ },
51
+ {
52
+ mode: "pdf",
53
+ best_for: {
54
+ zh: "只读交付、归档、对外发送、版式固定的正式报告。",
55
+ en: "Read-only delivery, archiving, external distribution, fixed-layout reports.",
56
+ },
57
+ effect: {
58
+ zh: "内容应先按报告写好,再渲染为 PDF;适合最终版,不适合迭代。",
59
+ en: "Write the report first, then render to PDF. Good for a final version, poor for iteration.",
60
+ },
61
+ fit: "best_locked_report",
62
+ },
63
+ {
64
+ mode: "presentations",
65
+ best_for: {
66
+ zh: "投资委员会汇报、pitch deck、老板快速看结论。",
67
+ en: "Investment-committee readouts, pitch decks, a fast look at the conclusion.",
68
+ },
69
+ effect: {
70
+ zh: "8-10 页 slide outline:结论页、多头页、空头页、估值页、催化剂页、风险页、仓位页。",
71
+ en: "An 8-10 slide outline: conclusion, long case, short case, valuation, catalysts, risks, position.",
72
+ },
73
+ fit: "best_committee_deck",
74
+ },
75
+ {
76
+ mode: "data_analytics",
77
+ best_for: {
78
+ zh: "指标表、KPI、估值敏感性、可视化 dashboard。",
79
+ en: "Metric tables, KPIs, valuation sensitivity, dashboard visuals.",
80
+ },
81
+ effect: {
82
+ zh: "适合把 revenue、margin、multiple、scenario 做成表和图;不适合单独写最终投资判断。",
83
+ en: "Good for turning revenue, margin, multiple and scenario work into tables and charts; not a substitute for the investment judgment itself.",
84
+ },
85
+ fit: "supporting_analytics",
86
+ },
87
+ {
88
+ mode: "public_equity",
89
+ best_for: {
90
+ zh: "上市股票 long/short、财报、估值、催化剂、仓位建议。",
91
+ en: "Listed-equity long/short, earnings, valuation, catalysts, position sizing.",
92
+ },
93
+ effect: {
94
+ zh: "最适合作为研究子代理和最终 PM memo 的核心材料。",
95
+ en: "The best fit for both the research subagents and the final PM memo.",
96
+ },
97
+ fit: "best_research_brain",
98
+ },
99
+ {
100
+ mode: "investment_banking",
101
+ best_for: {
102
+ zh: "增发、并购、融资、稀释、accretion/dilution、交易影响。",
103
+ en: "Offerings, M&A, financing, dilution, accretion/dilution, transaction impact.",
104
+ },
105
+ effect: {
106
+ zh: "适合写交易事件章节;没有活跃交易时不应让它主导最终结论。",
107
+ en: "Good for the transaction-event section; it should not drive the final conclusion when there is no live transaction.",
108
+ },
109
+ fit: "event_specialist",
110
+ },
111
+ ];
112
+
113
+ export function outputModeInstruction(mode, language = "English") {
114
+ const selected = OUTPUT_MODES.includes(mode) ? mode : "public_equity";
115
+ const picked = summaryModes(language).find((item) => item.mode === selected);
116
+ const chinese = isChineseLanguage(language);
117
+ return [
118
+ chinese ? "最终报告语言:中文。" : `Final report language: ${language}.`,
119
+ `Final output mode: ${selected}.`,
120
+ `Mode purpose: ${picked?.best_for || ""}`,
121
+ `Mode effect: ${picked?.effect || ""}`,
122
+ chinese
123
+ ? "report_markdown 必须是完整正文,不是运行说明。必须覆盖 report contract 的所有章节,尤其新闻、最新财报、前瞻门槛、卖方修正、估值、风险、仓位、数据缺口和来源表。聊天摘要可以简洁,但 final_report.md 不得偷懒。"
124
+ : "report_markdown must be the complete report body, not an execution note. It must cover every report-contract section, especially news, latest earnings, forward thresholds, sell-side revisions, valuation, risks, position sizing, data gaps, and source table. The chat handoff may be concise; final_report.md must not be lazy.",
125
+ selected === "presentations"
126
+ ? "In report_markdown, write a slide-by-slide outline with slide titles and concise bullets, not dense prose."
127
+ : "",
128
+ selected === "documents"
129
+ ? "In report_markdown, write a formal investment memo suitable for DOCX conversion: title, executive summary, recommendation, evidence, risks, source appendix."
130
+ : "",
131
+ selected === "pdf"
132
+ ? "In report_markdown, write a polished locked-report structure with page-ready headings, source table, and concise executive summary."
133
+ : "",
134
+ selected === "data_analytics"
135
+ ? "In report_markdown, emphasize tables, chart specs, metrics, scenario sensitivity, and dashboard-ready fields."
136
+ : "",
137
+ selected === "public_equity"
138
+ ? "In report_markdown, write like a Public Equity Investing memo-builder final output: Recommendation / Decision Ask, Executive Summary, Thesis and Evidence, What Must Be True, Valuation / Scenario Work, Risks and Disconfirmers, Catalysts and Monitoring, Short / Medium / Long-Term View, Implementation Considerations, Open Items, and Source Table. Make it read like a PM-facing equity research report, not a generic chatbot answer."
139
+ : "",
140
+ selected === "investment_banking"
141
+ ? "In report_markdown, write the final synthesis like a banker event-analysis section only when transaction evidence exists: transaction overview, dilution/accretion, EPS/share-count impact, net cash/debt, valuation multiple, stock-pressure implications, and deal risks. If there is no live transaction, state that Investment Banking is supporting context rather than the final owner."
142
+ : "",
143
+ ].filter(Boolean).join("\n");
144
+ }
@@ -0,0 +1,256 @@
1
+ import { join } from "node:path";
2
+ import { LIMITS, MASTER_STANCES, RATINGS } from "./constants.mjs";
3
+ import { internalError } from "./errors.mjs";
4
+ import { isChineseLanguage } from "./lang.mjs";
5
+ import { cleanLog, clip } from "./text.mjs";
6
+ import { scopedSourceId } from "./gates.mjs";
7
+ import { runPath } from "./run-store.mjs";
8
+ import { packetSummary } from "./markdown.mjs";
9
+
10
+ export function rawRecordText(packet) {
11
+ if (typeof packet?.raw_text === "string" && packet.raw_text.trim()) return packet.raw_text;
12
+ if (!packet || typeof packet !== "object" || Array.isArray(packet)) return JSON.stringify(packet || {}, null, 2);
13
+ const { raw_text, ...withoutRawText } = packet;
14
+ return JSON.stringify(withoutRawText, null, 2);
15
+ }
16
+
17
+ export function extractJson(text) {
18
+ try {
19
+ return JSON.parse(text);
20
+ } catch {
21
+ const start = text.indexOf("{");
22
+ const end = text.lastIndexOf("}");
23
+ if (start !== -1 && end > start) {
24
+ return JSON.parse(text.slice(start, end + 1));
25
+ }
26
+ throw internalError("subagent did not return JSON");
27
+ }
28
+ }
29
+
30
+ export function normalizePacket(packet, task, symbol, asOfDate, raw = "") {
31
+ const sourceIdMap = new Map();
32
+ const sources = Array.isArray(packet?.sources) ? packet.sources.map((source, index) => {
33
+ const original = String(source?.id || `S${index + 1}`);
34
+ const id = scopedSourceId(task, original, index);
35
+ sourceIdMap.set(original, id);
36
+ return { ...(source && typeof source === "object" ? source : {}), id };
37
+ }) : [];
38
+ const claims = Array.isArray(packet?.claims) ? packet.claims.map((claim) => ({
39
+ ...(claim && typeof claim === "object" ? claim : {}),
40
+ source_ids: Array.isArray(claim?.source_ids)
41
+ ? claim.source_ids.map((id) => sourceIdMap.get(String(id)) || scopedSourceId(task, id)).filter(Boolean)
42
+ : [],
43
+ })) : [];
44
+ return {
45
+ task,
46
+ symbol,
47
+ as_of: asOfDate,
48
+ summary: typeof packet?.summary === "string" ? packet.summary : raw.slice(0, LIMITS.CLEAN_LOG_BYTES),
49
+ claims,
50
+ metrics: packet?.metrics && typeof packet.metrics === "object" ? packet.metrics : {},
51
+ sources,
52
+ open_questions: Array.isArray(packet?.open_questions) ? packet.open_questions : [],
53
+ confidence: ["high", "medium", "low"].includes(packet?.confidence) ? packet.confidence : "low",
54
+ // How much material this task actually had. Deliberately separate from confidence:
55
+ // a rich-but-contradictory task can be A/low, a sparse-but-decisive one C/high.
56
+ information_richness: ["A", "B", "C"].includes(packet?.information_richness) ? packet.information_richness : "unrated",
57
+ thread_id: typeof packet?.thread_id === "string" ? packet.thread_id : undefined,
58
+ thread_title: typeof packet?.thread_title === "string" ? packet.thread_title : undefined,
59
+ execution_mode: typeof packet?.execution_mode === "string" ? packet.execution_mode : undefined,
60
+ raw_text: raw,
61
+ };
62
+ }
63
+
64
+ export function normalizeDebate(packet, role, run, raw = "") {
65
+ return {
66
+ role,
67
+ symbol: run.symbol,
68
+ as_of: run.as_of,
69
+ verdict: typeof packet?.verdict === "string" ? packet.verdict : "",
70
+ rating: RATINGS.includes(packet?.rating) ? packet.rating : "Hold",
71
+ winner: ["bull", "bear", "balanced", "unknown"].includes(packet?.winner) ? packet.winner : "unknown",
72
+ summary: typeof packet?.summary === "string" ? packet.summary : raw.slice(0, LIMITS.CLEAN_LOG_BYTES),
73
+ long_thesis: Array.isArray(packet?.long_thesis) ? packet.long_thesis : [],
74
+ short_thesis: Array.isArray(packet?.short_thesis) ? packet.short_thesis : [],
75
+ valuation_range: typeof packet?.valuation_range === "string" ? packet.valuation_range : "",
76
+ catalysts: Array.isArray(packet?.catalysts) ? packet.catalysts : [],
77
+ risks: Array.isArray(packet?.risks) ? packet.risks : [],
78
+ position: typeof packet?.position === "string" ? packet.position : "",
79
+ invalidation: Array.isArray(packet?.invalidation) ? packet.invalidation : [],
80
+ source_ids: Array.isArray(packet?.source_ids) ? packet.source_ids : [],
81
+ confidence: ["high", "medium", "low"].includes(packet?.confidence) ? packet.confidence : "low",
82
+ questions: Array.isArray(packet?.questions) ? packet.questions : [],
83
+ questions_answered: Array.isArray(packet?.questions_answered) ? packet.questions_answered : [],
84
+ debate_rounds: Array.isArray(packet?.debate_rounds) ? packet.debate_rounds : [],
85
+ report_markdown: typeof packet?.report_markdown === "string" ? packet.report_markdown : "",
86
+ thread_id: typeof packet?.thread_id === "string" ? packet.thread_id : undefined,
87
+ thread_title: typeof packet?.thread_title === "string" ? packet.thread_title : undefined,
88
+ execution_mode: typeof packet?.execution_mode === "string" ? packet.execution_mode : undefined,
89
+ raw_text: raw,
90
+ };
91
+ }
92
+
93
+ export function dryPacket(task, symbol, asOfDate, prompt, language = "English") {
94
+ const chinese = isChineseLanguage(language);
95
+ return normalizePacket({
96
+ summary: chinese ? `已计划 ${symbol} 的 ${task} 子代理。` : `Planned ${task} subagent for ${symbol}.`,
97
+ claims: [{
98
+ claim: chinese ? "仅 dry run;没有执行外部研究。" : "Dry run only; no external research executed.",
99
+ evidence: chinese ? "生成的 prompt 已保存在 raw_text。" : "The generated prompt is stored in raw_text.",
100
+ confidence: "low",
101
+ source_ids: [],
102
+ }],
103
+ open_questions: [chinese ? "不要传 dry_run,或传 dry_run=false,即可执行 Codex 子代理。" : "Run again without dry_run, or with dry_run=false, to execute Codex subagents."],
104
+ confidence: "low",
105
+ }, task, symbol, asOfDate, prompt);
106
+ }
107
+
108
+ export function dryDebate(role, run, prompt) {
109
+ const chinese = isChineseLanguage(run.language);
110
+ return normalizeDebate({
111
+ verdict: "DRY_RUN",
112
+ rating: "Hold",
113
+ winner: "unknown",
114
+ summary: chinese ? `已计划 ${run.symbol} 的 ${role} 综合。` : `Planned ${role} synthesis for ${run.symbol}.`,
115
+ confidence: "low",
116
+ report_markdown: chinese ? `# ${run.symbol} ${role}\n\n仅 dry run。\n` : `# ${run.symbol} ${role}\n\nDry run only.\n`,
117
+ }, role, run, prompt);
118
+ }
119
+
120
+ export function compactEvidence(run) {
121
+ return {
122
+ run_id: run.run_id,
123
+ symbol: run.symbol,
124
+ as_of: run.as_of,
125
+ packets: run.packets.map((packet) => ({
126
+ task: packet.task,
127
+ summary: packet.summary,
128
+ claims: packet.claims,
129
+ metrics: packet.metrics,
130
+ sources: packet.sources,
131
+ open_questions: packet.open_questions,
132
+ confidence: packet.confidence,
133
+ })),
134
+ };
135
+ }
136
+
137
+ export function debateFromCodex(result, role, run, fallbackPrompt) {
138
+ if (!result.ok) return dryDebate(role, run, cleanLog(result.stderr || result.text || fallbackPrompt));
139
+ try {
140
+ return normalizeDebate(extractJson(result.text), role, run, result.text);
141
+ } catch {
142
+ return normalizeDebate({
143
+ verdict: "PARSE_FAILED",
144
+ rating: "Hold",
145
+ winner: "unknown",
146
+ summary: `${role} returned non-JSON output.`,
147
+ confidence: "low",
148
+ report_markdown: cleanLog(result.text),
149
+ }, role, run, cleanLog(result.text));
150
+ }
151
+ }
152
+
153
+ export function mergeDebateRounds(rounds) {
154
+ const list = (rounds || []).filter(Boolean);
155
+ if (list.length === 0) return null;
156
+ const base = list[list.length - 1];
157
+ const debate_rounds = list.map((packet, index) => ({
158
+ round: index + 1,
159
+ summary: packet.summary || "",
160
+ long_thesis: packet.long_thesis || [],
161
+ short_thesis: packet.short_thesis || [],
162
+ questions: packet.questions || [],
163
+ questions_answered: packet.questions_answered || [],
164
+ raw_text: packet.raw_text || "",
165
+ }));
166
+ return { ...base, debate_rounds };
167
+ }
168
+
169
+ export function confidenceScore(value) {
170
+ return ({ high: 3, medium: 2, low: 1 })[value] || 1;
171
+ }
172
+
173
+ export function summarizeRun(run, userPrompt = "") {
174
+ const claims = run.packets.flatMap((packet) =>
175
+ packet.claims.map((claim) => ({ ...claim, task: packet.task, packet_confidence: packet.confidence }))
176
+ );
177
+ const avg = run.packets.reduce((sum, packet) => sum + confidenceScore(packet.confidence), 0) / Math.max(1, run.packets.length);
178
+ const confidence = avg >= 2.5 ? "high" : avg >= 1.7 ? "medium" : "low";
179
+ return {
180
+ run_id: run.run_id,
181
+ symbol: run.symbol,
182
+ as_of: run.as_of,
183
+ objective: userPrompt,
184
+ final_decision: run.dry_run ? "DRY_RUN" : "NEEDS_MANAGER_REVIEW",
185
+ confidence,
186
+ thesis: claims.slice(0, 12),
187
+ open_questions: [...new Set(run.packets.flatMap((packet) => packet.open_questions || []))],
188
+ source_count: run.packets.reduce((sum, packet) => sum + (packet.sources?.length || 0), 0),
189
+ evidence_path: join(runPath(run.run_id), "evidence.json"),
190
+ };
191
+ }
192
+
193
+ export function managerFallback(run, userPrompt = "") {
194
+ const summary = summarizeRun(run, userPrompt);
195
+ const chinese = isChineseLanguage(run.language);
196
+ const analystLog = run.packets.length
197
+ ? run.packets.map((packet) => {
198
+ const claims = (packet.claims || []).slice(0, 5).map((claim) => ` - ${claim.claim}`).join("\n");
199
+ const gaps = (packet.open_questions || []).slice(0, 3).map((item) => ` - ${item}`).join("\n");
200
+ return `### ${packet.task}\n- Confidence: ${packet.confidence || "unknown"}\n- Summary: ${packet.summary || "None"}\n${claims ? `- Key findings:\n${claims}\n` : ""}${gaps ? `- Data gaps:\n${gaps}\n` : ""}`;
201
+ }).join("\n\n")
202
+ : (chinese ? "未生成 evidence packets。" : "No evidence packets were generated.");
203
+ const debateRecord = chinese
204
+ ? "经理综合子代理未完成,因此没有完整多空交叉辩论记录;以上证据只能作为投资委员会初稿。"
205
+ : "The manager synthesis subagent did not complete, so a full bull/bear cross-debate record is unavailable; the evidence above is only an investment-committee draft.";
206
+ return normalizeDebate({
207
+ verdict: summary.final_decision,
208
+ rating: "Hold",
209
+ winner: "unknown",
210
+ summary: chinese ? "证据已收集,但未运行经理综合子代理。" : "Evidence was collected, but the manager synthesis subagent did not run.",
211
+ long_thesis: summary.thesis.filter((claim) => claim.confidence !== "low").slice(0, 6).map((claim) => claim.claim),
212
+ short_thesis: summary.open_questions.slice(0, 6),
213
+ confidence: summary.confidence,
214
+ report_markdown: chinese
215
+ ? `# ${run.symbol} 投资委员会初稿\n\n## 结论\n${summary.final_decision}\n\n## 分析师工作记录\n${analystLog}\n\n## 多空辩论记录\n${debateRecord}\n\n## 多头观点\n${summary.thesis.filter((claim) => claim.confidence !== "low").slice(0, 6).map((claim) => `- ${claim.claim}`).join("\n") || "- 本轮没有可用多头论点。"}\n\n## 空头观点\n${summary.open_questions.slice(0, 6).map((item) => `- ${item}`).join("\n") || "- 本轮没有可用空头论点。"}\n\n## 市场预期与隐含门槛\n${clip(packetSummary(run, "forward_expectations"), 900) || "- 本轮没有前瞻预期证据。"}\n\n## 分析师评级/目标价变化\n${clip(packetSummary(run, "sell_side_revisions"), 900) || "- 本轮没有卖方修正证据。"}\n\n## 电话会管理层信号\n${clip(packetSummary(run, "earnings_call_transcript"), 900) || "- 本轮没有电话会证据。"}\n\n## 量化/因子视角\n${clip(packetSummary(run, "quant_factor"), 900) || "- 本轮没有量化因子证据。"}\n\n## 新闻和公司/行业人物发言信号\n${clip([packetSummary(run, "news_industry_management"), packetSummary(run, "management_industry_voices")].filter(Boolean).join("\n"), 1200) || "- 本轮没有新闻或人物发言证据。"}\n\n## short interest / borrow / options 信息\n${clip(packetSummary(run, "quant_factor"), 700) || "- 本轮没有 short interest / borrow / options 数据。"}\n\n## 战略交易 / 银行事件\n${clip(packetSummary(run, "ib_event_analysis"), 900) || "- 本轮没有交易事件证据。"}\n\n## 估值区间\n${clip(packetSummary(run, "valuation_long_short"), 900) || "- 本轮没有估值证据。"}\n\n## 价位参考\n- 经理综合未完成,无法给出价格条件表。需要:估值区间、最差年份盈利、历史估值分位。\n\n## 关键催化剂\n- 等待 portfolio_manager 完整综合。\n\n## 主要风险\n${summary.open_questions.slice(0, 6).map((item) => `- ${item}`).join("\n") || "- 暂未发现额外风险。"}\n\n## 仓位建议\n- 经理综合未完成前仅作为初稿,不给正式仓位。\n\n## 短线 1-4 周判断\n- 需等待完整经理综合。\n\n## 中期 3-6 个月判断\n- 需等待完整经理综合。\n\n## 长期 12 个月判断\n- 需等待完整经理综合。\n\n## 数据缺口/未覆盖项\n${summary.open_questions.length ? summary.open_questions.map((item) => `- ${item}`).join("\n") : "- 未发现关键数据缺口。"}\n\n## 反证条件\n- 若证据来源缺失或完整经理综合失败,本初稿不能作为正式结论。\n\n## 置信度\n${summary.confidence}\n\n## 来源表\n- 来源数量: ${summary.source_count}\n`
216
+ : `# ${run.symbol} Investment Committee Draft\n\n## Conclusion\n${summary.final_decision}\n\n## Analyst Work Log\n${analystLog}\n\n## Bull/Bear Debate Record\n${debateRecord}\n\n## Long Thesis\n${summary.thesis.filter((claim) => claim.confidence !== "low").slice(0, 6).map((claim) => `- ${claim.claim}`).join("\n") || "- No usable long thesis yet."}\n\n## Short Thesis\n${summary.open_questions.slice(0, 6).map((item) => `- ${item}`).join("\n") || "- No usable short thesis yet."}\n\n## Market Expectations and Implied Thresholds\n${clip(packetSummary(run, "forward_expectations"), 900) || "- No forward-expectations evidence in this run."}\n\n## Analyst Rating and Target-Price Revisions\n${clip(packetSummary(run, "sell_side_revisions"), 900) || "- No sell-side revision evidence in this run."}\n\n## Earnings Call Management Signals\n${clip(packetSummary(run, "earnings_call_transcript"), 900) || "- No earnings-call evidence in this run."}\n\n## Quant Factor / Technical Risk View\n${clip(packetSummary(run, "quant_factor"), 900) || "- No quant-factor evidence in this run."}\n\n## News and Company / Industry Voice Signals\n${clip([packetSummary(run, "news_industry_management"), packetSummary(run, "management_industry_voices")].filter(Boolean).join("\n"), 1200) || "- No news or voice evidence in this run."}\n\n## Short Interest / Borrow / Options Information\n${clip(packetSummary(run, "quant_factor"), 700) || "- No short interest / borrow / options data in this run."}\n\n## Strategic Transaction or Banking Event\n${clip(packetSummary(run, "ib_event_analysis"), 900) || "- No transaction evidence in this run."}\n\n## Valuation Range\n${clip(packetSummary(run, "valuation_long_short"), 900) || "- No valuation evidence in this run."}\n\n## Price Levels\n- Manager synthesis did not complete, so no price-condition table can be given. Needed: a valuation range, worst-year earnings, and the historical valuation percentile.\n\n## Key Catalysts\n- Wait for completed portfolio-manager synthesis.\n\n## Major Risks\n${summary.open_questions.slice(0, 6).map((item) => `- ${item}`).join("\n") || "- No additional risks surfaced yet."}\n\n## Position Recommendation\n- Draft only; no formal position before completed manager synthesis.\n\n## Short-Term 1-4 Week View\n- Requires completed manager synthesis.\n\n## Medium-Term 3-6 Month View\n- Requires completed manager synthesis.\n\n## Long-Term 12 Month View\n- Requires completed manager synthesis.\n\n## Data Gaps / Unavailable Data\n${summary.open_questions.length ? summary.open_questions.map((item) => `- ${item}`).join("\n") : "- No critical data gaps were found."}\n\n## Invalidation Conditions\n- If evidence sources are missing or manager synthesis fails, this draft cannot stand as the final decision.\n\n## Confidence\n${summary.confidence}\n\n## Source Table\n- Source count: ${summary.source_count}\n`,
217
+ }, "portfolio_manager", run);
218
+ }
219
+
220
+ /**
221
+ * A master's opinion. Deliberately NOT a debate packet: a master issues no rating and
222
+ * declares no winner. out_of_scope is a first-class stance -- "by my method this name is
223
+ * outside what I can judge" is a conclusion, not an abstention.
224
+ */
225
+ export function normalizeMasterOpinion(packet, masterId, run, raw = "") {
226
+ const list = (value) => (Array.isArray(value) ? value.filter((x) => typeof x === "string") : []);
227
+ return {
228
+ master: masterId,
229
+ symbol: run.symbol,
230
+ as_of: run.as_of,
231
+ verdict: typeof packet?.verdict === "string" ? packet.verdict : "",
232
+ stance: MASTER_STANCES.includes(packet?.stance) ? packet.stance : "cautious",
233
+ summary: typeof packet?.summary === "string" ? packet.summary : raw.slice(0, LIMITS.CLEAN_LOG_BYTES),
234
+ key_findings: list(packet?.key_findings),
235
+ disagreements: list(packet?.disagreements),
236
+ disqualifiers_triggered: list(packet?.disqualifiers_triggered),
237
+ what_would_change_my_mind: list(packet?.what_would_change_my_mind),
238
+ source_ids: list(packet?.source_ids),
239
+ confidence: ["high", "medium", "low"].includes(packet?.confidence) ? packet.confidence : "low",
240
+ thread_id: typeof packet?.thread_id === "string" ? packet.thread_id : undefined,
241
+ raw_text: raw,
242
+ };
243
+ }
244
+
245
+ /** Compact master opinions for injection into the debate prompt. */
246
+ export function compactMasterOpinions(run) {
247
+ return (run.master_opinions || []).map((opinion) => ({
248
+ master: opinion.master,
249
+ stance: opinion.stance,
250
+ verdict: opinion.verdict,
251
+ key_findings: opinion.key_findings,
252
+ disagreements: opinion.disagreements,
253
+ disqualifiers_triggered: opinion.disqualifiers_triggered,
254
+ confidence: opinion.confidence,
255
+ }));
256
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Persona file format: a `---json` fenced frontmatter block, then a body split into
3
+ * language sections by `<!-- lang:xx -->` markers.
4
+ *
5
+ * JSON rather than YAML because this repo has no dependencies and a hand-rolled YAML
6
+ * subset parser mis-parses things like `description: Buy: or Sell` silently, producing a
7
+ * wrong prompt with no error. JSON.parse is exact and fails loudly with a position.
8
+ * Persona files are source data; the generator emits real YAML frontmatter for the host
9
+ * surfaces, and serializing a known key set is safe in a way parsing arbitrary YAML is not.
10
+ *
11
+ * `<!-- lang:xx -->` rather than `## zh` because persona bodies legitimately contain `##`
12
+ * headings, an HTML comment can never collide, and GitHub renders it as nothing.
13
+ */
14
+
15
+ const FRONTMATTER = /^---json\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/;
16
+ const LANG_MARKER = /^<!--\s*lang:\s*([a-z]{2}(?:-[a-z]{2})?)\s*-->\s*$/i;
17
+
18
+ export function parsePersonaFile(text, file = "<persona>") {
19
+ const match = FRONTMATTER.exec(String(text ?? ""));
20
+ if (!match) throw new Error(`${file}: missing ---json frontmatter block`);
21
+
22
+ let meta;
23
+ try {
24
+ meta = JSON.parse(match[1]);
25
+ } catch (error) {
26
+ throw new Error(`${file}: frontmatter is not valid JSON: ${error.message}`);
27
+ }
28
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
29
+ throw new Error(`${file}: frontmatter must be a JSON object`);
30
+ }
31
+
32
+ const bodies = {};
33
+ let current = null;
34
+ let sawMarker = false;
35
+ const preamble = [];
36
+ for (const line of String(text).slice(match[0].length).split(/\r?\n/)) {
37
+ const marker = LANG_MARKER.exec(line);
38
+ if (marker) {
39
+ sawMarker = true;
40
+ current = marker[1].toLowerCase();
41
+ if (!bodies[current]) bodies[current] = [];
42
+ continue;
43
+ }
44
+ if (current) bodies[current].push(line);
45
+ else preamble.push(line);
46
+ }
47
+ if (!sawMarker) throw new Error(`${file}: body has no <!-- lang:xx --> section`);
48
+ if (preamble.join("").trim()) {
49
+ throw new Error(`${file}: text before the first <!-- lang:xx --> marker would be silently dropped`);
50
+ }
51
+
52
+ return {
53
+ meta,
54
+ bodies: Object.fromEntries(Object.entries(bodies).map(([lang, lines]) => [lang, lines.join("\n").trim()])),
55
+ };
56
+ }
57
+
58
+ /** Serialize a fixed, known key set to YAML frontmatter for generated host files. */
59
+ export function toYamlFrontmatter(fields) {
60
+ const lines = ["---"];
61
+ for (const [key, value] of Object.entries(fields)) {
62
+ if (value === undefined || value === null || value === "") continue;
63
+ if (Array.isArray(value)) {
64
+ if (value.length === 0) continue;
65
+ lines.push(`${key}: ${value.map(yamlScalar).join(", ")}`);
66
+ continue;
67
+ }
68
+ lines.push(`${key}: ${yamlScalar(value)}`);
69
+ }
70
+ lines.push("---");
71
+ return lines.join("\n");
72
+ }
73
+
74
+ function yamlScalar(value) {
75
+ const text = String(value);
76
+ // Quote anything a YAML reader could misinterpret. Cheap and always safe.
77
+ return /^[A-Za-z0-9][A-Za-z0-9 ._/-]*$/.test(text) && !/:\s/.test(text)
78
+ ? text
79
+ : JSON.stringify(text);
80
+ }