alphacouncil-agent 0.9.2 → 0.9.4

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 (79) hide show
  1. package/.claude/commands/alpha.md +22 -3
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.grok/commands/alpha.md +22 -3
  6. package/.opencode/command/alpha.md +22 -3
  7. package/AGENTS.md +27 -4
  8. package/CHANGELOG.md +67 -0
  9. package/CLAUDE.md +41 -7
  10. package/README.ja.md +21 -11
  11. package/README.md +35 -11
  12. package/README.zh-CN.md +31 -11
  13. package/commands/alpha.md +22 -3
  14. package/data/build-profile.v1.json +1 -1
  15. package/data/master-selector-method-locales.v1.mjs +286 -0
  16. package/docs/INSTALL.md +43 -6
  17. package/docs/releases/v0.9.3.md +86 -0
  18. package/docs/releases/v0.9.4.md +79 -0
  19. package/docs/report-contract.md +50 -1
  20. package/knowledge/ai-assisted-solo/experiments/runs/a.json +4 -4
  21. package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
  22. package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
  23. package/knowledge/ai-assisted-solo/experiments/runs/d13.json +16 -16
  24. package/knowledge/ai-assisted-solo/experiments/runs/d26.json +29 -29
  25. package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +19 -19
  26. package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +32 -32
  27. package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +33 -33
  28. package/knowledge/ai-assisted-solo/experiments/simulation-input.json +79 -79
  29. package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
  30. package/knowledge/solo-test/masters/master_ackman/manifest.json +14 -4
  31. package/knowledge/solo-test/masters/master_aschenbrenner/manifest.json +14 -4
  32. package/knowledge/solo-test/masters/master_asness/manifest.json +14 -4
  33. package/knowledge/solo-test/masters/master_buffett/manifest.json +14 -4
  34. package/knowledge/solo-test/masters/master_burry/manifest.json +14 -4
  35. package/knowledge/solo-test/masters/master_cathie_wood/manifest.json +14 -4
  36. package/knowledge/solo-test/masters/master_dalio/manifest.json +14 -4
  37. package/knowledge/solo-test/masters/master_damodaran/manifest.json +14 -4
  38. package/knowledge/solo-test/masters/master_druckenmiller/manifest.json +14 -4
  39. package/knowledge/solo-test/masters/master_duan_yongping/manifest.json +14 -4
  40. package/knowledge/solo-test/masters/master_fisher/manifest.json +14 -4
  41. package/knowledge/solo-test/masters/master_forensic_short/manifest.json +14 -4
  42. package/knowledge/solo-test/masters/master_graham/manifest.json +14 -4
  43. package/knowledge/solo-test/masters/master_jhunjhunwala/manifest.json +14 -4
  44. package/knowledge/solo-test/masters/master_klarman/manifest.json +14 -4
  45. package/knowledge/solo-test/masters/master_li_lu/manifest.json +14 -4
  46. package/knowledge/solo-test/masters/master_lynch/manifest.json +14 -4
  47. package/knowledge/solo-test/masters/master_marks/manifest.json +14 -4
  48. package/knowledge/solo-test/masters/master_munger/manifest.json +14 -4
  49. package/knowledge/solo-test/masters/master_natenberg/manifest.json +14 -4
  50. package/knowledge/solo-test/masters/master_pabrai/manifest.json +14 -4
  51. package/knowledge/solo-test/masters/master_simons/manifest.json +14 -4
  52. package/knowledge/solo-test/masters/master_sinclair/manifest.json +14 -4
  53. package/knowledge/solo-test/masters/master_soros/manifest.json +14 -4
  54. package/knowledge/solo-test/masters/master_taleb/manifest.json +14 -4
  55. package/knowledge/solo-test/masters/master_thorp/manifest.json +14 -4
  56. package/mcp/lib/codex.mjs +3 -3
  57. package/mcp/lib/constants.mjs +61 -25
  58. package/mcp/lib/council-options.mjs +95 -40
  59. package/mcp/lib/council-selection.mjs +95 -9
  60. package/mcp/lib/gates.mjs +78 -11
  61. package/mcp/lib/lang.mjs +109 -2
  62. package/mcp/lib/markdown.mjs +444 -135
  63. package/mcp/lib/master-catalog.mjs +28 -3
  64. package/mcp/lib/orchestrator.mjs +952 -265
  65. package/mcp/lib/packets.mjs +173 -20
  66. package/mcp/lib/personas/engine.mjs +30 -30
  67. package/mcp/lib/personas-v3/compiler.mjs +2 -0
  68. package/mcp/lib/personas-v3/loader.mjs +10 -7
  69. package/mcp/lib/personas-v3/source-acquisition.mjs +10 -3
  70. package/mcp/lib/prompts.mjs +33 -0
  71. package/mcp/lib/rpc.mjs +75 -33
  72. package/mcp/lib/run-store.mjs +23 -0
  73. package/package.json +2 -2
  74. package/schemas/persona-v3.schema.json +37 -2
  75. package/scripts/lib/packaged-host-parity.mjs +54 -5
  76. package/scripts/lib/persona-v3-solo-test-packs.mjs +31 -5
  77. package/scripts/package-smoke.mjs +31 -12
  78. package/scripts/run-tests.mjs +23 -2
  79. package/skills/alphacouncil-agent/SKILL.md +50 -7
package/mcp/lib/codex.mjs CHANGED
@@ -51,9 +51,9 @@ export function stopChild(child, force = false) {
51
51
  * second Codex-backed search bridge, creating recursive workers and multi-minute nested
52
52
  * timeouts. Authentication still comes from CODEX_HOME according to the Codex CLI contract.
53
53
  */
54
- export function codexWorkerArgs(outFile, dataDir = DATA_DIR) {
54
+ export function codexWorkerArgs(outFile, dataDir = DATA_DIR, { search = true } = {}) {
55
55
  return [
56
- "--search",
56
+ ...(search ? ["--search"] : []),
57
57
  "-s",
58
58
  "read-only",
59
59
  "-a",
@@ -73,7 +73,7 @@ export function runCodex(prompt, timeoutMs, onStart = () => {}, onHeartbeat = ()
73
73
  return new Promise((resolvePromise) => {
74
74
  mkdirSync(DATA_DIR, { recursive: true });
75
75
  const outFile = join(DATA_DIR, `codex-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
76
- const args = codexWorkerArgs(outFile);
76
+ const args = codexWorkerArgs(outFile, DATA_DIR, { search: runtime.search !== false });
77
77
  const invocation = codexInvocation(args);
78
78
  const spawnWorker = runtime.spawn || spawn;
79
79
  const stopWorker = runtime.stopChild || stopChild;
@@ -59,37 +59,37 @@ export const OUTPUT_MODES = [
59
59
  * several legitimately terse sections ("## Confidence\nmedium").
60
60
  */
61
61
  export const REPORT_SECTIONS = [
62
- { id: "conclusion", aliases: ["结论", "conclusion"], min_body: 6 },
63
- { id: "analyst_work_log", aliases: ["分析师工作记录", "analyst work log"], min_body: 12, per_task: true },
64
- { id: "debate_record", aliases: ["多空辩论记录", "bull bear debate", "debate record"], min_body: 20 },
62
+ { id: "conclusion", aliases: ["结论", "conclusion", "結論", "결론"], min_body: 6 },
63
+ { id: "analyst_work_log", aliases: ["分析师工作记录", "analyst work log", "アナリスト作業記録", "分析担当作業記録", "분석가 작업 기록"], min_body: 12, per_task: true },
64
+ { id: "debate_record", aliases: ["多空辩论记录", "bull bear debate", "debate record", "強気弱気討論記録", "多空討論記録", "롱 숏 토론 기록", "강세 약세 토론 기록"], min_body: 20 },
65
65
  // Required only when a bench actually ran. A report that spends ten master seats and
66
66
  // then mentions none of them is the defect this entry exists to catch; a screen-only
67
67
  // run that never selected one must not be failed for omitting it.
68
- { id: "master_bench", aliases: ["大师席", "master席位", "master bench", "master lens"], min_body: 20, when_masters: true },
69
- { id: "market_expectations", aliases: ["市场预期", "market expectations"], min_body: 8 },
70
- { id: "analyst_rating", aliases: ["分析师评级", "analyst rating", "target price"], min_body: 8 },
71
- { id: "earnings_call", aliases: ["电话会", "earnings call"], min_body: 8 },
72
- { id: "quant", aliases: ["量化", "quant"], min_body: 8 },
73
- { id: "news", aliases: ["新闻", "news"], min_body: 8 },
74
- { id: "short_interest", aliases: ["short interest", "borrow"], min_body: 8 },
75
- { id: "strategic_transaction", aliases: ["战略交易", "strategic transaction", "banking event"], min_body: 8 },
76
- { id: "valuation", aliases: ["估值", "valuation"], min_body: 8 },
68
+ { id: "master_bench", aliases: ["大师席", "master席位", "master bench", "master lens", "マスターベンチ", "マスター・ベンチ", "メソッド席", "마스터 벤치", "방법론 좌석"], min_body: 20, when_masters: true },
69
+ { id: "market_expectations", aliases: ["市场预期", "market expectations", "市場予想", "市場期待", "시장 기대"], min_body: 8 },
70
+ { id: "analyst_rating", aliases: ["分析师评级", "analyst rating", "target price", "アナリスト評価", "目標株価", "애널리스트 등급", "목표가"], min_body: 8 },
71
+ { id: "earnings_call", aliases: ["电话会", "earnings call", "決算説明会", "실적 발표 콜"], min_body: 8 },
72
+ { id: "quant", aliases: ["量化", "quant", "クオンツ", "定量", "퀀트", "정량"], min_body: 8 },
73
+ { id: "news", aliases: ["新闻", "news", "ニュース", "뉴스"], min_body: 8 },
74
+ { id: "short_interest", aliases: ["short interest", "borrow", "空売り", "貸株", "공매도", "대차"], min_body: 8 },
75
+ { id: "strategic_transaction", aliases: ["战略交易", "strategic transaction", "banking event", "戦略取引", "銀行イベント", "전략적 거래", "금융 이벤트"], min_body: 8 },
76
+ { id: "valuation", aliases: ["估值", "valuation", "バリュエーション", "企業価値評価", "가치평가"], min_body: 8 },
77
77
  // Required, and deliberately not called "target price". A single number pretends to a
78
78
  // precision nobody has; what a reader needs is the price at which the case changes.
79
79
  // Three master lenses already demand this -- Marks asks at what price it stops being
80
80
  // "leave it", Graham asks for a calculable floor, Thorp asks for a size -- and the PM
81
81
  // used to be able to skip it with "the cycle position is undetermined".
82
- { id: "price_levels", aliases: ["价位", "入场价位", "price levels", "entry level", "价格条件"], min_body: 30 },
83
- { id: "catalysts", aliases: ["催化剂", "catalyst", "catalysts"], min_body: 8 },
84
- { id: "risks", aliases: ["风险", "risk", "risks"], min_body: 8 },
85
- { id: "position", aliases: ["仓位", "position"], min_body: 8 },
86
- { id: "short_term", aliases: ["短线", "short term"], min_body: 6 },
87
- { id: "medium_term", aliases: ["中期", "medium term"], min_body: 6 },
88
- { id: "long_term", aliases: ["长期", "long term"], min_body: 6 },
89
- { id: "data_gaps", aliases: ["数据缺口", "data gaps", "unavailable data"], min_body: 8 },
90
- { id: "invalidation", aliases: ["反证", "invalidation"], min_body: 8 },
91
- { id: "confidence", aliases: ["置信", "confidence"], min_body: 3 },
92
- { id: "source_table", aliases: ["来源表", "source table"], min_body: 6 },
82
+ { id: "price_levels", aliases: ["价位", "入场价位", "price levels", "entry level", "价格条件", "価格条件", "エントリー価格", "가격 조건", "진입 가격"], min_body: 30 },
83
+ { id: "catalysts", aliases: ["催化剂", "catalyst", "catalysts", "カタリスト", "材料", "촉매"], min_body: 8 },
84
+ { id: "risks", aliases: ["风险", "risk", "risks", "リスク", "위험"], min_body: 8 },
85
+ { id: "position", aliases: ["仓位", "position", "ポジション", "配分", "포지션"], min_body: 8 },
86
+ { id: "short_term", aliases: ["短线", "short term", "短期", "단기"], min_body: 6 },
87
+ { id: "medium_term", aliases: ["中期", "medium term", "중기"], min_body: 6 },
88
+ { id: "long_term", aliases: ["长期", "long term", "長期", "장기"], min_body: 6 },
89
+ { id: "data_gaps", aliases: ["数据缺口", "data gaps", "unavailable data", "データ欠落", "利用不可データ", "데이터 공백", "사용 불가 데이터"], min_body: 8 },
90
+ { id: "invalidation", aliases: ["反证", "invalidation", "無効化条件", "反証条件", "무효화 조건", "반증 조건"], min_body: 8 },
91
+ { id: "confidence", aliases: ["置信", "confidence", "信頼度", "신뢰도"], min_body: 3 },
92
+ { id: "source_table", aliases: ["来源表", "source table", "出典表", "ソース表", "출처 표"], min_body: 6 },
93
93
  ];
94
94
 
95
95
  // Quick reports are deliberately a different publication scope. Requiring the full
@@ -159,6 +159,42 @@ export const LIMITS = Object.freeze({
159
159
  SIGKILL_GRACE_MS: 5000,
160
160
  /** Default per-subagent Codex timeout. */
161
161
  CODEX_TIMEOUT_MS: Number(process.env.ALPHACOUNCIL_AGENT_TIMEOUT_MS) || 600000,
162
+ /** Non-overridable public ceiling for a plugin-managed full council. */
163
+ FULL_HARD_MAX_MS: 30 * 60 * 1000,
164
+ /** Default full-council queue-to-persistence budget. Operators may only lower it. */
165
+ FULL_TOTAL_MS: Math.max(1_000, Math.min(
166
+ Number(process.env.ALPHACOUNCIL_FULL_TOTAL_MS) || 30 * 60 * 1000,
167
+ 30 * 60 * 1000,
168
+ )),
169
+ /** Deterministic grounding is useful, but it may not hold the whole run hostage. */
170
+ FULL_GROUNDING_MS: 30 * 1000,
171
+ /** All eight full evidence seats launch in one wave under this per-seat cap. */
172
+ FULL_EVIDENCE_MS: Math.max(1_000, Math.min(
173
+ Number(process.env.ALPHACOUNCIL_FULL_EVIDENCE_MS) || 6 * 60 * 1000,
174
+ 6 * 60 * 1000,
175
+ )),
176
+ /** Every selected method gets one isolated explanation worker after its decision freezes. */
177
+ FULL_MASTER_MS: Math.max(1_000, Math.min(
178
+ Number(process.env.ALPHACOUNCIL_FULL_MASTER_MS) || 2 * 60 * 1000,
179
+ 2 * 60 * 1000,
180
+ )),
181
+ /** Bull and bear run together inside each of the three full-council rounds. */
182
+ FULL_DEBATE_MS: Math.max(1_000, Math.min(
183
+ Number(process.env.ALPHACOUNCIL_FULL_DEBATE_MS) || 150 * 1000,
184
+ 150 * 1000,
185
+ )),
186
+ /** The PM is the final model call and receives its own bounded slice. */
187
+ FULL_PM_MS: Math.max(1_000, Math.min(
188
+ Number(process.env.ALPHACOUNCIL_FULL_PM_MS) || 180 * 1000,
189
+ 180 * 1000,
190
+ )),
191
+ /** Reserved for forced child settlement, deterministic assembly and atomic persistence. */
192
+ FULL_FINALIZE_RESERVE_MS: 45 * 1000,
193
+ /** A malformed response gets one short, no-search transport-only repair. */
194
+ PARSE_REPAIR_MS: 30 * 1000,
195
+ PARSE_REPAIR_INPUT_CHARS: 80 * 1000,
196
+ FULL_EVIDENCE_CONCURRENCY: 8,
197
+ FULL_MASTER_CONCURRENCY: 13,
162
198
  /** Non-overridable public ceiling for a quick council, including retries and synthesis. */
163
199
  QUICK_HARD_MAX_MS: 10 * 60 * 1000,
164
200
  /** Default quick budget. Operators may lower it for stricter environments, never raise it. */
@@ -191,8 +227,8 @@ export const LIMITS = Object.freeze({
191
227
  Math.trunc(Number(process.env.ALPHACOUNCIL_QUICK_MIN_SUCCESSFUL_TASKS) || 2),
192
228
  )),
193
229
  CONCURRENCY_MIN: 1,
194
- CONCURRENCY_MAX: 6,
195
- CONCURRENCY_DEFAULT: Number(process.env.ALPHACOUNCIL_AGENT_CONCURRENCY) || 3,
230
+ CONCURRENCY_MAX: 26,
231
+ CONCURRENCY_DEFAULT: Number(process.env.ALPHACOUNCIL_AGENT_CONCURRENCY) || 8,
196
232
  /** A selection is deliberately short-lived and may create exactly one council run. */
197
233
  SELECTION_TTL_MS: Number(process.env.ALPHACOUNCIL_SELECTION_TTL_MS) || 60 * 60 * 1000,
198
234
  /** Selection lock metadata advertises this lease, but a live owner is never pre-empted. */
@@ -4,6 +4,7 @@ import { loadPacks } from "./personas-v2/loader.mjs";
4
4
  import { compiledPersonaPacks } from "./personas-v3/registry.mjs";
5
5
  import { sha256 } from "./personas-v3/canonical.mjs";
6
6
  import { selectorCard } from "./master-catalog.mjs";
7
+ import { languageKey, localized } from "./lang.mjs";
7
8
 
8
9
  /**
9
10
  * The menu a host shows before a run starts.
@@ -42,7 +43,8 @@ const estimateSelectionRange = ({ analysts = 0, allMasters = 1, verifiers = 0, d
42
43
  };
43
44
 
44
45
  export function councilOptions({ language = "English" } = {}) {
45
- const chinese = /中文|chinese|zh/i.test(String(language));
46
+ const locale = languageKey(language);
47
+ const copy = (messages) => localized(language, messages);
46
48
  const reg = registry();
47
49
  const packs = loadPacks();
48
50
  const v3Packs = compiledPersonaPacks();
@@ -59,15 +61,6 @@ export function councilOptions({ language = "English" } = {}) {
59
61
  covers: (p.tags || []).join(", "),
60
62
  }));
61
63
 
62
- const rosterChoices = masterRosters.map((roster) => {
63
- const members = selectRoster(reg, { kind: "master", roster });
64
- return {
65
- roster,
66
- count: members.length,
67
- members: members.map((m) => ({ id: m.id, title: personaTitle(m, language) })),
68
- };
69
- });
70
-
71
64
  // A flattened, stable-order catalog is the selection source of truth. Rosters remain
72
65
  // useful shortcuts, but a user may choose any 1..N individual methods across schools.
73
66
  const masterChoices = reg.ids("master").map((id, offset) => {
@@ -77,7 +70,13 @@ export function councilOptions({ language = "English" } = {}) {
77
70
  const v3Selection = v3?.manifest?.selection;
78
71
  const v3Label = v3?.admitted_label;
79
72
  const provisionalV3 = v3?.build_profile === "solo_test";
80
- const localized = (value) => chinese ? value?.zh : value?.en;
73
+ const field = (value, label) => {
74
+ const selected = value?.[locale];
75
+ if (typeof selected !== "string" || !selected.trim()) {
76
+ throw new Error(`${id}: missing ${locale} selector ${label}`);
77
+ }
78
+ return selected;
79
+ };
81
80
  // Until a physical v3 pack exists, bind the receipt to the exact v2 pack or prompt
82
81
  // persona rather than publishing a null hash. A prompt edit must invalidate a catalog
83
82
  // the user saw before that edit just as a v3 policy edit does.
@@ -94,13 +93,19 @@ export function councilOptions({ language = "English" } = {}) {
94
93
  return {
95
94
  index: offset + 1,
96
95
  id,
97
- title: localized(v3Label) || personaTitle(persona, language),
96
+ title: v3Label ? field(v3Label, "title") : personaTitle(persona, language),
98
97
  ...(v3Selection ? {
99
- identity: localized(v3Selection.identity),
100
- method: localized(v3Selection.method),
101
- best_for: localized(v3Selection.best_for),
98
+ identity: field(v3Selection.identity, "identity"),
99
+ method: field(v3Selection.method, "method"),
100
+ best_for: field(v3Selection.best_for, "best_for"),
102
101
  } : selectorCard(persona, language)),
103
102
  maturity: v3?.maturity || pack?.kind || "prompt_lens",
103
+ maturity_label: copy({
104
+ en: v3?.maturity === "method_model" ? "Validated method model" : v3?.maturity === "candidate" ? "Candidate method" : "Provisional operator lens",
105
+ zh: v3?.maturity === "method_model" ? "已验证方法模型" : v3?.maturity === "candidate" ? "候选方法" : "临时操作视角",
106
+ ja: v3?.maturity === "method_model" ? "検証済みメソッドモデル" : v3?.maturity === "candidate" ? "候補メソッド" : "暫定オペレーター・レンズ",
107
+ ko: v3?.maturity === "method_model" ? "검증된 방법론 모델" : v3?.maturity === "candidate" ? "후보 방법론" : "임시 오퍼레이터 렌즈",
108
+ }),
104
109
  runtime_level: v3?.admission?.level || (pack ? "v2_operator" : "v1_prompt"),
105
110
  admission_level: v3?.admission?.level || (pack ? "operator_lens" : "prompt_lens"),
106
111
  pack_format: v3 ? provisionalV3 ? "schema_v3_solo_test" : "schema_v3_physical" : pack ? "schema_v2_legacy" : "prompt_v1_legacy",
@@ -119,15 +124,31 @@ export function councilOptions({ language = "English" } = {}) {
119
124
  };
120
125
  });
121
126
 
127
+ const masterById = new Map(masterChoices.map((master) => [master.id, master]));
128
+ const rosterChoices = masterRosters.map((roster) => {
129
+ const members = selectRoster(reg, { kind: "master", roster });
130
+ return {
131
+ roster,
132
+ count: members.length,
133
+ members: members.map((member) => ({
134
+ id: member.id,
135
+ title: masterById.get(member.id)?.title || personaTitle(member, language),
136
+ })),
137
+ };
138
+ });
139
+
122
140
  const allMasters = masterChoices.length;
123
141
  const verifiers = reg.ids("verifier");
124
142
 
125
143
  const presets = [
126
144
  {
127
145
  id: "quick",
128
- label: chinese
129
- ? "快速:4 位核心分析师并行 + 最多 4 位大师 + 单轮并行多空 + 短综合"
130
- : "Quick: 4 core analysts in parallel + up to 4 masters + one parallel bull/bear round + short synthesis",
146
+ label: copy({
147
+ en: "Quick: 4 core analysts in parallel + up to 4 masters + one parallel bull/bear round + short synthesis",
148
+ zh: "快速:4 位核心分析师并行 + 最多 4 位大师 + 单轮并行多空 + 短综合",
149
+ ja: "クイック:中核アナリスト4席を並列実行 + マスター最大4席 + 1回の並列Bull/Bear + 短い総合判断",
150
+ ko: "퀵: 핵심 분석가 4개 좌석 병렬 + 마스터 최대 4개 좌석 + 1회 병렬 Bull/Bear + 짧은 종합 판단",
151
+ }),
131
152
  analysts: QUICK_TASKS,
132
153
  master_selection: "required_1_to_N",
133
154
  master_selection_maximum: 4,
@@ -139,33 +160,52 @@ export function councilOptions({ language = "English" } = {}) {
139
160
  debate_rounds: 1,
140
161
  report_contract: "quick_v1",
141
162
  full_council_equivalent: false,
142
- good_for: chinese
143
- ? "十分钟级方向性初读:保留大师、核心分析师和近期公司/行业新闻;不做三轮交叉问答或对抗核验"
144
- : "A bounded directional read retaining masters, core analysts and recent company/industry news; no three-round cross-exam or adversarial verification.",
163
+ good_for: copy({
164
+ en: "A bounded directional read retaining masters, core analysts and recent company/industry news; no three-round cross-exam or adversarial verification.",
165
+ zh: "十分钟级方向性初读:保留大师、核心分析师和近期公司/行业新闻;不做三轮交叉问答或对抗核验",
166
+ ja: "短時間の方向性確認。マスター、中核アナリスト、直近の企業・業界ニュースを含むが、3ラウンドの反対尋問や対抗検証は行わない。",
167
+ ko: "짧은 방향성 검토. 마스터, 핵심 분석가, 최근 기업·산업 뉴스를 포함하지만 3라운드 교차 질문이나 적대적 검증은 수행하지 않는다.",
168
+ }),
145
169
  },
146
170
  {
147
171
  id: "standard",
148
- label: chinese ? "标准:8 位分析师 + 所选大师 + 辩论" : "Standard: 8 analysts + selected masters + debate",
172
+ label: copy({
173
+ en: "Standard: 8 analysts + selected masters + debate",
174
+ zh: "标准:8 位分析师 + 所选大师 + 辩论",
175
+ ja: "標準:アナリスト8席 + 選択したマスター + 討論",
176
+ ko: "표준: 분석가 8개 좌석 + 선택한 마스터 + 토론",
177
+ }),
149
178
  analysts: DEFAULT_TASKS,
150
179
  master_selection: "required_1_to_N",
151
180
  suggested_masters_roster: "masters-core",
152
181
  verify: false,
153
182
  ...estimateSelectionRange({ analysts: DEFAULT_TASKS.length, allMasters }),
154
- good_for: chinese
155
- ? "默认推荐。覆盖完整证据面,并运行用户本次确认的方法席"
156
- : "The recommended default: full evidence coverage plus this run's confirmed methods.",
183
+ good_for: copy({
184
+ en: "The recommended default: full evidence coverage plus this run's confirmed methods.",
185
+ zh: "默认推荐。覆盖完整证据面,并运行用户本次确认的方法席",
186
+ ja: "推奨される標準設定。証拠範囲を完全にカバーし、この実行で確定したメソッド席を動かす。",
187
+ ko: "권장 기본값. 전체 증거 범위를 다루고 이번 실행에서 확정한 방법론 좌석을 실행한다.",
188
+ }),
157
189
  },
158
190
  {
159
191
  id: "deep",
160
- label: chinese ? "深度:全部分析师 + 所选大师 + 交叉验证 + 辩论" : "Deep: every analyst + selected masters + verification + debate",
192
+ label: copy({
193
+ en: "Deep: every analyst + selected masters + verification + debate",
194
+ zh: "深度:全部分析师 + 所选大师 + 交叉验证 + 辩论",
195
+ ja: "詳細:全アナリスト + 選択したマスター + 交差検証 + 討論",
196
+ ko: "심층: 전체 분석가 + 선택한 마스터 + 교차 검증 + 토론",
197
+ }),
161
198
  analysts: allAnalysts.map((p) => p.id),
162
199
  master_selection: "required_1_to_N",
163
200
  suggested_masters_roster: "masters-core",
164
201
  verify: true,
165
202
  ...estimateSelectionRange({ analysts: allAnalysts.length, allMasters, verifiers: verifiers.length * 3 }),
166
- good_for: chinese
167
- ? "要下真钱的时候用。每条承重论断都会被回源、独立重算和反面检索"
168
- : "For a decision with real money behind it. Every load-bearing claim is re-sourced, re-derived and attacked.",
203
+ good_for: copy({
204
+ en: "For a decision with real money behind it. Every load-bearing claim is re-sourced, re-derived and attacked.",
205
+ zh: "要下真钱的时候用。每条承重论断都会被回源、独立重算和反面检索",
206
+ ja: "実資金を伴う判断向け。重要な主張をすべて原典確認、独立再計算、反証検索にかける。",
207
+ ko: "실제 자금이 걸린 판단용. 핵심 주장을 모두 원출처 확인, 독립 재계산, 반증 검색에 부친다.",
208
+ }),
169
209
  },
170
210
  ];
171
211
 
@@ -179,16 +219,31 @@ export function councilOptions({ language = "English" } = {}) {
179
219
  all_master_ids: masterChoices.map((m) => m.id),
180
220
  all_masters_count: allMasters,
181
221
  verifiers: verifiers.map((id) => ({ id, title: personaTitle(reg.get(id), language) })),
182
- how_to_ask: chinese ? [
183
- "每次新的委员会运行都先展示逐席名单,并要求用户选择至少 1 位大师;可以单选、任意多选或全选。",
184
- "用户已经点名时,把这些席位预选出来,但仍要展示名单并让用户提交本次选择。旧运行的选择不能自动复用。",
185
- "优先使用宿主原生多选;容纳不了完整名单时,展示固定编号表,并接受编号、稳定 ID all。",
186
- "选择提交本身就是确认。不要再追加第二个确认问题,但没有 selection receipt 就不能开始研究。",
187
- ] : [
188
- "Before every new council run, show the individual catalog and require at least one master selection. The user may pick one, any combination, or all.",
189
- "If the request already names masters, preselect them, but still show the catalog and require a submission for this run. Never reuse an old run's selection silently.",
190
- "Prefer a host-native multi-select. If it cannot hold the full catalog, show the stable numbered table and accept numbers, stable IDs, or all.",
191
- "Submitting the selection is the confirmation. Do not add another confirmation question, but do not start research without a selection receipt.",
192
- ],
222
+ how_to_ask: copy({
223
+ en: [
224
+ "Before every new council run, show the individual catalog and require at least one master selection. The user may pick one, any combination, or all.",
225
+ "If the request already names masters, preselect them, but still show the catalog and require a submission for this run. Never reuse an old run's selection silently.",
226
+ "Prefer a host-native multi-select. If it cannot hold the full catalog, show the stable numbered table and accept numbers, stable IDs, or all.",
227
+ "Submitting the selection is the confirmation. Do not add another confirmation question, but do not start research without a selection receipt.",
228
+ ],
229
+ zh: [
230
+ "每次新的委员会运行都先展示逐席名单,并要求用户选择至少 1 位大师;可以单选、任意多选或全选。",
231
+ "用户已经点名时,把这些席位预选出来,但仍要展示名单并让用户提交本次选择。旧运行的选择不能自动复用。",
232
+ "优先使用宿主原生多选;容纳不了完整名单时,展示固定编号表,并接受编号、稳定 ID 或 all。",
233
+ "选择提交本身就是确认。不要再追加第二个确认问题,但没有 selection receipt 就不能开始研究。",
234
+ ],
235
+ ja: [
236
+ "新しい委員会実行の前に必ず席ごとの一覧を表示し、少なくとも1つのマスター席を選んでもらう。単独、複数、全選択が可能。",
237
+ "依頼文でマスターが指定済みでも、事前選択として表示したうえで今回の選択送信を求める。以前の実行の選択を黙って再利用しない。",
238
+ "ホストの複数選択を優先し、一覧を収められない場合は固定番号表を示して番号、stable ID、allを受け付ける。",
239
+ "選択の送信自体を確定とする。二重確認は追加しないが、selection receiptなしで調査を開始しない。",
240
+ ],
241
+ ko: [
242
+ "새 위원회 실행 전에는 좌석별 목록을 모두 보여 주고 최소 1개 마스터 좌석을 선택받는다. 단일, 복수, 전체 선택이 가능하다.",
243
+ "요청에 마스터가 이미 지정되어 있어도 사전 선택으로 표시한 뒤 이번 실행의 선택 제출을 받아야 한다. 이전 실행의 선택을 조용히 재사용하지 않는다.",
244
+ "호스트의 다중 선택 기능을 우선하고, 전체 목록을 담을 수 없으면 고정 번호표를 보여 준 뒤 번호, stable ID 또는 all을 받는다.",
245
+ "선택 제출 자체가 확정이다. 두 번째 확인 질문은 추가하지 않지만 selection receipt 없이는 조사를 시작하지 않는다.",
246
+ ],
247
+ }),
193
248
  };
194
249
  }
@@ -5,6 +5,7 @@ import { COUNCIL_MODES, LIMITS, SELECTIONS_DIR } from "./constants.mjs";
5
5
  import { councilOptions } from "./council-options.mjs";
6
6
  import { invalidParams } from "./errors.mjs";
7
7
  import { readJson, writeJson } from "./fsutil.mjs";
8
+ import { resolveLanguage } from "./lang.mjs";
8
9
  import { safeSymbol } from "./run-store.mjs";
9
10
  import { cleanupSelectionStore } from "./selection-cleanup.mjs";
10
11
  import { ensureSelectionLockStore, withSelectionLock } from "./selection-locks.mjs";
@@ -12,11 +13,34 @@ import { ensureSelectionLockStore, withSelectionLock } from "./selection-locks.m
12
13
  const SELECTION_ID = /^SEL-[0-9a-f-]{36}$/i;
13
14
  const RECEIPT_ID = /^RCP-[0-9a-f-]{36}$/i;
14
15
  const QUICK_MASTER_MAX = 4;
16
+ const SUPPORTED_SELECTION_LANGUAGES = Object.freeze(["中文", "English", "日本語", "한국어"]);
15
17
 
16
18
  function digest(value) {
17
19
  return createHash("sha256").update(JSON.stringify(value)).digest("hex");
18
20
  }
19
21
 
22
+ function sameJson(left, right) {
23
+ return JSON.stringify(left) === JSON.stringify(right);
24
+ }
25
+
26
+ function receiptSelectionHash(receipt) {
27
+ return digest({
28
+ schema_version: receipt.schema_version,
29
+ selection_receipt: receipt.selection_receipt,
30
+ selection_id: receipt.selection_id,
31
+ symbol: receipt.symbol,
32
+ council_mode: receipt.council_mode,
33
+ catalog_hash: receipt.catalog_hash,
34
+ request_hash: receipt.request_hash,
35
+ intent_hash: receipt.intent_hash,
36
+ selected_master_ids: receipt.selected_master_ids,
37
+ selected_master_pack_hashes: receipt.selected_master_pack_hashes,
38
+ selection_mode: receipt.selection_mode,
39
+ created_at: receipt.created_at,
40
+ expires_at: receipt.expires_at,
41
+ });
42
+ }
43
+
20
44
  function councilMode(value) {
21
45
  const mode = value === undefined ? "full" : String(value);
22
46
  if (!COUNCIL_MODES.includes(mode)) {
@@ -27,6 +51,21 @@ function councilMode(value) {
27
51
  return mode;
28
52
  }
29
53
 
54
+ function selectionLanguage(args = {}) {
55
+ const language = resolveLanguage({ language: args.language, prompt: args.prompt });
56
+ if (!SUPPORTED_SELECTION_LANGUAGES.includes(language)) {
57
+ throw invalidParams(
58
+ `Selection language ${JSON.stringify(language)} is unsupported. Use zh-CN, en-US, ja-JP or ko-KR.`,
59
+ {
60
+ reason: "UNSUPPORTED_SELECTION_LANGUAGE",
61
+ requested_language: args.language ?? null,
62
+ supported_languages: ["zh-CN", "en-US", "ja-JP", "ko-KR"],
63
+ },
64
+ );
65
+ }
66
+ return language;
67
+ }
68
+
30
69
  function ensureStore() {
31
70
  ensureSelectionLockStore(SELECTIONS_DIR);
32
71
  }
@@ -76,7 +115,8 @@ function markExpired(record) {
76
115
  }
77
116
 
78
117
  export function catalogSnapshot(language = "English") {
79
- const options = councilOptions({ language });
118
+ const effectiveLanguage = selectionLanguage({ language });
119
+ const options = councilOptions({ language: effectiveLanguage });
80
120
  const catalog = {
81
121
  schema_version: 1,
82
122
  language: options.language,
@@ -94,8 +134,8 @@ export function beginCouncilSelection(args = {}, { now = Date.now() } = {}) {
94
134
  // performed before the new record exists and never follows symlinks or unknown names.
95
135
  cleanupSelectionStore({ selectionsDir: SELECTIONS_DIR, now });
96
136
  const symbol = safeSymbol(args.symbol);
97
- const language = String(args.language || "English");
98
137
  const prompt = typeof args.prompt === "string" ? args.prompt : "";
138
+ const language = selectionLanguage({ language: args.language, prompt });
99
139
  const mode = councilMode(args.council_mode);
100
140
  const catalog = catalogSnapshot(language);
101
141
  const preselected = args.preselected_master_ids === undefined
@@ -349,12 +389,6 @@ function confirmCouncilSelectionUnlocked(args = {}, { now = Date.now() } = {}) {
349
389
  catalog_hash: record.catalog_hash,
350
390
  request_hash: record.request_hash,
351
391
  intent_hash: record.intent_hash,
352
- selection_hash: digest({
353
- selection_id: record.selection_id,
354
- catalog_hash: record.catalog_hash,
355
- council_mode: record.council_mode || "full",
356
- selected_master_ids: record.selected_master_ids,
357
- }),
358
392
  selected_master_ids: record.selected_master_ids,
359
393
  selected_master_pack_hashes: selectedPackHashes,
360
394
  selection_mode: record.selection_mode,
@@ -363,6 +397,7 @@ function confirmCouncilSelectionUnlocked(args = {}, { now = Date.now() } = {}) {
363
397
  consumed_at: null,
364
398
  consumed_by_run_id: null,
365
399
  };
400
+ receiptRecord.selection_hash = receiptSelectionHash(receiptRecord);
366
401
  writeJson(selectionPath(record.selection_id), record);
367
402
  writeJson(receiptPath(receipt), receiptRecord);
368
403
  return confirmationResult(record);
@@ -388,6 +423,7 @@ function confirmationResult(record) {
388
423
  selection_receipt: record.selection_receipt,
389
424
  status: record.status,
390
425
  symbol: record.symbol,
426
+ language: record.language,
391
427
  council_mode: record.council_mode || "full",
392
428
  catalog_hash: record.catalog_hash,
393
429
  intent_hash: record.intent_hash,
@@ -406,7 +442,50 @@ function confirmationResult(record) {
406
442
  function consumeCouncilSelectionUnlocked(args = {}, { now = Date.now() } = {}) {
407
443
  ensureStore();
408
444
  const receipt = readReceipt(args.selection_receipt);
445
+ if (receipt.selection_receipt !== args.selection_receipt) {
446
+ throw invalidParams("The selection receipt file does not match its requested receipt id.", {
447
+ reason: "MASTER_SELECTION_RECORD_MISMATCH",
448
+ mismatched_fields: ["selection_receipt"],
449
+ });
450
+ }
409
451
  const selection = readSelection(receipt.selection_id);
452
+ const expectedPackHashes = Object.fromEntries(
453
+ selection.catalog.masters
454
+ .filter((master) => selection.selected_master_ids.includes(master.id))
455
+ .map((master) => [master.id, master.pack_hash]),
456
+ );
457
+ const recordBindings = [
458
+ ["schema_version", receipt.schema_version, selection.schema_version],
459
+ ["selection_id", receipt.selection_id, selection.selection_id],
460
+ ["selection_receipt", receipt.selection_receipt, selection.selection_receipt],
461
+ ["symbol", receipt.symbol, selection.symbol],
462
+ ["council_mode", receipt.council_mode || "full", selection.council_mode || "full"],
463
+ ["catalog_hash", receipt.catalog_hash, selection.catalog_hash],
464
+ ["request_hash", receipt.request_hash, selection.request_hash],
465
+ ["intent_hash", receipt.intent_hash, selection.intent_hash],
466
+ ["selected_master_ids", receipt.selected_master_ids, selection.selected_master_ids],
467
+ ["selection_mode", receipt.selection_mode, selection.selection_mode],
468
+ ["created_at", receipt.created_at, selection.confirmed_at],
469
+ ["expires_at", receipt.expires_at, selection.expires_at],
470
+ ];
471
+ const mismatchedBindings = recordBindings
472
+ .filter(([, receiptValue, selectionValue]) => !sameJson(receiptValue, selectionValue))
473
+ .map(([field]) => field);
474
+ if (mismatchedBindings.length) {
475
+ throw invalidParams("The selection receipt does not match its confirmed selection record.", {
476
+ reason: "MASTER_SELECTION_RECORD_MISMATCH",
477
+ mismatched_fields: mismatchedBindings,
478
+ });
479
+ }
480
+ if (!sameJson(receipt.selected_master_pack_hashes, expectedPackHashes)) {
481
+ throw invalidParams("The selected persona pack hashes do not match the confirmed selection record.", {
482
+ reason: "MASTER_SELECTION_PACK_HASH_MISMATCH",
483
+ mismatched_master_ids: [...new Set([
484
+ ...selection.selected_master_ids,
485
+ ...Object.keys(receipt.selected_master_pack_hashes || {}),
486
+ ])].filter((id) => receipt.selected_master_pack_hashes?.[id] !== expectedPackHashes[id]),
487
+ });
488
+ }
410
489
  const symbol = safeSymbol(args.symbol);
411
490
  if (receipt.symbol !== symbol || selection.symbol !== symbol) {
412
491
  throw invalidParams(`Selection receipt is for ${receipt.symbol}, not ${symbol}.`, {
@@ -434,8 +513,15 @@ function consumeCouncilSelectionUnlocked(args = {}, { now = Date.now() } = {}) {
434
513
  mismatched_master_ids: mismatchedPackHashes,
435
514
  });
436
515
  }
437
- const language = String(args.language || "English");
516
+ const expectedSelectionHash = receiptSelectionHash(receipt);
517
+ if (receipt.selection_hash !== expectedSelectionHash) {
518
+ throw invalidParams("The selection receipt hash is invalid.", {
519
+ reason: "MASTER_SELECTION_HASH_MISMATCH",
520
+ expected_selection_hash: expectedSelectionHash,
521
+ });
522
+ }
438
523
  const prompt = typeof args.prompt === "string" ? args.prompt : "";
524
+ const language = selectionLanguage({ language: args.language, prompt });
439
525
  const mode = councilMode(args.council_mode);
440
526
  if ((receipt.council_mode || "full") !== mode || (selection.council_mode || "full") !== mode) {
441
527
  throw invalidParams("The selection receipt belongs to a different council mode.", {