alphacouncil-agent 0.6.0 → 0.8.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.
@@ -0,0 +1,182 @@
1
+ {
2
+ "schema_version": 2,
3
+ "persona_id": "master_marks",
4
+ "display_name": {
5
+ "en": "Marks Method Model",
6
+ "zh": "霍华德·马克斯方法模型"
7
+ },
8
+ "kind": "operator_lens",
9
+
10
+ "admission": {
11
+ "propositions": 5,
12
+ "primary_sources": 3,
13
+ "decision_cases": 0,
14
+ "failure_cases": 1,
15
+ "vetoes": 2,
16
+ "counterfactuals": 0
17
+ },
18
+
19
+ "sources": [
20
+ {
21
+ "id": "marks:S01",
22
+ "grade": "A",
23
+ "title": "Oaktree Capital memos by Howard Marks (signed, author's own)",
24
+ "url": "https://www.oaktreecapital.com/insights/memos",
25
+ "date": "1990-2026",
26
+ "excerpt": "we cannot predict but we can prepare; where we stand in the cycle"
27
+ },
28
+ {
29
+ "id": "marks:S02",
30
+ "grade": "A",
31
+ "title": "The Most Important Thing (author's own text)",
32
+ "url": "https://cup.columbia.edu/book/the-most-important-thing/9780231162845",
33
+ "date": "2011-05-01",
34
+ "excerpt": "risk is the probability of permanent loss, not volatility; second-level thinking"
35
+ },
36
+ {
37
+ "id": "marks:S03",
38
+ "grade": "A",
39
+ "title": "Mastering the Market Cycle (author's own text)",
40
+ "url": "https://www.hmhbooks.com/shop/books/mastering-the-market-cycle/9781328479259",
41
+ "date": "2018-10-02",
42
+ "excerpt": "calibrate position to where we stand, not to a forecast"
43
+ }
44
+ ],
45
+
46
+ "doctrine": [
47
+ {
48
+ "rule_id": "marks.risk_definition.01",
49
+ "claim": "Risk is the probability of permanent capital loss, not price volatility; a volatile asset that cannot be impaired is not the risk being priced",
50
+ "scope": ["public_equity", "credit"],
51
+ "trigger": "risk_assessment",
52
+ "action": "require_evidence",
53
+ "source_ids": ["marks:S02"],
54
+ "confidence": "high",
55
+ "counterexamples": [],
56
+ "version": 1
57
+ },
58
+ {
59
+ "rule_id": "marks.second_level.01",
60
+ "claim": "A view that matches consensus carries no return; the question is what price already assumes",
61
+ "scope": ["public_equity"],
62
+ "trigger": "view_matches_consensus",
63
+ "action": "downweight",
64
+ "source_ids": ["marks:S02"],
65
+ "confidence": "high",
66
+ "counterexamples": [],
67
+ "version": 1
68
+ },
69
+ {
70
+ "rule_id": "marks.cycle_position.01",
71
+ "claim": "Calibrate aggressiveness to cycle position rather than to a forecast of the next move",
72
+ "scope": ["portfolio"],
73
+ "trigger": "sizing_decision",
74
+ "action": "size_cap",
75
+ "source_ids": ["marks:S03"],
76
+ "confidence": "high",
77
+ "counterexamples": [],
78
+ "version": 1
79
+ },
80
+ {
81
+ "rule_id": "marks.euphoria_veto.01",
82
+ "claim": "Refuse to add when credit is loose, spreads are compressed and scepticism has disappeared, however good the asset",
83
+ "scope": ["public_equity", "credit"],
84
+ "trigger": "cycle_euphoria",
85
+ "action": "veto",
86
+ "source_ids": ["marks:S01", "marks:S03"],
87
+ "confidence": "high",
88
+ "counterexamples": [],
89
+ "version": 1
90
+ },
91
+ {
92
+ "rule_id": "marks.no_forecast.01",
93
+ "claim": "Do not forecast macro; read where we stand and what is already priced",
94
+ "scope": ["portfolio"],
95
+ "trigger": "macro_forecast_requested",
96
+ "action": "out_of_scope",
97
+ "source_ids": ["marks:S01"],
98
+ "confidence": "high",
99
+ "counterexamples": [],
100
+ "version": 1
101
+ }
102
+ ],
103
+
104
+ "research_policy": {
105
+ "priorities": ["cycle_position", "consensus_gap", "price_implied_expectations", "permanent_loss"],
106
+ "must_recompute": ["price_implied_growth", "downside_to_impairment"],
107
+ "must_seek_disconfirming": ["what the price already assumes", "who is on the other side", "credit availability"],
108
+ "evidence_slice": ["valuation_long_short", "quant_factor", "macro_regime"]
109
+ },
110
+
111
+ "decision_policy": {
112
+ "eligibility": {
113
+ "requires": [
114
+ "cycle.valuation_percentile",
115
+ "cycle.credit_spread_percentile"
116
+ ],
117
+ "on_fail": "out_of_scope"
118
+ },
119
+ "scoring": {
120
+ "max_score": 10,
121
+ "rules": [
122
+ {
123
+ "id": "valuation_percentile",
124
+ "metric": "cycle.valuation_percentile",
125
+ "op": "<=",
126
+ "value": 0.4,
127
+ "points": 3,
128
+ "provenance": "marks:S03 calibrate to where we stand; the lower decile of the range is where the odds sit"
129
+ },
130
+ {
131
+ "id": "consensus_gap",
132
+ "metric": "cycle.consensus_gap_score",
133
+ "op": ">=",
134
+ "value": 0.5,
135
+ "points": 3,
136
+ "provenance": "marks:S02 second-level thinking; a view identical to consensus earns nothing"
137
+ },
138
+ {
139
+ "id": "credit_not_euphoric",
140
+ "metric": "cycle.credit_spread_percentile",
141
+ "op": ">=",
142
+ "value": 0.3,
143
+ "points": 2,
144
+ "provenance": "marks:S01 loose credit is the reliable marker of a late cycle"
145
+ },
146
+ {
147
+ "id": "impairment_bounded",
148
+ "metric": "risk.permanent_loss_bounded",
149
+ "op": "==",
150
+ "value": true,
151
+ "points": 2,
152
+ "provenance": "marks:S02 risk is permanent loss; a bounded downside is the precondition, not the upside"
153
+ }
154
+ ]
155
+ },
156
+ "stance_bands": [
157
+ { "min_ratio": 0.7, "stance": "constructive" },
158
+ { "min_ratio": 0.4, "stance": "cautious" },
159
+ { "min_ratio": 0, "stance": "opposed" }
160
+ ],
161
+ "vetoes": [
162
+ {
163
+ "id": "cycle_euphoria",
164
+ "condition": "cycle.valuation_percentile >= 0.9",
165
+ "source_ids": ["marks:S03"]
166
+ },
167
+ {
168
+ "id": "unbounded_impairment",
169
+ "condition": "!risk.permanent_loss_bounded",
170
+ "source_ids": ["marks:S02"]
171
+ }
172
+ ]
173
+ },
174
+
175
+ "memory_policy": {
176
+ "horizon_days": 540,
177
+ "belief_decay_days": 720,
178
+ "leak_rule": "public_at <= as_of AND memory_created_at <= as_of"
179
+ },
180
+
181
+ "tools": ["get_macro_snapshot", "get_market_narrative"]
182
+ }
@@ -0,0 +1,182 @@
1
+ {
2
+ "schema_version": 2,
3
+ "persona_id": "master_taleb",
4
+ "display_name": {
5
+ "en": "Taleb Method Model",
6
+ "zh": "塔勒布方法模型"
7
+ },
8
+ "kind": "operator_lens",
9
+
10
+ "admission": {
11
+ "propositions": 5,
12
+ "primary_sources": 3,
13
+ "decision_cases": 0,
14
+ "failure_cases": 1,
15
+ "vetoes": 2,
16
+ "counterfactuals": 0
17
+ },
18
+
19
+ "sources": [
20
+ {
21
+ "id": "taleb:S01",
22
+ "grade": "A",
23
+ "title": "Antifragile: Things That Gain from Disorder (author's own text)",
24
+ "url": "https://www.penguinrandomhouse.com/books/176227/antifragile-by-nassim-nicholas-taleb/",
25
+ "date": "2012-11-27",
26
+ "excerpt": "convexity: gaining more from favourable outcomes than losing from unfavourable ones"
27
+ },
28
+ {
29
+ "id": "taleb:S02",
30
+ "grade": "A",
31
+ "title": "Dynamic Hedging: Managing Vanilla and Exotic Options",
32
+ "url": "https://www.wiley.com/en-us/Dynamic+Hedging-p-9780471152804",
33
+ "date": "1997-01-14",
34
+ "excerpt": "skew and the pricing of tails"
35
+ },
36
+ {
37
+ "id": "taleb:S03",
38
+ "grade": "A",
39
+ "title": "The Black Swan / Incerto: ruin and non-ergodicity",
40
+ "url": "https://www.penguinrandomhouse.com/books/176226/the-black-swan-by-nassim-nicholas-taleb/",
41
+ "date": "2007-04-17",
42
+ "excerpt": "absorbing barriers: a strategy that can reach ruin has no expected value worth quoting"
43
+ }
44
+ ],
45
+
46
+ "doctrine": [
47
+ {
48
+ "rule_id": "taleb.ruin_first.01",
49
+ "claim": "Rule out ruin before discussing return; an absorbing barrier voids any expected value",
50
+ "scope": ["public_equity", "portfolio", "derivatives"],
51
+ "trigger": "ruin_path_exists",
52
+ "action": "veto",
53
+ "source_ids": ["taleb:S03"],
54
+ "confidence": "high",
55
+ "counterexamples": [],
56
+ "version": 1
57
+ },
58
+ {
59
+ "rule_id": "taleb.convexity.01",
60
+ "claim": "Prefer payoffs that gain more from favourable surprise than they lose from unfavourable; refuse concave payoffs whatever their carry",
61
+ "scope": ["derivatives", "public_equity"],
62
+ "trigger": "payoff_shape_assessment",
63
+ "action": "require_evidence",
64
+ "source_ids": ["taleb:S01"],
65
+ "confidence": "high",
66
+ "counterexamples": [],
67
+ "version": 1
68
+ },
69
+ {
70
+ "rule_id": "taleb.tail_pricing.01",
71
+ "claim": "A flat skew on a security that has demonstrated jumps means the tail is not being paid for",
72
+ "scope": ["derivatives"],
73
+ "trigger": "skew_flat_with_realized_jumps",
74
+ "action": "upweight",
75
+ "source_ids": ["taleb:S02"],
76
+ "confidence": "medium",
77
+ "counterexamples": ["a wide bid-ask can make a skew reading a measurement artifact rather than a price"],
78
+ "version": 1
79
+ },
80
+ {
81
+ "rule_id": "taleb.frictions.01",
82
+ "claim": "An edge smaller than the round-trip cost of expressing it is not an edge",
83
+ "scope": ["derivatives"],
84
+ "trigger": "edge_below_friction",
85
+ "action": "veto",
86
+ "source_ids": ["taleb:S02"],
87
+ "confidence": "high",
88
+ "counterexamples": [],
89
+ "version": 1
90
+ },
91
+ {
92
+ "rule_id": "taleb.no_point_forecast.01",
93
+ "claim": "Do not issue a direction or a target price; state the payoff structure and the conditions of ruin",
94
+ "scope": ["public_equity", "derivatives"],
95
+ "trigger": "price_target_requested",
96
+ "action": "out_of_scope",
97
+ "source_ids": ["taleb:S03"],
98
+ "confidence": "high",
99
+ "counterexamples": [],
100
+ "version": 1
101
+ }
102
+ ],
103
+
104
+ "research_policy": {
105
+ "priorities": ["fragility", "tail_pricing", "liquidity", "leverage"],
106
+ "must_recompute": ["realized_vs_implied_vol", "round_trip_friction"],
107
+ "must_seek_disconfirming": ["hidden leverage", "covenant triggers", "liquidity withdrawal", "expiry gaps around known events"],
108
+ "evidence_slice": ["quant_factor", "ib_event_analysis"]
109
+ },
110
+
111
+ "decision_policy": {
112
+ "eligibility": {
113
+ "requires": [
114
+ "options.chain_available",
115
+ "options.atm_iv"
116
+ ],
117
+ "on_fail": "out_of_scope"
118
+ },
119
+ "scoring": {
120
+ "max_score": 10,
121
+ "rules": [
122
+ {
123
+ "id": "implied_below_realized",
124
+ "metric": "options.realized_minus_implied_vol_points",
125
+ "op": ">=",
126
+ "value": 5,
127
+ "points": 3,
128
+ "provenance": "taleb:S02 tails are cheap when implied sits below demonstrated realized movement"
129
+ },
130
+ {
131
+ "id": "skew_flat",
132
+ "metric": "options.skew_25d_points",
133
+ "op": "<=",
134
+ "value": 1,
135
+ "points": 2,
136
+ "provenance": "taleb:S02 a flat skew on a jumping security means nobody is paying for the tail"
137
+ },
138
+ {
139
+ "id": "edge_survives_friction",
140
+ "metric": "options.net_edge_vol_points",
141
+ "op": ">=",
142
+ "value": 3,
143
+ "points": 3,
144
+ "provenance": "taleb:S02 an edge below round-trip cost is not an edge; friction is subtracted before it counts"
145
+ },
146
+ {
147
+ "id": "event_covered",
148
+ "metric": "options.expiry_covers_next_event",
149
+ "op": "==",
150
+ "value": true,
151
+ "points": 2,
152
+ "provenance": "taleb:S01 convexity bought outside the event window is not convexity, it is decay"
153
+ }
154
+ ]
155
+ },
156
+ "stance_bands": [
157
+ { "min_ratio": 0.7, "stance": "constructive" },
158
+ { "min_ratio": 0.35, "stance": "cautious" },
159
+ { "min_ratio": 0, "stance": "opposed" }
160
+ ],
161
+ "vetoes": [
162
+ {
163
+ "id": "ruin_path",
164
+ "condition": "risk.ruin_possible",
165
+ "source_ids": ["taleb:S03"]
166
+ },
167
+ {
168
+ "id": "edge_eaten_by_friction",
169
+ "condition": "options.net_edge_vol_points <= 0",
170
+ "source_ids": ["taleb:S02"]
171
+ }
172
+ ]
173
+ },
174
+
175
+ "memory_policy": {
176
+ "horizon_days": 120,
177
+ "belief_decay_days": 180,
178
+ "leak_rule": "public_at <= as_of AND memory_created_at <= as_of"
179
+ },
180
+
181
+ "tools": ["get_options_chain", "get_quote"]
182
+ }
@@ -51,6 +51,10 @@ export const REPORT_SECTIONS = [
51
51
  { id: "conclusion", aliases: ["结论", "conclusion"], min_body: 6 },
52
52
  { id: "analyst_work_log", aliases: ["分析师工作记录", "analyst work log"], min_body: 12, per_task: true },
53
53
  { id: "debate_record", aliases: ["多空辩论记录", "bull bear debate", "debate record"], min_body: 20 },
54
+ // Required only when a bench actually ran. A report that spends ten master seats and
55
+ // then mentions none of them is the defect this entry exists to catch; a screen-only
56
+ // run that never selected one must not be failed for omitting it.
57
+ { id: "master_bench", aliases: ["大师席", "master bench", "master lens"], min_body: 20, when_masters: true },
54
58
  { id: "market_expectations", aliases: ["市场预期", "market expectations"], min_body: 8 },
55
59
  { id: "analyst_rating", aliases: ["分析师评级", "analyst rating", "target price"], min_body: 8 },
56
60
  { id: "earnings_call", aliases: ["电话会", "earnings call"], min_body: 8 },
package/mcp/lib/gates.mjs CHANGED
@@ -161,7 +161,9 @@ export function validateFinalReport(markdown, run) {
161
161
  const missing = [];
162
162
  const sections = [];
163
163
 
164
+ const benchRan = ((run?.masters || []).length > 0) || ((run?.master_opinions || []).length > 0);
164
165
  for (const section of REPORT_SECTIONS) {
166
+ if (section.when_masters && !benchRan) continue;
165
167
  const heading = assigned.get(section.id);
166
168
  if (!heading) {
167
169
  missing.push(`missing section: ${section.id}`);
@@ -203,6 +205,6 @@ export function validateFinalReport(markdown, run) {
203
205
  missing,
204
206
  sections,
205
207
  checked_at: new Date().toISOString(),
206
- required_sections: REPORT_SECTIONS.map((section) => section.id),
208
+ required_sections: REPORT_SECTIONS.filter((s) => !s.when_masters || benchRan).map((section) => section.id),
207
209
  };
208
210
  }
@@ -6,6 +6,7 @@ import { isChineseLanguage } from "./lang.mjs";
6
6
  import { bullets, clip, fence } from "./text.mjs";
7
7
  import { completenessStatus, validateFinalReport, verificationStatus, withCompletenessBanner, withDisclaimer, withVerificationBanner } from "./gates.mjs";
8
8
  import { agentState, appendEvent, artifactPaths, runPath, taskState } from "./run-store.mjs";
9
+ import { personaTitle, registry } from "./personas/registry.mjs";
9
10
 
10
11
  export function renderPacketMarkdown(packet, index) {
11
12
  const claims = packet.claims.length
@@ -49,6 +50,176 @@ export function renderPacketMarkdown(packet, index) {
49
50
  ].join("\n");
50
51
  }
51
52
 
53
+ /**
54
+ * A recorded master opinion, rendered so a reader can see what the lens actually said.
55
+ *
56
+ * Master opinions were stored, gated for completeness and weighted into the synthesis, and
57
+ * then rendered nowhere: a run could select ten lenses, pass every gate, and emit a report
58
+ * in which none of them were readable. `out_of_scope` is included deliberately — a method
59
+ * declining to judge is a finding, and hiding it is how a bench looks unanimous.
60
+ */
61
+ export function renderMasterMarkdown(opinion, lang) {
62
+ if (!opinion) return "";
63
+ const title = masterTitle(opinion.master, lang);
64
+ return [
65
+ `## ${title}`,
66
+ "",
67
+ `- ID: ${opinion.master}`,
68
+ `- Stance: ${opinion.stance || "unknown"}`,
69
+ `- Verdict: ${opinion.verdict || ""}`,
70
+ `- Confidence: ${opinion.confidence || "low"}`,
71
+ opinion.thread_id ? `- Visible thread ID: ${opinion.thread_id}` : "",
72
+ "",
73
+ "### Summary",
74
+ opinion.summary || "",
75
+ "",
76
+ "### Key Findings",
77
+ bullets(opinion.key_findings),
78
+ "",
79
+ "### Disagreements With The Analysts",
80
+ bullets(opinion.disagreements),
81
+ "",
82
+ "### Disqualifiers Triggered",
83
+ bullets(opinion.disqualifiers_triggered),
84
+ "",
85
+ "### What Would Change My Mind",
86
+ bullets(opinion.what_would_change_my_mind),
87
+ "",
88
+ "### Sources",
89
+ (opinion.source_ids || []).length ? (opinion.source_ids || []).map((id) => `- ${id}`).join("\n") : "- None",
90
+ ].filter((line) => line !== "").join("\n");
91
+ }
92
+
93
+ /**
94
+ * Printed above every rendered bench, in the report's language.
95
+ *
96
+ * Master seats share a base model, an evidence brief and a context window, so their errors
97
+ * are correlated and their agreement is not independent confirmation. Published measurements
98
+ * put LLM error correlation above 60%, which is why a tally of concurring seats is the
99
+ * weakest thing a council produces and the dissenting seat is the informative one. Stating
100
+ * this next to the opinions is the difference between a bench and a vote count.
101
+ */
102
+ export function masterCorrelationNote(run) {
103
+ const opinions = run?.master_opinions || [];
104
+ if (!opinions.length) return "";
105
+ const stances = new Map();
106
+ for (const opinion of opinions) {
107
+ const key = opinion.stance || "unknown";
108
+ stances.set(key, (stances.get(key) || 0) + 1);
109
+ }
110
+ const spread = [...stances.entries()].map(([stance, n]) => `${stance}=${n}`).join(", ");
111
+ const zh = isChineseLanguage(run?.language);
112
+ return zh
113
+ ? [
114
+ "> **这些席位不是独立样本。** 它们共享同一个基础模型、同一份证据简报和同一个上下文,",
115
+ `> 因此错误是相关的。本次立场分布(${spread})**不能当作票数来计算**:一致本身是预期结果,`,
116
+ "> 不是发现。有信息量的是分歧席位,以及它的分歧来自信息差还是方法差。",
117
+ ].join("\n")
118
+ : [
119
+ "> **These seats are not independent samples.** They share a base model, an evidence",
120
+ `> brief and a context window, so their errors are correlated. The stance spread (${spread})`,
121
+ "> **is not a vote count**: agreement is the expected outcome, not a finding. The",
122
+ "> informative seat is the dissenting one, and why it dissents.",
123
+ ].join("\n");
124
+ }
125
+
126
+ /**
127
+ * The bench, ordered so the dissent is read first.
128
+ *
129
+ * A concurring seat is the weakest thing a council produces and the minority is where the
130
+ * information is -- measurements put the minority right in roughly one divergent case in
131
+ * four, and a majority rule discards exactly that. Printing the concurring block first
132
+ * reproduces the tally in prose even when the numbers have been removed, so the order is
133
+ * part of the fix rather than presentation.
134
+ */
135
+ export function renderBenchSummary(run) {
136
+ const opinions = run?.master_opinions || [];
137
+ if (!opinions.length) return "";
138
+ const zh = isChineseLanguage(run?.language);
139
+ const counts = new Map();
140
+ for (const o of opinions) counts.set(o.stance || "unknown", (counts.get(o.stance || "unknown") || 0) + 1);
141
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
142
+ const majority = ranked[0]?.[0];
143
+ const minority = opinions.filter((o) => (o.stance || "unknown") !== majority);
144
+ const concurring = opinions.filter((o) => (o.stance || "unknown") === majority);
145
+
146
+ const row = (o) => `| ${masterTitle(o.master, run?.language)} | ${o.stance || "unknown"} | ${o.confidence || "low"} | ${clip(o.verdict || o.summary || "", 90)} |`;
147
+ const head = zh
148
+ ? ["| 方法 | 立场 | 置信度 | 判断 |", "|---|---|---|---|"]
149
+ : ["| Method | Stance | Confidence | Verdict |", "|---|---|---|---|"];
150
+
151
+ const sections = [masterCorrelationNote(run), ""];
152
+ if (minority.length) {
153
+ sections.push(
154
+ zh ? "### 少数派(先读这个)" : "### Minority report (read this first)",
155
+ "",
156
+ zh
157
+ ? `${minority.length} 席与多数不同。分歧席位是本次运行里信息量最高的部分——请先判断分歧来自信息差还是方法差。`
158
+ : `${minority.length} seat(s) diverge. Divergence is the highest-information part of this run: establish whether it comes from the evidence slice or from the method.`,
159
+ "",
160
+ ...head,
161
+ ...minority.map(row),
162
+ "",
163
+ );
164
+ } else {
165
+ sections.push(
166
+ zh
167
+ ? "### 少数派:无\n\n所有席位立场一致。鉴于它们共享模型与证据,一致是预期结果而非确认——本次运行没有产生任何独立的反对意见。"
168
+ : "### Minority report: none\n\nEvery seat agreed. Given a shared model and a shared brief, agreement is the expected outcome rather than confirmation: this run produced no independent dissent.",
169
+ "",
170
+ );
171
+ }
172
+ sections.push(
173
+ zh ? "### 其余席位" : "### Concurring seats",
174
+ "",
175
+ ...head,
176
+ ...concurring.map(row),
177
+ );
178
+ return sections.filter((s) => s !== undefined).join("\n");
179
+ }
180
+
181
+ /**
182
+ * The deterministic half, printed as evidence rather than as a vote.
183
+ *
184
+ * Shows what each method could actually measure. `coverage` is the honest column: a score
185
+ * produced from a fifth of a rule set has sampled the company, not judged it.
186
+ */
187
+ export function renderDecisionTable(decisions, lang) {
188
+ if (!Array.isArray(decisions) || !decisions.length) return "";
189
+ const zh = isChineseLanguage(lang);
190
+ const head = zh
191
+ ? ["| 方法 | 可评估 | 得分 | 覆盖率 | 立场 | 依据 |", "|---|---|---|---|---|---|"]
192
+ : ["| Method | Eligible | Score | Coverage | Stance | Basis |", "|---|---|---|---|---|---|"];
193
+ const rows = decisions.map((d) => {
194
+ const eligible = d.reason === "eligibility" ? (zh ? "否" : "no") : (zh ? "是" : "yes");
195
+ const score = d.score && d.score.max_possible ? `${d.score.score}/${d.score.max_possible}` : "—";
196
+ const coverage = d.score && d.score.declared_max ? `${Math.round((d.score.coverage || 0) * 100)}%` : "—";
197
+ return `| ${d.persona_id} | ${eligible} | ${score} | ${coverage} | ${d.stance} | ${d.reason} |`;
198
+ });
199
+ return [
200
+ zh ? "### 确定性评分(模型调用之前)" : "### Deterministic scoring (before any model call)",
201
+ "",
202
+ ...head,
203
+ ...rows,
204
+ "",
205
+ zh
206
+ ? "> 覆盖率是这张表最重要的一列:只跑得动一小部分规则的方法是抽样了这家公司,不是判断了它。`可评估=否` 的席位没有花费任何模型调用。"
207
+ : "> Coverage is the column that matters: a method that could run a fraction of its rules sampled the company rather than judging it. Rows marked not eligible cost no model call.",
208
+ ].join("\n");
209
+ }
210
+
211
+ /** Registry title when the persona resolves, the raw id when it does not. */
212
+ function masterTitle(id, lang) {
213
+ if (!id) return "Master";
214
+ try {
215
+ const persona = registry().get(id);
216
+ const title = personaTitle(persona, lang);
217
+ return title && title !== id ? `${title} (${id})` : id;
218
+ } catch {
219
+ return id;
220
+ }
221
+ }
222
+
52
223
  export function renderDebateRounds(rounds) {
53
224
  if (!Array.isArray(rounds) || rounds.length === 0) return "";
54
225
  const blocks = rounds.map((round) => [
@@ -163,6 +334,17 @@ export function writeAllAgentsMarkdown(run, debate = {}) {
163
334
  "",
164
335
  ...run.packets.map(renderPacketMarkdown),
165
336
  ];
337
+ const opinions = run.master_opinions || [];
338
+ if (opinions.length) {
339
+ sections.push(
340
+ "",
341
+ "# Master Bench",
342
+ "",
343
+ renderBenchSummary(run),
344
+ "",
345
+ ...opinions.map((opinion) => renderMasterMarkdown(opinion, run.language)),
346
+ );
347
+ }
166
348
  if (debate.bull || debate.bear || debate.manager) {
167
349
  sections.push(
168
350
  "",
@@ -193,6 +375,9 @@ export function writeAnalystMarkdownFiles(run, debate = {}) {
193
375
  for (const [role, packet] of debateFiles) {
194
376
  if (packet) writeFileSync(join(dir, `${role}.md`), `${renderDebateMarkdown({ ...packet, role })}\n`);
195
377
  }
378
+ for (const opinion of run.master_opinions || []) {
379
+ writeFileSync(join(dir, `${opinion.master}.md`), `${renderMasterMarkdown(opinion, run.language)}\n`);
380
+ }
196
381
  }
197
382
 
198
383
  export function writeReportQuality(run, markdown) {
@@ -242,6 +427,10 @@ export function writeArtifactIndex(run, debate = {}) {
242
427
  debate.bull ? `- bull_researcher: ${artifacts.analyst_markdown.bull_researcher}` : "",
243
428
  debate.bear ? `- bear_researcher: ${artifacts.analyst_markdown.bear_researcher}` : "",
244
429
  debate.manager ? `- portfolio_manager: ${artifacts.analyst_markdown.portfolio_manager}` : "",
430
+ ...((run.master_opinions || []).length
431
+ ? ["", "## Master Bench Markdown Files", "",
432
+ ...(run.master_opinions || []).map((o) => `- ${o.master} (${o.stance || "unknown"}): ${join(runPath(run.run_id), `${o.master}.md`)}`)]
433
+ : []),
245
434
  ].filter(Boolean);
246
435
  writeFileSync(artifacts.artifact_index_md, `${lines.join("\n")}\n`);
247
436
  return artifacts.artifact_index_md;