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/rpc.mjs CHANGED
@@ -4,7 +4,7 @@ import readline from "node:readline";
4
4
  import { COUNCIL_MODES, LIMITS, MASTER_STANCES, OUTPUT_MODES, QUICK_TASKS, SERVER_NAME, VERSION } from "./constants.mjs";
5
5
  import { RpcCode, methodNotFound, invalidParams, toRpcError } from "./errors.mjs";
6
6
  import { readJson, readJsonl, writeJson } from "./fsutil.mjs";
7
- import { resolveLanguage } from "./lang.mjs";
7
+ import { localized, resolveLanguage } from "./lang.mjs";
8
8
  import { sweepStaleOutputs } from "./codex.mjs";
9
9
  import { completenessStatus, sourceManifest } from "./gates.mjs";
10
10
  import { artifactPaths, existingDebate, runId, runPath, safeSymbol, saveRun } from "./run-store.mjs";
@@ -76,7 +76,14 @@ export function recordAck(run, extra = {}) {
76
76
  };
77
77
  }
78
78
 
79
- function renderSelectionCatalog(data, zh) {
79
+ function renderSelectionCatalog(data) {
80
+ const copy = (messages) => localized(data.language, messages);
81
+ const labels = copy({
82
+ en: { identity: "Identity", method: "Method", bestFor: "Best for", maturity: "Maturity", pack: "Pack format", preselected: "preselected" },
83
+ zh: { identity: "身份", method: "方法", bestFor: "适合", maturity: "成熟度", pack: "物理格式", preselected: "已预选" },
84
+ ja: { identity: "人物像", method: "手法", bestFor: "適した対象", maturity: "成熟度", pack: "パック形式", preselected: "事前選択済み" },
85
+ ko: { identity: "정체성", method: "방법", bestFor: "적합 대상", maturity: "성숙도", pack: "팩 형식", preselected: "사전 선택" },
86
+ });
80
87
  const preselected = new Set(data.preselected_master_ids || []);
81
88
  // Some MCP hosts expose only text content even when the server also returns
82
89
  // structuredContent. Keep the selection handshake usable on those hosts by
@@ -91,31 +98,45 @@ function renderSelectionCatalog(data, zh) {
91
98
  council_mode: data.council_mode,
92
99
  })}`;
93
100
  const cards = data.masters.map((master) => [
94
- `${master.index}. ${master.title} [${master.id}]${preselected.has(master.id) ? (zh ? " [已预选]" : " [preselected]") : ""}`,
95
- `${zh ? "身份" : "Identity"}: ${master.identity}`,
96
- `${zh ? "方法" : "Method"}: ${master.method}`,
97
- `${zh ? "适合" : "Best for"}: ${master.best_for}`,
98
- `${zh ? "成熟度" : "Maturity"}: ${master.maturity}`,
99
- `${zh ? "物理格式" : "Pack format"}: ${master.pack_format} (${master.admission_level})`,
101
+ `${master.index}. ${master.title} [${master.id}]${preselected.has(master.id) ? ` [${labels.preselected}]` : ""}`,
102
+ `${labels.identity}: ${master.identity}`,
103
+ `${labels.method}: ${master.method}`,
104
+ `${labels.bestFor}: ${master.best_for}`,
105
+ `${labels.maturity}: ${master.maturity_label} (${master.maturity})`,
106
+ `${labels.pack}: ${master.pack_format} (${master.admission_level})`,
100
107
  ].join("\n ")).join("\n\n");
101
108
  const quick = data.council_mode === "quick";
102
- const instructions = zh
103
- ? quick
104
- ? `Quick 模式请选择 1 ${data.maximum} 位大师。回复编号、范围或稳定 ID,例如:1 / 1,3,8 / 1-4 / master_buffett;不支持 all 全选。`
105
- : `请选择 1 至 ${data.maximum} 位大师。回复编号、范围、稳定 ID,或 all 全选,例如:1 / 1,3,8 / 1-5 / master_buffett / all。`
106
- : quick
107
- ? `Quick mode: choose 1 to ${data.maximum} masters. Submit numbers, ranges, or stable IDs, for example: 1 / 1,3,8 / 1-4 / master_buffett. Selecting all is not supported.`
108
- : `Choose 1 to ${data.maximum} masters. Submit numbers, ranges, stable IDs, or all, for example: 1 / 1,3,8 / 1-5 / master_buffett / all.`;
109
+ const instructions = quick
110
+ ? copy({
111
+ en: `Quick mode: choose 1 to ${data.maximum} masters. Submit numbers, ranges, or stable IDs, for example: 1 / 1,3,8 / 1-4 / master_buffett. Selecting all is not supported.`,
112
+ zh: `Quick 模式请选择 1 至 ${data.maximum} 位大师。回复编号、范围或稳定 ID,例如:1 / 1,3,8 / 1-4 / master_buffett;不支持 all 全选。`,
113
+ ja: `Quick モードでは1席から${data.maximum}席を選んでください。番号、範囲、stable ID(例:1 / 1,3,8 / 1-4 / master_buffett)を送信してください。all は使用できません。`,
114
+ ko: `Quick 모드에서는 1개에서 ${data.maximum} 마스터 좌석을 선택하십시오. 번호, 범위 또는 stable ID(예: 1 / 1,3,8 / 1-4 / master_buffett)를 제출하십시오. all 지원하지 않습니다.`,
115
+ })
116
+ : copy({
117
+ en: `Choose 1 to ${data.maximum} masters. Submit numbers, ranges, stable IDs, or all, for example: 1 / 1,3,8 / 1-5 / master_buffett / all.`,
118
+ zh: `请选择 1 至 ${data.maximum} 位大师。回复编号、范围、稳定 ID,或 all 全选,例如:1 / 1,3,8 / 1-5 / master_buffett / all。`,
119
+ ja: `1席から${data.maximum}席のマスターを選んでください。番号、範囲、stable ID、または all(例:1 / 1,3,8 / 1-5 / master_buffett / all)を送信してください。`,
120
+ ko: `1개에서 ${data.maximum}개 마스터 좌석을 선택하십시오. 번호, 범위, stable ID 또는 all(예: 1 / 1,3,8 / 1-5 / master_buffett / all)을 제출하십시오.`,
121
+ });
109
122
  return [
110
- zh
111
- ? `大师选择(目录共 ${data.masters.length} 席;最多选择 ${data.maximum} 席)`
112
- : `Master selection (${data.masters.length} in catalog; choose up to ${data.maximum})`,
123
+ copy({
124
+ en: `Master selection (${data.masters.length} in catalog; choose up to ${data.maximum})`,
125
+ zh: `大师选择(目录共 ${data.masters.length} 席;最多选择 ${data.maximum} 席)`,
126
+ ja: `マスター選択(全${data.masters.length}席、最大${data.maximum}席まで)`,
127
+ ko: `마스터 선택(전체 ${data.masters.length}개 좌석, 최대 ${data.maximum}개 선택)`,
128
+ }),
113
129
  fallbackContext,
114
130
  "",
115
131
  cards,
116
132
  "",
117
133
  instructions,
118
- zh ? "提交选择后才会开始研究。" : "Research starts only after this selection is submitted.",
134
+ copy({
135
+ en: "Research starts only after this selection is submitted.",
136
+ zh: "提交选择后才会开始研究。",
137
+ ja: "選択を送信するまで調査は開始されません。",
138
+ ko: "선택을 제출하기 전에는 조사를 시작하지 않습니다.",
139
+ }),
119
140
  ].join("\n");
120
141
  }
121
142
 
@@ -176,6 +197,12 @@ function startValidation(args, entryTool) {
176
197
  maximum_ms: LIMITS.QUICK_HARD_MAX_MS,
177
198
  });
178
199
  }
200
+ if (mode === "full" && args.total_timeout_ms > LIMITS.FULL_HARD_MAX_MS) {
201
+ throw invalidParams(`Full council total_timeout_ms cannot exceed ${LIMITS.FULL_HARD_MAX_MS}.`, {
202
+ reason: "FULL_TOTAL_TIMEOUT_EXCEEDS_MAX",
203
+ maximum_ms: LIMITS.FULL_HARD_MAX_MS,
204
+ });
205
+ }
179
206
  if (mode === "quick" && args.synthesis === false) {
180
207
  throw invalidParams("Quick council requires its one-round bull/bear and portfolio-manager synthesis.", {
181
208
  reason: "QUICK_SYNTHESIS_REQUIRED",
@@ -230,7 +257,9 @@ function selectedRunArgs(args = {}, entryTool) {
230
257
  selection_receipt: args.selection_receipt,
231
258
  symbol,
232
259
  run_id: id,
233
- language: args.language || "English",
260
+ // Preserve omission so consumeCouncilSelection can infer the same locale
261
+ // from the prompt that begin_council_selection bound into the receipt.
262
+ language: args.language,
234
263
  prompt: typeof args.prompt === "string" ? args.prompt : "",
235
264
  council_mode: args.council_mode || "full",
236
265
  });
@@ -373,9 +402,8 @@ export function tools() {
373
402
  synthesis_timeout_ms: { type: "number", default: LIMITS.CODEX_TIMEOUT_MS },
374
403
  total_timeout_ms: {
375
404
  type: "number",
376
- default: LIMITS.QUICK_TOTAL_MS,
377
- maximum: LIMITS.QUICK_HARD_MAX_MS,
378
- description: "Hard end-to-end wall-clock budget for council_mode=quick, including grounding wait, retries and synthesis. It may be lowered but never raised above ten minutes. Full mode has no implicit global deadline.",
405
+ maximum: LIMITS.FULL_HARD_MAX_MS,
406
+ description: "Hard queue-to-persistence wall-clock budget. quick defaults to and cannot exceed ten minutes; full defaults to and cannot exceed thirty minutes. Callers may lower the applicable ceiling, never raise it.",
379
407
  },
380
408
  output_mode: { type: "string", enum: OUTPUT_MODES, default: "public_equity", description: "Final synthesis target shape." },
381
409
  selection_receipt: { type: "string", description: "One-run receipt returned by confirm_master_selection. Required for every council run and consumed exactly once." },
@@ -435,7 +463,7 @@ export function tools() {
435
463
  },
436
464
  required: ["symbol", "selection_receipt"],
437
465
  }),
438
- 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.", {
466
+ tool("record_visible_packet", "MANDATORY sequential step (not optional): record one completed visible evidence agent packet into a planned visible run. Every reader-facing field must use the run language or the packet is rejected without changing run state. Every planned evidence task MUST be recorded before debate completion and portfolio_manager.", {
439
467
  type: "object",
440
468
  properties: {
441
469
  run_id: { type: "string" },
@@ -446,16 +474,27 @@ export function tools() {
446
474
  },
447
475
  required: ["run_id", "task", "packet"],
448
476
  }),
449
- 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.", {
477
+ tool("record_visible_decision", "Record exactly one visible decision step. Full visible bull_researcher and bear_researcher calls MUST supply round=1, then round=2, then round=3; both sides of the prior round are required before advancing. Round 2 asks exactly three questions; Round 3 preserves its own questions and answers the opponent's questions with exact bindings. Packets are persisted by role+round: an identical replay is idempotent and conflicting content is rejected. portfolio_manager accepts no round and is rejected until all evidence, selected masters, both three-round sides, and the exact Q&A gate are complete. Every reader-facing field must use the run language.", {
450
478
  type: "object",
451
479
  properties: {
452
480
  run_id: { type: "string" },
453
481
  role: { type: "string", enum: debateIds },
482
+ round: { type: "integer", minimum: 1, maximum: 3, description: "Required for bull_researcher and bear_researcher. Full visible runs require 1, 2, and 3 in order. Omit for portfolio_manager." },
454
483
  packet: { type: "object" },
455
484
  thread_id: { type: "string" },
456
485
  thread_title: { type: "string" },
457
486
  },
458
487
  required: ["run_id", "role", "packet"],
488
+ allOf: [
489
+ {
490
+ if: { properties: { role: { enum: ["bull_researcher", "bear_researcher"] } }, required: ["role"] },
491
+ then: { required: ["round"] },
492
+ },
493
+ {
494
+ if: { properties: { role: { const: "portfolio_manager" } }, required: ["role"] },
495
+ then: { not: { required: ["round"] } },
496
+ },
497
+ ],
459
498
  }),
460
499
  tool("collect_evidence", "Launch Codex subagents and save shared JSON evidence packets. Use dry_run=true only for planning/self-tests.", {
461
500
  type: "object",
@@ -504,7 +543,7 @@ export function tools() {
504
543
  type: "object",
505
544
  properties: { language: common.language },
506
545
  }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }),
507
- tool("record_master_opinion", "Record one completed selected 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 recorded conclusion rather than an abstention. Every selected seat must either report or be settled deterministically before the run can be complete.", {
546
+ tool("record_master_opinion", "Record one completed selected master-seat opinion into a planned visible run. Every reader-facing field must use the run language or the opinion is rejected without changing run state. 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 recorded conclusion rather than an abstention. Every selected seat must either report or be settled deterministically before the run can be complete.", {
508
547
  type: "object",
509
548
  properties: {
510
549
  run_id: { type: "string" },
@@ -692,8 +731,7 @@ export async function handleToolCall(id, params) {
692
731
  const args = params?.arguments || {};
693
732
  if (name === "begin_council_selection") {
694
733
  const data = beginCouncilSelection(args);
695
- const zh = /中文|chinese|zh/i.test(String(data.language || ""));
696
- sendResult(id, jsonContent(renderSelectionCatalog(data, zh), data));
734
+ sendResult(id, jsonContent(renderSelectionCatalog(data), data));
697
735
  return;
698
736
  }
699
737
  if (name === "confirm_master_selection") {
@@ -705,10 +743,13 @@ export async function handleToolCall(id, params) {
705
743
  intent_hash: data.intent_hash,
706
744
  council_mode: data.council_mode,
707
745
  })}`;
708
- sendResult(id, jsonContent(
709
- `${fallbackContext}\nConfirmed ${data.selected_count} master seat(s) for ${data.symbol}. Use the one-time selection_receipt to start this run.`,
710
- data,
711
- ));
746
+ const confirmation = localized(data.language, {
747
+ en: `Confirmed ${data.selected_count} master seat(s) for ${data.symbol}. Use the one-time selection_receipt to start this run.`,
748
+ zh: `已为 ${data.symbol} 确认 ${data.selected_count} 个大师席位。请使用一次性 selection_receipt 启动本轮运行。`,
749
+ ja: `${data.symbol} について ${data.selected_count} 席のマスターを確定しました。今回の実行を開始するには1回限りの selection_receipt を使用してください。`,
750
+ ko: `${data.symbol}에 대해 마스터 ${data.selected_count}개 좌석을 확정했습니다. 이번 실행을 시작하려면 일회용 selection_receipt를 사용하십시오.`,
751
+ });
752
+ sendResult(id, jsonContent(`${fallbackContext}\n${confirmation}`, data));
712
753
  return;
713
754
  }
714
755
  if (name === "plan_visible_run") {
@@ -766,11 +807,12 @@ export async function handleToolCall(id, params) {
766
807
  if (name === "record_visible_decision") {
767
808
  const result = recordVisibleDecision(args);
768
809
  sendResult(id, jsonContent(
769
- `Recorded visible decision ${args.role} for ${result.run.symbol}: ${result.run.run_id}`,
810
+ `Recorded visible decision ${args.role}${args.round ? ` round ${args.round}` : ""} for ${result.run.symbol}: ${result.run.run_id}`,
770
811
  // decision and opinion are small and are what a caller reads back; only the full
771
812
  // run object is dropped.
772
813
  recordAck(result.run, {
773
814
  decision: result.decision,
815
+ idempotent_replay: result.idempotent_replay === true,
774
816
  report_quality: result.report_quality?.status,
775
817
  missing_report_items: result.report_quality?.missing || [],
776
818
  }),
@@ -54,6 +54,13 @@ export function statusSnapshot(run) {
54
54
  const recordedMasters = (run.master_opinions || []).map((opinion) => opinion.master);
55
55
  const recordedSet = new Set(recordedMasters);
56
56
  const pendingMasters = selectedMasters.filter((master) => !recordedSet.has(master));
57
+ const visibleDebate = run.execution_mode === "visible_host_threads" ? run.visible_debate : null;
58
+ const visibleDebateRounds = visibleDebate
59
+ ? Object.fromEntries(["bull_researcher", "bear_researcher"].map((role) => [
60
+ role,
61
+ Object.keys(visibleDebate.rounds?.[role] || {}).map(Number).filter(Number.isInteger).sort((a, b) => a - b),
62
+ ]))
63
+ : null;
57
64
  return {
58
65
  run_id: run.run_id,
59
66
  symbol: run.symbol,
@@ -62,13 +69,29 @@ export function statusSnapshot(run) {
62
69
  execution_mode: run.execution_mode,
63
70
  council_mode: run.council_mode || "full",
64
71
  debate_format: run.debate_format || "three_round_cross_exam",
72
+ visible_debate_contract: visibleDebate?.contract || null,
73
+ visible_debate_rounds_expected: visibleDebate?.rounds_expected || null,
74
+ visible_debate_rounds_recorded: visibleDebateRounds,
75
+ visible_debate_qna_gate: visibleDebate?.qna_gate?.status || null,
65
76
  report_contract: run.council_mode === "quick" ? "quick_v1" : "full_v2",
66
77
  full_council_equivalent: run.council_mode !== "quick",
78
+ master_worker_contract: run.execution_mode === "background_codex_exec"
79
+ ? "one_isolated_worker_per_selected_method_v1"
80
+ : run.execution_mode === "dry_run"
81
+ ? "planned_not_executed"
82
+ : "host_managed_not_plugin_enforced",
83
+ deadline_enforced: run.deadline_enforced === true,
67
84
  time_budget_ms: run.time_budget_ms || null,
68
85
  deadline_at: run.deadline_at || null,
69
86
  remaining_budget_ms: run.deadline_at
70
87
  ? Math.max(0, Date.parse(run.deadline_at) - Date.now())
71
88
  : null,
89
+ elapsed_ms: run.started_at
90
+ ? Math.max(0, Date.parse(run.completed_at || new Date().toISOString()) - Date.parse(run.started_at))
91
+ : null,
92
+ deadline_met: run.deadline_at && run.completed_at
93
+ ? Date.parse(run.completed_at) <= Date.parse(run.deadline_at)
94
+ : null,
72
95
  visibility_required: run.visibility_required,
73
96
  dry_run: run.dry_run,
74
97
  status: run.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alphacouncil-agent",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Multi-agent public-equity research workflow plugin for Codex & Claude Code: sourced evidence packets, bull/bear debate, and a portfolio-manager Buy/Overweight/Hold/Underweight/Sell decision.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "check": "node scripts/selfcheck.mjs",
34
34
  "test": "node scripts/run-tests.mjs",
35
35
  "test:source": "node --test",
36
- "test:package": "node scripts/package-smoke.mjs",
36
+ "test:package": "node scripts/check-packaged-host-parity.mjs --check",
37
37
  "sync": "node scripts/sync-personas.mjs",
38
38
  "persona:corpus": "node scripts/report-persona-corpus-gaps.mjs",
39
39
  "persona:build-specs": "node scripts/report-persona-v3-build-specs.mjs",
@@ -19,6 +19,31 @@
19
19
  "components"
20
20
  ],
21
21
  "additionalProperties": false,
22
+ "allOf": [
23
+ {
24
+ "if": {
25
+ "required": ["build_profile"],
26
+ "properties": { "build_profile": { "const": "solo_test" } }
27
+ },
28
+ "then": {
29
+ "properties": {
30
+ "identity": {
31
+ "properties": {
32
+ "public_label": { "$ref": "#/$defs/localizedTextFourLocales" },
33
+ "operator_label": { "$ref": "#/$defs/localizedTextFourLocales" }
34
+ }
35
+ },
36
+ "selection": {
37
+ "properties": {
38
+ "identity": { "$ref": "#/$defs/localizedTextFourLocales" },
39
+ "method": { "$ref": "#/$defs/localizedTextFourLocales" },
40
+ "best_for": { "$ref": "#/$defs/localizedTextFourLocales" }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ],
22
47
  "properties": {
23
48
  "schema_version": { "const": 3 },
24
49
  "build_profile": {
@@ -38,7 +63,9 @@
38
63
  "additionalProperties": false,
39
64
  "properties": {
40
65
  "en": { "type": "string", "minLength": 3 },
41
- "zh": { "type": "string", "minLength": 2 }
66
+ "zh": { "type": "string", "minLength": 2 },
67
+ "ja": { "type": "string", "minLength": 2 },
68
+ "ko": { "type": "string", "minLength": 2 }
42
69
  }
43
70
  },
44
71
  "operator_label": {
@@ -192,9 +219,17 @@
192
219
  "additionalProperties": false,
193
220
  "properties": {
194
221
  "en": { "type": "string", "minLength": 3 },
195
- "zh": { "type": "string", "minLength": 2 }
222
+ "zh": { "type": "string", "minLength": 2 },
223
+ "ja": { "type": "string", "minLength": 2 },
224
+ "ko": { "type": "string", "minLength": 2 }
196
225
  }
197
226
  },
227
+ "localizedTextFourLocales": {
228
+ "allOf": [
229
+ { "$ref": "#/$defs/localizedText" },
230
+ { "required": ["en", "zh", "ja", "ko"] }
231
+ ]
232
+ },
198
233
  "strings": {
199
234
  "type": "array",
200
235
  "items": { "type": "string", "minLength": 1 },
@@ -29,7 +29,12 @@ import { isDeepStrictEqual } from "node:util";
29
29
  const HERE = dirname(fileURLToPath(import.meta.url));
30
30
  export const PACKAGED_PARITY_REPO_ROOT = resolve(HERE, "../..");
31
31
  export const PACKAGED_HOST_IDS = Object.freeze(["claude_code", "codex", "opencode", "grok"]);
32
- export const PACKAGED_SELECTION_INPUT = "2,23-24,26";
32
+ export const PACKAGED_SELECTION_INPUT = Object.freeze([
33
+ "master_buffett",
34
+ "master_damodaran",
35
+ "master_taleb",
36
+ "master_sinclair",
37
+ ]);
33
38
 
34
39
  const TEMP_PREFIX = "alphacouncil-packaged-parity-";
35
40
  const SYMBOL = "NOK";
@@ -39,6 +44,13 @@ const PROMPT = "packaged adapter parity fixture";
39
44
  const TASKS = Object.freeze(["market_data"]);
40
45
  const REQUEST_TIMEOUT_MS = 30_000;
41
46
  const PROCESS_TIMEOUT_MS = 120_000;
47
+ const EXPECTED_TOOL_COUNT = 31;
48
+ const INSTALLED_LOCALE_CASES = Object.freeze([
49
+ Object.freeze({ language: "en-US", prompt: "installed package locale smoke", key: "en", script: /[A-Za-z]/u }),
50
+ Object.freeze({ language: "zh-CN", prompt: "安装包语言烟雾测试", key: "zh", script: /\p{Script=Han}/u }),
51
+ Object.freeze({ language: "ja-JP", prompt: "インストール済みパッケージの言語スモークテスト", key: "ja", script: /[\p{Script=Hiragana}\p{Script=Katakana}]/u }),
52
+ Object.freeze({ language: "ko-KR", prompt: "설치 패키지 언어 스모크 테스트", key: "ko", script: /\p{Script=Hangul}/u }),
53
+ ]);
42
54
  const REQUIRED_PACKAGE_FILES = Object.freeze([
43
55
  "mcp/server.mjs",
44
56
  "data/host-capabilities.v1.json",
@@ -575,16 +587,28 @@ function assertCompleteCatalog(opened, text, hostId) {
575
587
  for (const [offset, master] of opened.masters.entries()) {
576
588
  if (master.index !== offset + 1 || seen.has(master.id)) fail(`${hostId}: catalog order or IDs are invalid`);
577
589
  seen.add(master.id);
578
- for (const field of ["identity", "method", "best_for", "maturity", "pack_hash"]) {
590
+ for (const field of ["identity", "method", "best_for", "maturity", "maturity_label", "pack_hash"]) {
579
591
  if (typeof master[field] !== "string" || !master[field]) fail(`${hostId}: catalog seat ${master.id} lacks ${field}`);
580
592
  }
581
593
  if (!/^sha256:[a-f0-9]{64}$/u.test(master.pack_hash)) fail(`${hostId}: ${master.id} pack hash is invalid`);
582
- for (const visible of [`${master.index}. `, `[${master.id}]`, master.identity, master.method, master.best_for, master.maturity]) {
594
+ for (const visible of [`${master.index}. `, `[${master.id}]`, master.identity, master.method, master.best_for, master.maturity, master.maturity_label]) {
583
595
  if (!text.includes(visible)) fail(`${hostId}: returned text did not display ${master.id} field ${visible}`);
584
596
  }
585
597
  }
586
598
  }
587
599
 
600
+ function assertLocalizedCatalog(opened, text, locale) {
601
+ assertCompleteCatalog(opened, text, `installed-${locale.key}`);
602
+ for (const master of opened.masters) {
603
+ for (const field of ["identity", "method", "best_for", "maturity_label"]) {
604
+ if (!locale.script.test(master[field])) fail(`installed-${locale.key}: ${master.id}.${field} lacks requested-locale prose`);
605
+ if (locale.key !== "en" && /\b[a-z]+(?:_[a-z0-9]+)+\b/u.test(master[field])) {
606
+ fail(`installed-${locale.key}: ${master.id}.${field} leaked a machine domain id into reader-facing prose`);
607
+ }
608
+ }
609
+ }
610
+ }
611
+
588
612
  function normalizedReceiptBinding(record) {
589
613
  return {
590
614
  symbol: record.symbol,
@@ -624,7 +648,27 @@ async function executePackagedAdapters({ pack, surfaces, tempRoot }) {
624
648
  networkSentinel,
625
649
  });
626
650
  await server.request("initialize", {});
627
- sessions.push({ hostId, dataDir, server });
651
+ const listed = await server.request("tools/list", {});
652
+ const tools = listed.result?.tools || [];
653
+ if (tools.length !== EXPECTED_TOOL_COUNT) fail(`${hostId}: installed server exposed ${tools.length} tools, expected ${EXPECTED_TOOL_COUNT}`);
654
+ sessions.push({ hostId, dataDir, server, toolCount: tools.length });
655
+ }
656
+
657
+ const localeCases = process.env.ALPHACOUNCIL_PERSONA_BUILD_PROFILE === "production"
658
+ ? INSTALLED_LOCALE_CASES.slice(0, 1)
659
+ : INSTALLED_LOCALE_CASES;
660
+ const localeCatalogHashes = {};
661
+ for (const locale of localeCases) {
662
+ const response = await sessions[0].server.callTool("begin_council_selection", {
663
+ symbol: SYMBOL,
664
+ as_of: AS_OF,
665
+ language: locale.language,
666
+ prompt: locale.prompt,
667
+ host: "installed-package-locale-smoke",
668
+ });
669
+ const opened = structured(response, `installed ${locale.language} begin_council_selection`);
670
+ assertLocalizedCatalog(opened, response.result.content?.[0]?.text || "", locale);
671
+ localeCatalogHashes[locale.key] = opened.catalog_hash;
628
672
  }
629
673
 
630
674
  for (const session of sessions) {
@@ -642,7 +686,7 @@ async function executePackagedAdapters({ pack, surfaces, tempRoot }) {
642
686
  selection_id: opened.selection_id,
643
687
  catalog_hash: opened.catalog_hash,
644
688
  display_ack: true,
645
- selection: PACKAGED_SELECTION_INPUT,
689
+ selected_master_ids: PACKAGED_SELECTION_INPUT,
646
690
  }), `${session.hostId} confirm_master_selection`);
647
691
  session.opened = opened;
648
692
  session.confirmed = confirmed;
@@ -744,11 +788,15 @@ async function executePackagedAdapters({ pack, surfaces, tempRoot }) {
744
788
  evidence_scope: "installed_npm_tarball_mcp_stdio_adapters_only",
745
789
  host_order: PACKAGED_HOST_IDS,
746
790
  host_count: sessions.length,
791
+ tool_count: EXPECTED_TOOL_COUNT,
792
+ locale_count: localeCases.length,
793
+ locale_catalog_hashes: localeCatalogHashes,
747
794
  catalog_count: baseline.catalog.length,
748
795
  catalog_hash: baseline.catalog_hash,
749
796
  catalog_order_hash: sha256(baseline.catalog),
750
797
  maturity_counts: maturityCounts,
751
798
  selection_input: PACKAGED_SELECTION_INPUT,
799
+ selection_input_type: "stable_ids",
752
800
  selected_master_ids: baseline.selected_master_ids,
753
801
  selected_master_pack_hashes: baseline.selected_master_pack_hashes,
754
802
  receipt_binding_hash: sha256(baseline.receipt_binding),
@@ -774,6 +822,7 @@ async function executePackagedAdapters({ pack, surfaces, tempRoot }) {
774
822
  host_id: session.hostId,
775
823
  server_entry: "mcp/server.mjs",
776
824
  adapter_status: "passed",
825
+ tool_count: session.toolCount,
777
826
  catalog_count: session.catalog.length,
778
827
  catalog_hash: session.opened.catalog_hash,
779
828
  selected_master_ids: session.confirmed.selected_master_ids,
@@ -21,6 +21,11 @@ import { fileURLToPath } from "node:url";
21
21
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
22
22
 
23
23
  import buildInventory from "../../data/persona-v3-build-specs.v1.mjs";
24
+ import {
25
+ selectorBestForLocale,
26
+ selectorMethodLocale,
27
+ } from "../../data/master-selector-method-locales.v1.mjs";
28
+ import { selectorCard } from "../../mcp/lib/master-catalog.mjs";
24
29
  import { inspectPersonaAdmission } from "../../mcp/lib/personas-v3/admission.mjs";
25
30
  import {
26
31
  canonicalValue,
@@ -381,14 +386,26 @@ function buildDocuments({ seat, blueprint, rawTools, packVersion, formulaManifes
381
386
  if (policyErrors.length) fail(`${seat.persona_id}: generated deterministic policy is invalid`, { errors: policyErrors });
382
387
 
383
388
  const publicTitle = blueprint.canonical_title;
389
+ const selectorPersona = { id: seat.persona_id, title: publicTitle };
390
+ const chineseMethod = selectorCard(selectorPersona, "zh-CN").method;
384
391
  const manifest = canonicalValue({
385
392
  schema_version: 3,
386
393
  build_profile: "solo_test",
387
394
  pack_version: packVersion,
388
395
  identity: {
389
396
  persona_id: seat.persona_id,
390
- public_label: { en: `${publicTitle.en} Solo Test`, zh: `${publicTitle.zh}单人测试` },
391
- operator_label: { en: `${publicTitle.en} Provisional Operator Lens`, zh: `${publicTitle.zh}临时操作视角` },
397
+ public_label: {
398
+ en: `${publicTitle.en} Solo Test`,
399
+ zh: `${publicTitle.zh}单人测试`,
400
+ ja: `${publicTitle.en} 単独テスト`,
401
+ ko: `${publicTitle.en} 단독 테스트`,
402
+ },
403
+ operator_label: {
404
+ en: `${publicTitle.en} Provisional Operator Lens`,
405
+ zh: `${publicTitle.zh}临时操作视角`,
406
+ ja: `${publicTitle.en} 暫定オペレーター・レンズ`,
407
+ ko: `${publicTitle.en} 임시 오퍼레이터 렌즈`,
408
+ },
392
409
  maturity: "operator_lens",
393
410
  source_cutoff: SOURCE_DATE,
394
411
  },
@@ -396,11 +413,20 @@ function buildDocuments({ seat, blueprint, rawTools, packVersion, formulaManifes
396
413
  identity: {
397
414
  en: `${publicTitle.en}; project-derived and not human reviewed`,
398
415
  zh: `${publicTitle.zh};项目派生、未经人工审定`,
416
+ ja: `${publicTitle.en}。プロジェクト派生で、方法帰属について人による審査を受けていない暫定オペレーター・レンズ。本人の発言や現在の見解ではない。`,
417
+ ko: `${publicTitle.en}. 프로젝트에서 파생되었고 방법 귀속에 대한 인적 심사를 거치지 않은 임시 오퍼레이터 렌즈다. 본인의 발언이나 현재 견해가 아니다.`,
418
+ },
419
+ method: {
420
+ en: seat.method_scope.planning_hypothesis,
421
+ zh: chineseMethod,
422
+ ja: selectorMethodLocale(seat.persona_id, "ja"),
423
+ ko: selectorMethodLocale(seat.persona_id, "ko"),
399
424
  },
400
- method: { en: seat.method_scope.planning_hypothesis, zh: seat.method_scope.planning_hypothesis },
401
425
  best_for: {
402
- en: `Testing deterministic behavior in ${seat.method_scope.applicable_domains.join(", ")}`,
403
- zh: `用于测试确定性行为:${seat.method_scope.applicable_domains.join("")}`,
426
+ en: selectorBestForLocale(seat.persona_id, "en"),
427
+ zh: selectorBestForLocale(seat.persona_id, "zh"),
428
+ ja: selectorBestForLocale(seat.persona_id, "ja"),
429
+ ko: selectorBestForLocale(seat.persona_id, "ko"),
404
430
  },
405
431
  },
406
432
  capability: {
@@ -107,19 +107,38 @@ export async function runPackageSmoke() {
107
107
 
108
108
  const listed = await server.request("tools/list", {});
109
109
  const tools = listed.result?.tools || [];
110
+ assert.equal(tools.length, 31, "installed server must expose exactly 31 MCP tools");
110
111
  const names = new Set(tools.map((tool) => tool.name));
111
112
  for (const required of ["begin_council_selection", "confirm_master_selection", "plan_visible_run", "analyze_symbol"]) {
112
113
  assert.ok(names.has(required), `installed server is missing ${required}`);
113
114
  }
114
115
 
115
- const prompt = "bounded installed-package smoke";
116
- const opened = structured(await server.callTool("begin_council_selection", {
117
- symbol: "AAPL",
118
- language: "English",
119
- host: "package-smoke",
120
- prompt,
121
- }));
122
- assert.equal(opened.masters.length, 26, "installed catalog must contain exactly 26 seats");
116
+ const localeCases = [
117
+ { language: "en-US", prompt: "bounded installed-package smoke", script: /[A-Za-z]/u },
118
+ { language: "zh-CN", prompt: "安装包有界烟雾测试", script: /\p{Script=Han}/u },
119
+ { language: "ja-JP", prompt: "インストール済みパッケージの有界スモークテスト", script: /[\p{Script=Hiragana}\p{Script=Katakana}]/u },
120
+ { language: "ko-KR", prompt: "설치 패키지의 제한된 스모크 테스트", script: /\p{Script=Hangul}/u },
121
+ ];
122
+ let opened;
123
+ let prompt;
124
+ for (const locale of localeCases) {
125
+ const catalog = structured(await server.callTool("begin_council_selection", {
126
+ symbol: "AAPL",
127
+ language: locale.language,
128
+ host: "package-smoke",
129
+ prompt: locale.prompt,
130
+ }));
131
+ assert.equal(catalog.masters.length, 26, `${locale.language} installed catalog must contain exactly 26 seats`);
132
+ for (const master of catalog.masters) {
133
+ for (const field of ["identity", "method", "best_for", "maturity_label"]) {
134
+ assert.match(master[field], locale.script, `${locale.language} ${master.id}.${field}`);
135
+ }
136
+ }
137
+ if (locale.language === "zh-CN") {
138
+ opened = catalog;
139
+ prompt = locale.prompt;
140
+ }
141
+ }
123
142
 
124
143
  const confirmed = structured(await server.callTool("confirm_master_selection", {
125
144
  selection_id: opened.selection_id,
@@ -132,7 +151,7 @@ export async function runPackageSmoke() {
132
151
  const runId = `PACKAGE-SMOKE-${process.pid}`;
133
152
  const planned = structured(await server.callTool("plan_visible_run", {
134
153
  symbol: "AAPL",
135
- language: "English",
154
+ language: "zh-CN",
136
155
  prompt,
137
156
  run_id: runId,
138
157
  tasks: ["market_data"],
@@ -143,7 +162,7 @@ export async function runPackageSmoke() {
143
162
 
144
163
  const replay = await server.callTool("plan_visible_run", {
145
164
  symbol: "AAPL",
146
- language: "English",
165
+ language: "zh-CN",
147
166
  prompt,
148
167
  run_id: `${runId}-REPLAY`,
149
168
  tasks: ["market_data"],
@@ -152,8 +171,8 @@ export async function runPackageSmoke() {
152
171
  });
153
172
  assert.equal(replay.error?.data?.reason, "MASTER_SELECTION_REPLAYED");
154
173
 
155
- process.stdout.write(`package-smoke: passed tools=${tools.length} catalog=26 selected=1 replay_rejected=true\n`);
156
- return { tools: tools.length, catalog: 26, selected: 1, replay_rejected: true };
174
+ process.stdout.write(`package-smoke: passed tools=${tools.length} catalog=26 locales=4 selected=1 replay_rejected=true\n`);
175
+ return { tools: tools.length, catalog: 26, locales: 4, selected: 1, replay_rejected: true };
157
176
  } finally {
158
177
  if (!server.child.killed) await server.close().catch(() => server.child.kill());
159
178
  rmSync(dataDir, { recursive: true, force: true });
@@ -32,6 +32,23 @@ export const SOURCE_PORTABLE_EXCLUDED_TESTS = Object.freeze([
32
32
  ]);
33
33
 
34
34
  export const SOURCE_PORTABLE_EXCLUDED_TEST_COUNT = 14;
35
+ // The suite contains several package-install and hash-audit files that each spawn
36
+ // their own child processes. Node's CPU-count default can oversubscribe large CI
37
+ // hosts and turn deterministic sub-second fixtures into platform-specific timeouts.
38
+ export const SOURCE_TEST_CONCURRENCY = 4;
39
+
40
+ export function sourceTestConcurrencyArg(concurrency = SOURCE_TEST_CONCURRENCY, nodeVersion = process.versions.node) {
41
+ const [major = 0, minor = 0] = String(nodeVersion).split(".").map(Number);
42
+ const supported = (major === 18 && minor >= 19)
43
+ || (major === 20 && minor >= 10)
44
+ || major >= 21;
45
+ return supported ? `--test-concurrency=${concurrency}` : null;
46
+ }
47
+
48
+ function sourceTestArgs(files = []) {
49
+ const concurrencyArg = sourceTestConcurrencyArg();
50
+ return Object.freeze(["--test", ...(concurrencyArg ? [concurrencyArg] : []), ...files]);
51
+ }
35
52
 
36
53
  function listMjsFiles(root, directory = join(root, "test")) {
37
54
  if (!existsSync(directory)) return [];
@@ -81,7 +98,11 @@ export function buildTestPlan(root = repoRoot) {
81
98
  return Object.freeze({ mode: "installed_package", excluded: Object.freeze([]), args: Object.freeze(["scripts/package-smoke.mjs"]) });
82
99
  }
83
100
  if (staging === "present") {
84
- return Object.freeze({ mode: "source_with_staging", excluded: Object.freeze([]), args: Object.freeze(["--test"]) });
101
+ return Object.freeze({
102
+ mode: "source_with_staging",
103
+ excluded: Object.freeze([]),
104
+ args: sourceTestArgs(),
105
+ });
85
106
  }
86
107
 
87
108
  const excluded = new Set(SOURCE_PORTABLE_EXCLUDED_TESTS);
@@ -91,7 +112,7 @@ export function buildTestPlan(root = repoRoot) {
91
112
  return Object.freeze({
92
113
  mode: "source_portable",
93
114
  excluded: SOURCE_PORTABLE_EXCLUDED_TESTS,
94
- args: Object.freeze(["--test", ...selected]),
115
+ args: sourceTestArgs(selected),
95
116
  });
96
117
  }
97
118