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,737 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import readline from "node:readline";
4
+ import { LIMITS, OUTPUT_MODES, SERVER_NAME, VERSION } from "./constants.mjs";
5
+ import { RpcCode, methodNotFound, invalidParams, toRpcError } from "./errors.mjs";
6
+ import { readJson, readJsonl } from "./fsutil.mjs";
7
+ import { resolveLanguage } from "./lang.mjs";
8
+ import { sweepStaleOutputs } from "./codex.mjs";
9
+ import { sourceManifest } from "./gates.mjs";
10
+ import { artifactPaths, runPath, saveRun } from "./run-store.mjs";
11
+ import { summaryModes } from "./output-modes.mjs";
12
+ import { registry } from "./personas/registry.mjs";
13
+ import { preflightNetworkPermissions } from "./preflight.mjs";
14
+ import { getQuotes } from "./quotes.mjs";
15
+ import { MACRO_BLOCKS, getMacroSnapshot } from "./macro.mjs";
16
+ import { fetchOptionsChain } from "./options.mjs";
17
+ import { getMarketNarrative } from "./narrative.mjs";
18
+ import { getSocialPulse, verifyXPost } from "./social.mjs";
19
+ import { councilOptions } from "./council-options.mjs";
20
+ import { fetchFeeds, tickerNewsFeed, queryNewsFeed, filingsFeed } from "./feeds.mjs";
21
+ import { screenTicker, explainResult, screenBatch } from "./screen.mjs";
22
+ import { gatherGrounding, groundingBlock } from "./grounding.mjs";
23
+ import { fetchMarketFinancials, coverageFor, MARKETS } from "./markets.mjs";
24
+ import { table, mark, metricValue, groundingDashboard, label, threshold, skippedMark } from "./tables.mjs";
25
+ import { fetchUniverse } from "./sec.mjs";
26
+ import { industryBrief, listIndustries, industryCoverage, peersBySic, SIC_GROUPS } from "./industry.mjs";
27
+ import { analyzeSymbol, collectEvidence, recordMasterOpinion, recordVerifierVerdict, recordVisibleDecision, recordVisiblePacket, visibleAgentSpecs, visibleRun } from "./orchestrator.mjs";
28
+
29
+ export function send(message) {
30
+ process.stdout.write(`${JSON.stringify(message)}\n`);
31
+ }
32
+
33
+ export function sendResult(id, result) {
34
+ send({ jsonrpc: "2.0", id, result });
35
+ }
36
+
37
+ export function sendError(id, code, message, data) {
38
+ send({ jsonrpc: "2.0", id, error: { code, message, ...(data === undefined ? {} : { data }) } });
39
+ }
40
+
41
+ export function jsonContent(text, structuredContent = {}) {
42
+ return {
43
+ content: [{ type: "text", text }],
44
+ structuredContent,
45
+ };
46
+ }
47
+
48
+ export function tool(name, description, inputSchema, annotations = {}) {
49
+ return { name, description, inputSchema, annotations };
50
+ }
51
+
52
+ export function tools() {
53
+ // Derived from personas/, not from a frozen list: adding a persona file makes the role
54
+ // selectable through the tool schema with no code change.
55
+ const analystIds = registry().ids("analyst");
56
+ const debateIds = registry().ids("debate");
57
+ const masterIds = registry().ids("master");
58
+ const masterRosters = [...new Set(registry().all().filter((p) => p.kind === "master" && p.enabled).flatMap((p) => p.rosters))].sort();
59
+ const common = {
60
+ symbol: { type: "string", description: "Exchange ticker. US, HK, JP, KR, CN and TW symbols all work, e.g. AAPL, 0700.HK, 7203.T, 005930.KS, 600519.SS." },
61
+ as_of: { type: "string", description: "Analysis date YYYY-MM-DD. Defaults to today." },
62
+ prompt: { type: "string", description: "User objective or extra instructions." },
63
+ language: { type: "string", default: "auto", description: "Reader-facing language for subagents and final report, e.g. auto, zh-CN, en-US, ja-JP. Auto infers from prompt." },
64
+ tasks: { type: "array", items: { type: "string", enum: analystIds } },
65
+ dry_run: { type: "boolean", default: false, description: "Default false. Set true only for planning/self-tests without launching Codex subagents." },
66
+ max_concurrency: { type: "number", default: LIMITS.CONCURRENCY_DEFAULT },
67
+ timeout_ms: { type: "number", default: LIMITS.CODEX_TIMEOUT_MS },
68
+ synthesis: { type: "boolean", default: true, description: "Run bull, bear, and portfolio-manager synthesis after evidence collection." },
69
+ synthesis_timeout_ms: { type: "number", default: LIMITS.CODEX_TIMEOUT_MS },
70
+ output_mode: { type: "string", enum: OUTPUT_MODES, default: "public_equity", description: "Final synthesis target shape." },
71
+ masters_roster: { type: "string", enum: masterRosters, description: "Master bench; masters-core is the recommended default with twelve seats spanning value, classic, adversarial and quant schools. Masters read the finished evidence through one philosophy each and run BETWEEN the evidence stage and the debate; their disagreements become inputs the bull and bear must answer. They never gather evidence and are not part of the completeness gate." },
72
+ seat_weights: { type: "object", description: "Override the declared weight of any seat, e.g. {\"master_buffett\": 2, \"master_soros\": 0}. Weights are an editable prior, not an optimum: a return backtest of LLM judgment would be invalidated by look-ahead bias." },
73
+ masters: { type: "array", items: { type: "string", enum: masterIds }, description: "Explicit master personas, overriding masters_roster." },
74
+ visibility_required: { type: "boolean", default: false, description: "When true, headless MCP execution is rejected; use host-visible agents/threads and record their outputs." },
75
+ };
76
+ return [
77
+ tool("plan_visible_run", "MANDATORY first step (not optional): create the visible-host-thread AlphaCouncil Agent run envelope and prompts. Does NOT execute. You MUST then run every planned evidence agent and record each via record_visible_packet, then record bull_researcher and bear_researcher, before recording the portfolio_manager decision.", {
78
+ type: "object",
79
+ properties: {
80
+ symbol: common.symbol,
81
+ as_of: common.as_of,
82
+ prompt: common.prompt,
83
+ language: common.language,
84
+ tasks: common.tasks,
85
+ masters_roster: common.masters_roster,
86
+ masters: common.masters,
87
+ seat_weights: common.seat_weights,
88
+ grounding: { type: "object", description: "The `grounding` object from compose_research_brief. Injected into every analyst prompt." },
89
+ run_id: { type: "string" },
90
+ },
91
+ required: ["symbol"],
92
+ }),
93
+ tool("record_visible_packet", "MANDATORY sequential step (not optional): record one completed visible evidence agent packet into a planned visible run. Every planned evidence task MUST be recorded before the portfolio_manager decision; a run missing any planned packet will be marked incomplete.", {
94
+ type: "object",
95
+ properties: {
96
+ run_id: { type: "string" },
97
+ task: { type: "string", enum: analystIds },
98
+ packet: { type: "object" },
99
+ thread_id: { type: "string" },
100
+ thread_title: { type: "string" },
101
+ },
102
+ required: ["run_id", "task", "packet"],
103
+ }),
104
+ tool("record_visible_decision", "Record one completed visible bull_researcher / bear_researcher / portfolio_manager packet. Record bull_researcher and bear_researcher before portfolio_manager. For role=portfolio_manager, ALL planned evidence packets AND both debate researchers (bull_researcher and bear_researcher) MUST already be recorded; otherwise the run is marked status=incomplete (NOT complete) and final_report.md gets a visible INCOMPLETE banner. This is the LAST step.", {
105
+ type: "object",
106
+ properties: {
107
+ run_id: { type: "string" },
108
+ role: { type: "string", enum: debateIds },
109
+ packet: { type: "object" },
110
+ thread_id: { type: "string" },
111
+ thread_title: { type: "string" },
112
+ },
113
+ required: ["run_id", "role", "packet"],
114
+ }),
115
+ tool("collect_evidence", "Launch Codex subagents and save shared JSON evidence packets. Use dry_run=true only for planning/self-tests.", {
116
+ type: "object",
117
+ properties: common,
118
+ required: ["symbol"],
119
+ }),
120
+ tool("analyze_symbol", "Collect evidence and write a manager-style decision summary.", {
121
+ type: "object",
122
+ properties: common,
123
+ required: ["symbol"],
124
+ }),
125
+ tool("read_run", "Read a saved AlphaCouncil Agent run from the shared evidence store.", {
126
+ type: "object",
127
+ properties: { run_id: { type: "string" } },
128
+ required: ["run_id"],
129
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
130
+ tool("compare_summary_modes", "Compare chat, PDF, presentation, document, and specialist plugin modes for final AlphaCouncil Agent synthesis.", {
131
+ type: "object",
132
+ properties: { language: common.language },
133
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
134
+ tool("record_master_opinion", "Record one completed master-seat opinion into a planned visible run. Masters run AFTER every evidence packet is recorded and BEFORE the bull/bear debate, so the debate has their disagreements to argue with. A master may return stance=out_of_scope, which is a conclusion rather than an abstention. Masters are optional and never affect the completeness gate.", {
135
+ type: "object",
136
+ properties: {
137
+ run_id: { type: "string" },
138
+ master: { type: "string", enum: masterIds },
139
+ packet: { type: "object" },
140
+ thread_id: { type: "string" },
141
+ },
142
+ required: ["run_id", "master", "packet"],
143
+ }),
144
+ tool("get_macro_snapshot", "Keyless DELAYED top-down macro context in one call: rate curve, dollar and credit, commodities, risk appetite and breadth, and cross-market indices, plus derived pairs (10Y-3M spread, copper/gold, HY/IG, equal-weight vs cap-weight). Use it to place a single name inside its macro environment. These are observations, not a regime call, and unavailable series are data gaps for open_questions.", {
145
+ type: "object",
146
+ properties: {
147
+ blocks: {
148
+ type: "array",
149
+ items: { type: "string", enum: MACRO_BLOCKS.map((b) => b.id) },
150
+ description: "Subset of macro blocks. Defaults to all.",
151
+ },
152
+ },
153
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
154
+ tool("list_council_options", "The menu to show the user BEFORE a run starts: three presets (quick / standard / deep) with their seat counts and relative cost, every analyst seat, every master roster with its members, and the verifiers. A council can be four seats or thirty-eight, and that difference is the user's time and money -- so ask which they want rather than choosing for them. Skip the question only when they have already said (named a roster, said 'everything', said 'be quick').", {
155
+ type: "object",
156
+ properties: { language: { type: "string", description: "Language for the labels. Defaults to English." } },
157
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
158
+ tool("get_options_chain", "Keyless DELAYED options chain digest from CBOE for one US-listed symbol: ATM implied-volatility term structure, 25-delta skew, put/call ratios on open interest and volume, the strikes holding the most open interest, and the ATM bid-ask spread as a share of mid. Contracts reporting iv = 0 (expired or deep in the money) are excluded rather than read as zero volatility. This is a snapshot with no history, so IV percentile or rank CANNOT be computed from it and must stay an open question. Non-US listings are generally absent and are reported as unavailable, never guessed.", {
159
+ type: "object",
160
+ properties: {
161
+ symbol: { type: "string", description: "US-listed underlying, e.g. MU or BRK.B." },
162
+ as_of: { type: "string", description: "ISO date used to compute days-to-expiry. Defaults to today." },
163
+ },
164
+ required: ["symbol"],
165
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
166
+ tool("get_market_narrative", "What story the market is currently telling itself, read from keyless news feeds (Federal Reserve, SEC, WSJ, CNBC, Yahoo) and cross-checked against the macro tape. Returns ranked themes with their share of coverage, dated and linked sample headlines, and for each theme the actual market series that would corroborate it. Headline counts measure ATTENTION, never truth: where a theme leads coverage and its series has not moved, that divergence is the finding. Every item must carry a timestamp inside the window or it is reported as excluded. Themes come from a fixed lexicon, so a genuinely new narrative lands in unclassified_headlines rather than being discovered, and the output says so.", {
167
+ type: "object",
168
+ properties: {
169
+ days: { type: "number", description: "Recency window in days. Defaults to 7." },
170
+ as_of: { type: "string", description: "ISO date treated as now. Defaults to today." },
171
+ extra_queries: {
172
+ type: "array", items: { type: "string" },
173
+ description: "Up to 4 extra Google News queries to fold in, e.g. a sector or a country.",
174
+ },
175
+ top: { type: "number", description: "How many themes to return. Defaults to 6." },
176
+ },
177
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
178
+ tool("get_news", "Dated headlines for one symbol, one search query, or one company's SEC filings, from keyless feeds (Yahoo Finance RSS, Google News RSS, EDGAR Atom). Every item must carry a parsable timestamp inside the window; undated and out-of-window items are counted and sampled under excluded_outside_window rather than being shown as recent. Filings are the one source here that cannot be spun, so prefer them for anything material.", {
179
+ type: "object",
180
+ properties: {
181
+ symbol: { type: "string", description: "Ticker for the Yahoo per-name feed." },
182
+ query: { type: "string", description: "Free-text Google News query, e.g. 'HBM supply Samsung'." },
183
+ cik: { type: "string", description: "SEC CIK for the filings feed." },
184
+ forms: { type: "string", description: "Filing type for the CIK feed. Defaults to 8-K." },
185
+ days: { type: "number", description: "Recency window in days. Defaults to 14." },
186
+ as_of: { type: "string", description: "ISO date treated as now. Defaults to today." },
187
+ },
188
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
189
+ tool("get_social_pulse", "Keyless retail and technical-community discussion for a name or theme, from Reddit (searched inside the equity subreddits, not site-wide), Hacker News, and any Bluesky handles supplied. IMPORTANT: X / Twitter has NO free discovery channel -- Nitter search is dead, the X API bills per post and xAI bills per call -- so this does NOT cover professional FinTwit, and Reddit is not a substitute for it. Mention volume measures attention, never correctness. Nothing here may enter a conclusion alone; it is a lead to be confirmed against a filing or recorded in open_questions.", {
190
+ type: "object",
191
+ properties: {
192
+ query: { type: "string", description: "Company name or theme to search for." },
193
+ symbol: { type: "string", description: "Used as the query when query is absent." },
194
+ subreddits: { type: "array", items: { type: "string" }, description: "Override the default equity subreddits." },
195
+ handles: { type: "array", items: { type: "string" }, description: "Bluesky handles to read. Search needs auth; reading a named account does not." },
196
+ days: { type: "number", description: "Recency window in days. Defaults to 7." },
197
+ as_of: { type: "string", description: "ISO date treated as now." },
198
+ },
199
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
200
+ tool("verify_x_post", "Confirm that one X post id exists and read back its text, author and date. Use it whenever a report or search result quotes a post: a decoded snowflake timestamp proves nothing, because any invented 19-digit id decodes to a plausible date, so existence has to be checked separately. This is verification only and cannot search or discover posts.", {
201
+ type: "object",
202
+ properties: { id: { type: "string", description: "Numeric X post id." } },
203
+ required: ["id"],
204
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
205
+ tool("record_verifier_verdict", "Record one Stage 2b verifier outcome against the seat that cited the claim. Verdicts that failed verification (contradicted, disagree, refuted) automatically reduce that seat's weight in the portfolio-manager synthesis; cannot_confirm and source_unreachable reduce it less. A seat is down-weighted, never silently erased.", {
206
+ type: "object",
207
+ properties: {
208
+ run_id: { type: "string" },
209
+ verifier: { type: "string", enum: registry().ids("verifier") },
210
+ seat: { type: "string", description: "The seat whose claim was checked, e.g. bull_researcher or master_buffett." },
211
+ verdict: { type: "string", description: "Must be one of the verifier's declared verdict_values." },
212
+ claim: { type: "string" },
213
+ note: { type: "string" },
214
+ },
215
+ required: ["run_id", "verifier", "seat", "verdict"],
216
+ }),
217
+ tool("industry_brief", "Start from an industry rather than a ticker. Returns the participant list by position in the value chain -- INCLUDING the non-US names a SEC-only pipeline would silently drop, such as Korean and Japanese makers -- plus who actually drives demand, the questions a run must answer, how the industry behaves through a cycle, and which participants this pipeline can screen mechanically versus which need their own regulator's feed. Returns a frame, never a verdict.", {
218
+ type: "object",
219
+ properties: {
220
+ industry: { type: "string", description: "Free text; ids and aliases in both languages, e.g. memory, 存储, HBM, DRAM." },
221
+ },
222
+ required: ["industry"],
223
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
224
+ tool("market_financials", "Structured financials for ANY market, degrading in a stated order: keyless regulator feed (SEC for US, TWSE for Taiwan), then a feed needing a free key (DART for Korea, EDINET for Japan) reported as not-configured rather than pretended away, then quotes plus search which always work. Never returns an empty result silently -- it says which feed is missing, why, and what to use instead.", {
225
+ type: "object",
226
+ properties: {
227
+ symbol: { type: "string", description: "Exchange symbol, e.g. 2408.TW, 000660.KS, 285A.T, 0700.HK." },
228
+ corp_code: { type: "string", description: "Korea only: DART's 8-digit corp_code, which is not the ticker. Samsung Electronics is 00126380, SK hynix 00164779." },
229
+ year: { type: "number", description: "Korea only: fiscal year. Defaults to last year." },
230
+ },
231
+ required: ["symbol"],
232
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
233
+ tool("market_coverage", "Ask up front what this pipeline can and cannot fetch for a set of symbols, before building a report on them. Returns per-symbol whether structured financials are available, summary-only, or absent, and which environment variable would unlock a market. Names without a feed are still researchable from documents -- but every figure taken that way has to be labelled as such.", {
234
+ type: "object",
235
+ properties: { symbols: { type: "array", items: { type: "string" } } },
236
+ required: ["symbols"],
237
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
238
+ tool("compose_research_brief", "Assemble the hard facts BEFORE the analysts search: quote, SEC filer profile, the mechanical screen with every computed metric, macro readings, and the industry chain map. Returns both the structured data and a prompt block that tells an analyst these numbers are already established and that its search exists to explain, extend and challenge them -- with the rule that a searched number never silently overwrites a filed one. Pass the returned `grounding` object to plan_visible_run so every analyst prompt carries it.", {
239
+ type: "object",
240
+ properties: {
241
+ symbol: common.symbol,
242
+ cik: { type: "string", description: "SEC CIK; enables the filer profile and the mechanical screen." },
243
+ industry: { type: "string", description: "Industry query; adds the value-chain participants including non-US names." },
244
+ macro: { type: "boolean", default: true },
245
+ as_of: { type: "string", description: "YYYY-MM-DD; only filings filed by this date are used." },
246
+ language: common.language,
247
+ },
248
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
249
+ tool("industry_coverage", "Ask what is actually known about an industry BEFORE researching it. Returns whether a curated value-chain map exists, whether SEC's SIC classification covers it, or neither -- with guidance for each case. SIC reaches every industry with a US filer; a curated map additionally carries chain position, non-US participants and demand drivers. Use this so a report never presents an uncurated participant list as if it were complete.", {
250
+ type: "object",
251
+ properties: { industry: { type: "string" } },
252
+ required: ["industry"],
253
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
254
+ tool("industry_peers", "Find US filers related to a company using SEC's own SIC classification -- no curation and no model. Covers every industry with a US filer, including ones no curated map reaches. It gives no value-chain position and no non-US participants: for those prefer industry_brief where a map exists. Peer matching is by company name and is a starting universe, not an index membership list.", {
255
+ type: "object",
256
+ properties: {
257
+ cik: { type: "string", description: "Anchor company CIK. Resolve one with list_us_universe." },
258
+ limit: { type: "number", default: 25 },
259
+ },
260
+ required: ["cik"],
261
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
262
+ tool("list_industries", "The industries that have a hand-maintained map. Deliberately a short list: an unmapped industry should be handled by research rather than by inventing a participant list.", {
263
+ type: "object",
264
+ properties: {},
265
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
266
+ tool("screen_ticker", "Run the mechanical elimination screen against a company's own SEC filings. No language model is involved: seven hard rules (10y ROE, 5y cumulative FCF, interest cover, gross margin, OCF/NI, net margin, dilution) with three exemptions, and every rejection names the metric, the measured value and the threshold. Rules whose inputs are missing from the filings are reported as skipped, never as passes. Surviving is not a recommendation -- it means the name is worth research time. US filers only; other markets need their own regulator feed.", {
267
+ type: "object",
268
+ properties: {
269
+ cik: { type: "string", description: "SEC CIK. Use list_us_universe to resolve a ticker." },
270
+ ticker: { type: "string", description: "US ticker, e.g. MU. Resolved to a CIK against the SEC universe, so supplying this is enough." },
271
+ as_of: { type: "string", description: "YYYY-MM-DD. Only filings actually filed by this date are used, which is what keeps a historical screen free of look-ahead bias." },
272
+ },
273
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
274
+ tool("screen_candidates", "Run the mechanical elimination screen over a list of candidates and report every rejection with the metric, the measured value and the threshold. This is the 'find me stocks' path, and no language model participates in it. Capped at 40 names: SEC is one request per company and rate-limits, so narrow the funnel first with industry_brief, industry_peers or list_us_universe. A fetch failure is reported as unavailable rather than eliminated, because dropping a name because SEC timed out would bias the survivors.", {
275
+ type: "object",
276
+ properties: {
277
+ candidates: {
278
+ type: "array",
279
+ items: {
280
+ type: "object",
281
+ properties: { cik: { type: "string" }, ticker: { type: "string" } },
282
+ required: ["cik"],
283
+ },
284
+ description: "Up to 40 companies.",
285
+ },
286
+ as_of: { type: "string", description: "YYYY-MM-DD. Only filings filed by this date are used." },
287
+ },
288
+ required: ["candidates"],
289
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
290
+ tool("list_us_universe", "The full SEC list of US listed companies (~10k) as {cik, ticker, title}. Keyless. Use it to resolve a ticker to a CIK, or as the starting universe for a screen.", {
291
+ type: "object",
292
+ properties: {
293
+ contains: { type: "string", description: "Case-insensitive filter on ticker or company name." },
294
+ limit: { type: "number", default: 50 },
295
+ },
296
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
297
+ tool("preflight_permissions", "MANDATORY before a visible run: check that the host actually grants the network tools the evidence agents need. Background subagents cannot raise an interactive permission prompt, so a missing allowlist entry blocks their searches SILENTLY and they answer from training knowledge while still filling in every report section. Returns status ok | blocked | unknown with a remedy.", {
298
+ type: "object",
299
+ properties: {
300
+ roster: { type: "string", default: "default", description: "Which analyst roster the run will use." },
301
+ },
302
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
303
+ tool("get_quote", "Keyless DELAYED market data (Yahoo/Stooq, ~15m or EOD) for indices, index futures (incl. night session), FX, rates, vol, commodities, and stocks. Accepts plain names ('KOSPI','纳指期货','VIX','美元指数','10年美债','黄金') or raw tickers (^KS11, ES=F, 7203.T). Use for real index/futures/macro numbers; on error treat as a data gap (open_questions). Not real-time, not investment advice.", {
304
+ type: "object",
305
+ properties: {
306
+ symbols: { type: "array", items: { type: "string" }, description: "Names or tickers, e.g. ['KOSPI','ES=F','VIX','美元指数']." },
307
+ symbol: { type: "string", description: "Single name/ticker (alternative to symbols[])." },
308
+ },
309
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }),
310
+ ];
311
+ }
312
+
313
+ export async function handleToolCall(id, params) {
314
+ const name = params?.name;
315
+ const args = params?.arguments || {};
316
+ if (name === "plan_visible_run") {
317
+ const run = visibleRun(args);
318
+ // Gather the established facts here rather than accepting them only when a host
319
+ // remembers to pass them. Without this the whole visible path -- which is the path
320
+ // Claude Code uses -- runs every analyst and every master with no filings, no quote and
321
+ // no macro, and nothing in the output says so. Same reasoning as the preflight below:
322
+ // the host that skips the optional step is exactly the host whose seats fail quietly.
323
+ if (!run.grounding) {
324
+ run.grounding = await gatherGrounding({ symbol: run.symbol, asOf: run.as_of })
325
+ .catch((error) => ({ error: String(error?.message || error), facts_unavailable: true }));
326
+ saveRun(run);
327
+ }
328
+ const specs = visibleAgentSpecs(run, args.prompt || "");
329
+ // Returned with the plan, not left to a separate opt-in call: a host that skips the
330
+ // preflight is exactly the host whose subagents will fail silently.
331
+ const preflight = preflightNetworkPermissions({ roster: args.roster || "default" });
332
+ sendResult(id, jsonContent(
333
+ `Planned visible AlphaCouncil Agent run for ${run.symbol}: ${run.run_id}. `
334
+ + `Network preflight: ${preflight.status}. `
335
+ + `Established facts: ${run.grounding && !run.grounding.facts_unavailable ? "attached to every seat" : "UNAVAILABLE -- seats will run without filings or quotes, say so in the report"}.`,
336
+ {
337
+ run,
338
+ preflight,
339
+ ...specs,
340
+ artifacts: {
341
+ all_agents_md: join(runPath(run.run_id), "all_agents.md"),
342
+ status_json: join(runPath(run.run_id), "status.json"),
343
+ events_jsonl: join(runPath(run.run_id), "events.jsonl"),
344
+ },
345
+ }));
346
+ return;
347
+ }
348
+ if (name === "record_visible_packet") {
349
+ const run = recordVisiblePacket(args);
350
+ sendResult(id, jsonContent(`Recorded visible evidence packet ${args.task} for ${run.symbol}: ${run.run_id}`, run));
351
+ return;
352
+ }
353
+ if (name === "record_visible_decision") {
354
+ const result = recordVisibleDecision(args);
355
+ sendResult(id, jsonContent(`Recorded visible decision ${args.role} for ${result.run.symbol}: ${result.run.run_id}`, result));
356
+ return;
357
+ }
358
+ if (name === "record_master_opinion") {
359
+ const result = recordMasterOpinion(args);
360
+ sendResult(id, jsonContent(
361
+ `Recorded master opinion ${args.master} (${result.opinion.stance}) for ${result.run.symbol}: ${result.recorded}/${result.expected} master seats in.`,
362
+ result,
363
+ ));
364
+ return;
365
+ }
366
+ if (name === "record_verifier_verdict") {
367
+ const result = recordVerifierVerdict(args);
368
+ const seat = result.weights.seats.find((s) => s.seat === args.seat);
369
+ sendResult(id, jsonContent(
370
+ `Recorded ${args.verifier} -> ${args.verdict} for ${args.seat}. Effective weight now ${seat ? seat.effective_weight : "n/a"}.`,
371
+ result,
372
+ ));
373
+ return;
374
+ }
375
+ if (name === "industry_brief") {
376
+ const brief = industryBrief(args.industry);
377
+ const lines = [
378
+ `${brief.title.en} / ${brief.title.zh}: ${brief.participants.length} mapped participants across ${brief.layers.length} chain layers.`,
379
+ `Screenable from SEC filings: ${brief.coverage.sec_screenable.join(", ") || "none"}.`,
380
+ `Need a local regulator feed: ${brief.coverage.needs_local_regulator_feed.map((p) => `${p.symbol} (${p.market})`).join(", ") || "none"}.`,
381
+ ];
382
+ sendResult(id, jsonContent(lines.join("\n"), brief));
383
+ return;
384
+ }
385
+ if (name === "market_financials") {
386
+ const result = await fetchMarketFinancials(args.symbol, { corp_code: args.corp_code, year: args.year });
387
+ let text;
388
+ if (result.financials) {
389
+ const f = result.financials;
390
+ const rows = [
391
+ ["Revenue", metricValue(f.revenue, f.currency)],
392
+ ["Gross profit", metricValue(f.gross_profit, f.currency)],
393
+ ["Operating income", metricValue(f.operating_income, f.currency)],
394
+ ["Net income", metricValue(f.net_income, f.currency)],
395
+ ["EPS", String(f.eps ?? "n/a")],
396
+ ];
397
+ text = [
398
+ table(["Line", `Value (${f.unit})`], rows,
399
+ { title: `${f.company_name} ${f.gregorian_year ?? ""}Q${f.period.quarter} -- ${f.source}` }),
400
+ `\n${f.note}`,
401
+ ].join("\n");
402
+ } else {
403
+ text = [
404
+ `${args.symbol} (${result.market}, ${result.regulator}): no structured feed.`,
405
+ result.guidance,
406
+ result.fallback?.quote ? `Quote: ${result.fallback.quote.price}. ${result.fallback.caveat}` : "",
407
+ ].filter(Boolean).join("\n");
408
+ }
409
+ sendResult(id, jsonContent(text, result));
410
+ return;
411
+ }
412
+ if (name === "market_coverage") {
413
+ const coverage = coverageFor(args.symbols || []);
414
+ const rows = coverage.rows.map((r) => [r.symbol, r.market, r.regulator, r.structured_financials, r.needs_env || r.reason?.slice(0, 50) || "-"]);
415
+ const text = [
416
+ table(["Symbol", "Market", "Regulator", "Structured financials", "Blocker"], rows, { title: "Data coverage" }),
417
+ `\n${coverage.note}`,
418
+ ].join("\n");
419
+ sendResult(id, jsonContent(text, coverage));
420
+ return;
421
+ }
422
+ if (name === "compose_research_brief") {
423
+ const grounding = await gatherGrounding({
424
+ symbol: args.symbol, cik: args.cik, industry: args.industry,
425
+ macro: args.macro !== false, asOf: args.as_of,
426
+ });
427
+ const block = groundingBlock(grounding, resolveLanguage(args));
428
+ const facts = [
429
+ grounding.quote ? "quote" : null,
430
+ grounding.filer ? "filer profile" : null,
431
+ grounding.screen ? `screen (${grounding.screen.rules_computed}/${grounding.screen.rules_total} rules)` : null,
432
+ grounding.macro ? `macro (${grounding.macro.derived.length} readings)` : null,
433
+ grounding.industry?.participants ? `industry (${grounding.industry.participants.length} participants)` : null,
434
+ ].filter(Boolean);
435
+ sendResult(id, jsonContent(
436
+ groundingDashboard(grounding, resolveLanguage(args)),
437
+ { grounding, prompt_block: block, dashboard: groundingDashboard(grounding, resolveLanguage(args)) },
438
+ ));
439
+ return;
440
+ }
441
+ if (name === "industry_coverage") {
442
+ const coverage = industryCoverage(args.industry);
443
+ sendResult(id, jsonContent(
444
+ `"${args.industry}": curated map ${coverage.curated ? `yes (${coverage.curated.id})` : "no"}, SIC group ${coverage.sic_group ? `yes (${coverage.sic_group.id})` : "no"}. ${coverage.guidance}`,
445
+ coverage,
446
+ ));
447
+ return;
448
+ }
449
+ if (name === "industry_peers") {
450
+ const result = await peersBySic(args);
451
+ sendResult(id, jsonContent(
452
+ `${result.anchor.name}: SIC ${result.sic ?? "unknown"} (${result.sic_description ?? "-"})${result.group ? `, group ${result.group.id}` : ""}. ${result.peers.length} name-matched peers.`,
453
+ result,
454
+ ));
455
+ return;
456
+ }
457
+ if (name === "list_industries") {
458
+ const industries = listIndustries();
459
+ sendResult(id, jsonContent(
460
+ `${industries.length} curated industry map(s): ${industries.map((i) => i.id).join(", ")}. `
461
+ + `Plus ${SIC_GROUPS.length} SIC groups covering every US filer -- use industry_coverage to see which applies.`,
462
+ { curated: industries, sic_groups: SIC_GROUPS },
463
+ ));
464
+ return;
465
+ }
466
+ if (name === "screen_ticker") {
467
+ const result = await screenTicker(args);
468
+ const zh = /中文|chinese|zh/i.test(String(args.language || ""));
469
+ const rows = result.rules.map((r) => (r.skipped
470
+ ? [label(r.label, zh), zh ? "无法计算" : "not computable", "-", skippedMark(zh)]
471
+ : [label(r.label, zh), metricValue(r.value, r.unit), threshold(r.threshold, r.direction, r.unit), mark(r.passed)]));
472
+ const text = [
473
+ table(["Rule", "Measured", "Threshold", "Result"], rows, { title: `${result.ticker}: ${result.verdict}` }),
474
+ result.exemptions.length ? `\nExempted: ${result.exemptions.map((e) => `${e.rule} (${e.reason})`).join("; ")}` : "",
475
+ result.skipped_count ? `\n${result.skipped_count} rule(s) not computable from filings and NOT treated as passes.` : "",
476
+ ].filter(Boolean).join("\n");
477
+ sendResult(id, jsonContent(text, result));
478
+ return;
479
+ }
480
+ if (name === "screen_candidates") {
481
+ const result = await screenBatch({ candidates: args.candidates, asOf: args.as_of });
482
+ const rows = [
483
+ ...result.survivors.map((s2) => [s2.ticker, "survives", `${s2.rules_computed}/${s2.rules_total} rules`, "-"]),
484
+ ...result.eliminated.map((e) => [e.ticker, "**eliminated**", "-",
485
+ e.reasons.map((r) => `${r.rule} ${metricValue(r.measured, r.unit)} vs ${r.threshold}`).join("; ")]),
486
+ ...result.unavailable.map((u) => [u.ticker, "_unavailable_", "-", u.error?.slice(0, 60) ?? ""]),
487
+ ];
488
+ const text = [
489
+ table(["Ticker", "Verdict", "Coverage", "Eliminated by"], rows,
490
+ { title: `Screened ${result.screened}: ${result.survivors.length} survive, ${result.eliminated.length} eliminated` }),
491
+ "\nSurviving is not a recommendation -- these rules eliminate, they never select.",
492
+ ].join("\n");
493
+ sendResult(id, jsonContent(text, result));
494
+ return;
495
+ }
496
+ if (name === "list_us_universe") {
497
+ const all = await fetchUniverse();
498
+ const needle = String(args.contains || "").trim().toLowerCase();
499
+ const matched = needle
500
+ ? all.filter((c) => c.ticker.toLowerCase().includes(needle) || c.title.toLowerCase().includes(needle))
501
+ : all;
502
+ const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(500, args.limit)) : 50;
503
+ sendResult(id, jsonContent(
504
+ `${matched.length} of ${all.length} US filers matched${needle ? ` "${args.contains}"` : ""}; returning ${Math.min(limit, matched.length)}.`,
505
+ { total: all.length, matched: matched.length, companies: matched.slice(0, limit) },
506
+ ));
507
+ return;
508
+ }
509
+ if (name === "preflight_permissions") {
510
+ const result = preflightNetworkPermissions({ roster: args.roster || "default" });
511
+ sendResult(id, jsonContent(`Network permission preflight: ${result.status}. ${result.message}`, result));
512
+ return;
513
+ }
514
+ if (name === "get_macro_snapshot") {
515
+ const data = await getMacroSnapshot(args);
516
+ const total = data.blocks.reduce((sum, block) => sum + block.members.length, 0);
517
+ sendResult(id, jsonContent(
518
+ `Macro snapshot: ${total - data.unavailable.length}/${total} series, ${data.derived.filter((d) => d.available).length}/${data.derived.length} derived measures.`,
519
+ data,
520
+ ));
521
+ return;
522
+ }
523
+ if (name === "get_options_chain") {
524
+ const data = await fetchOptionsChain(args.symbol, { asOf: args.as_of });
525
+ if (!data.available) {
526
+ sendResult(id, jsonContent(`No options chain for ${data.symbol}: ${data.reason}`, data));
527
+ return;
528
+ }
529
+ const ref = data.reference_expiry;
530
+ sendResult(id, jsonContent(
531
+ `${data.symbol} options: ${data.contracts_with_iv}/${data.contracts_total} contracts with usable IV, `
532
+ + `reference ATM IV ${ref ? (ref.atm_iv * 100).toFixed(1) + "% at " + ref.dte + "d (" + ref.expiry + ")" : "unavailable"}, `
533
+ + `put/call OI ${data.open_interest.put_call_ratio ?? "n/a"}. Delayed. IV percentile is not computable from this snapshot.`,
534
+ data,
535
+ ));
536
+ return;
537
+ }
538
+ if (name === "get_market_narrative") {
539
+ const data = await getMarketNarrative(args || {});
540
+ const lead = data.themes[0];
541
+ sendResult(id, jsonContent(
542
+ `Market narrative over ${data.window_days}d: ${data.headlines_in_window} headlines in window `
543
+ + `(${data.excluded_outside_window} excluded as stale or undated). `
544
+ + (lead ? `Leading theme: ${lead.label.en} at ${lead.share_of_coverage_pct}% of coverage. ` : "No theme matched. ")
545
+ + `${data.unclassified_headlines} headlines matched no known theme.`,
546
+ data,
547
+ ));
548
+ return;
549
+ }
550
+ if (name === "get_news") {
551
+ const specs = [];
552
+ if (args.symbol) specs.push(tickerNewsFeed(args.symbol));
553
+ if (args.query) specs.push(queryNewsFeed(args.query));
554
+ if (args.cik) specs.push(filingsFeed(args.cik, args.forms || "8-K"));
555
+ if (!specs.length) throw invalidParams("get_news needs at least one of symbol, query or cik");
556
+ const data = await fetchFeeds(specs, { days: args.days ?? 14, asOf: args.as_of ?? null });
557
+ sendResult(id, jsonContent(
558
+ `${data.items.length} headlines in the last ${args.days ?? 14}d from ${data.feeds.filter((f) => f.ok).length}/${data.feeds.length} feeds; `
559
+ + `${data.excluded_outside_window} excluded as stale or undated.`,
560
+ data,
561
+ ));
562
+ return;
563
+ }
564
+ if (name === "get_social_pulse") {
565
+ const data = await getSocialPulse(args || {});
566
+ sendResult(id, jsonContent(
567
+ `Social pulse for ${data.query ?? "(no query)"}: ${data.counts.reddit} Reddit, ${data.counts.hackernews} HN, `
568
+ + `${data.counts.bluesky} Bluesky over ${data.window_days}d. No free X discovery exists, so professional `
569
+ + `FinTwit is NOT covered. Mentions measure attention, not correctness.`,
570
+ data,
571
+ ));
572
+ return;
573
+ }
574
+ if (name === "verify_x_post") {
575
+ const data = await verifyXPost(args.id);
576
+ sendResult(id, jsonContent(
577
+ data.exists ? `Post ${data.id} exists: @${data.author ?? "unknown"} on ${(data.created_at || "").slice(0, 10)}.`
578
+ : `Post ${data.id} could not be confirmed: ${data.reason}.`,
579
+ data,
580
+ ));
581
+ return;
582
+ }
583
+ if (name === "list_council_options") {
584
+ const data = councilOptions({ language: args.language || "English" });
585
+ const rows = data.presets.map((p) => [
586
+ p.id, p.label, `${p.seats} seats`, `~${p.rough_minutes}m`, p.good_for,
587
+ ]);
588
+ const text = [
589
+ table(["Preset", "What runs", "Seats", "Rough cost", "Good for"], rows, { title: "Choose a council" }),
590
+ "",
591
+ `Master rosters: ${data.master_rosters.map((r) => `${r.roster} (${r.count})`).join(" · ")} — ${data.all_masters_count} lenses in total.`,
592
+ `Analysts: ${data.analysts.length} available, ${data.default_analysts.length} in the default fan-out.`,
593
+ "",
594
+ "Ask the user which they want before starting. Do not ask if they already said.",
595
+ ].join("\n");
596
+ sendResult(id, jsonContent(text, data));
597
+ return;
598
+ }
599
+ if (name === "get_quote") {
600
+ const data = await getQuotes(args);
601
+ const ok = data.quotes.filter((q) => !q.error).length;
602
+ sendResult(id, jsonContent(`Fetched ${ok}/${data.quotes.length} delayed quotes`, data));
603
+ return;
604
+ }
605
+ if (name === "collect_evidence") {
606
+ const run = await collectEvidence(args);
607
+ sendResult(id, jsonContent(`Saved ${run.packets.length} evidence packets for ${run.symbol}: ${run.run_id}`, run));
608
+ return;
609
+ }
610
+ if (name === "analyze_symbol") {
611
+ const result = await analyzeSymbol(args);
612
+ sendResult(id, jsonContent(`Saved AlphaCouncil Agent analysis for ${result.run.symbol}: ${result.run.run_id}`, result));
613
+ return;
614
+ }
615
+ if (name === "read_run") {
616
+ const idArg = args.run_id;
617
+ const dir = runPath(idArg);
618
+ const evidence = readJson(join(dir, "evidence.json"));
619
+ const decisionPath = join(dir, "decision.json");
620
+ const decision = existsSync(decisionPath) ? readJson(decisionPath) : null;
621
+ const allAgentsPath = join(dir, "all_agents.md");
622
+ const finalReportPath = join(dir, "final_report.md");
623
+ const userResponsePath = join(dir, "user_response.md");
624
+ const statusPath = join(dir, "status.json");
625
+ const eventsPath = join(dir, "events.jsonl");
626
+ const sourceManifestPath = join(dir, "source_manifest.json");
627
+ const reportQualityPath = join(dir, "report_quality.json");
628
+ const eventLog = readJsonl(eventsPath);
629
+ sendResult(id, jsonContent(`Loaded AlphaCouncil Agent run ${idArg}`, {
630
+ evidence,
631
+ decision,
632
+ source_manifest: existsSync(sourceManifestPath) ? readJson(sourceManifestPath) : sourceManifest(evidence),
633
+ report_quality: existsSync(reportQualityPath) ? readJson(reportQualityPath) : null,
634
+ status: existsSync(statusPath) ? readJson(statusPath) : null,
635
+ events: eventLog.entries,
636
+ events_parse_errors: eventLog.parse_errors,
637
+ artifacts: {
638
+ ...artifactPaths(evidence),
639
+ all_agents_md: allAgentsPath,
640
+ final_report_md: finalReportPath,
641
+ user_response_md: userResponsePath,
642
+ source_manifest_json: sourceManifestPath,
643
+ status_json: statusPath,
644
+ events_jsonl: eventsPath,
645
+ },
646
+ all_agents_markdown: existsSync(allAgentsPath) ? readFileSync(allAgentsPath, "utf8") : "",
647
+ final_report_markdown: existsSync(finalReportPath) ? readFileSync(finalReportPath, "utf8") : "",
648
+ user_response_markdown: existsSync(userResponsePath) ? readFileSync(userResponsePath, "utf8") : "",
649
+ }));
650
+ return;
651
+ }
652
+ if (name === "compare_summary_modes") {
653
+ const modes = summaryModes(resolveLanguage(args));
654
+ sendResult(id, jsonContent(JSON.stringify(modes, null, 2), { modes }));
655
+ return;
656
+ }
657
+ throw methodNotFound(`Unknown tool: ${name}`);
658
+ }
659
+
660
+ export async function handleRequest(message) {
661
+ const { id, method, params } = message;
662
+ if (startupFailure && id !== undefined) {
663
+ sendError(id, RpcCode.INTERNAL_ERROR, `alphacouncil-agent cannot serve requests: ${startupFailure}`);
664
+ return;
665
+ }
666
+ if (method === "initialize") {
667
+ sendResult(id, {
668
+ protocolVersion: params?.protocolVersion ?? "2025-11-25",
669
+ capabilities: { tools: {} },
670
+ serverInfo: { name: SERVER_NAME, version: VERSION },
671
+ instructions: "Use AlphaCouncil Agent to coordinate public-equity research subagents, save shared evidence packets, and produce manager-style long/short decisions.",
672
+ });
673
+ return;
674
+ }
675
+ if (method === "ping") {
676
+ sendResult(id, {});
677
+ return;
678
+ }
679
+ if (method === "tools/list") {
680
+ sendResult(id, { tools: tools() });
681
+ return;
682
+ }
683
+ if (method === "tools/call") {
684
+ try {
685
+ await handleToolCall(id, params);
686
+ } catch (error) {
687
+ // Previously every throw became INVALID_PARAMS, so a missing run directory or a
688
+ // failed fetch was reported to the host as a caller mistake.
689
+ const rpc = toRpcError(error);
690
+ sendError(id, rpc.code, rpc.message, rpc.data);
691
+ }
692
+ return;
693
+ }
694
+ if (id !== undefined) {
695
+ sendError(id, RpcCode.METHOD_NOT_FOUND, `Method not found: ${method}`);
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Set when the persona set fails to load. Every request then answers with an actionable
701
+ * error instead of the server appearing healthy and producing empty prompts later.
702
+ */
703
+ let startupFailure = null;
704
+
705
+ export function startStdioServer() {
706
+ // stdout is the JSON-RPC frame channel; diagnostics must never go there.
707
+ process.on("uncaughtException", (error) => {
708
+ process.stderr.write(`[alphacouncil] uncaught exception: ${error?.stack || error}\n`);
709
+ });
710
+ process.on("unhandledRejection", (reason) => {
711
+ process.stderr.write(`[alphacouncil] unhandled rejection: ${reason?.stack || reason}\n`);
712
+ });
713
+ sweepStaleOutputs();
714
+ // Load personas eagerly. Lazy loading made `initialize` succeed against a missing or
715
+ // malformed persona set, so the host believed the server was healthy and only found
716
+ // out several tool calls later.
717
+ try {
718
+ registry();
719
+ } catch (error) {
720
+ startupFailure = error.message;
721
+ process.stderr.write(`[alphacouncil] ${error.message}\n`);
722
+ }
723
+ const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
724
+ lines.on("line", (line) => {
725
+ if (!line.trim()) return;
726
+ let message;
727
+ try {
728
+ message = JSON.parse(line);
729
+ } catch (error) {
730
+ // Answering is strictly better than the old silent `catch {}`: the host learns
731
+ // its frame was rejected instead of waiting for a reply that never comes.
732
+ sendError(null, RpcCode.PARSE_ERROR, `invalid JSON frame: ${error.message}`);
733
+ return;
734
+ }
735
+ void handleRequest(message);
736
+ });
737
+ }