@algosuite/vo-mcp 0.2.0-beta.49 → 0.2.0-beta.50

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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @algosuite/vo-mcp
2
2
 
3
+ > **Architecture mandate (ADR-004, 2026-08-16): thin runner, cloud brain.** This package must shrink to a thin executor + shim — spawn the agent, manage worktrees/pushes, hold credentials, carry the local half of the MCP transport. Prompt composition, routing, watcher/merge/repair decisions, policy tables and the consensus engine belong in `cloud-run/vo-control-plane` / `cloud-run/vo-moat-plane`, not here (this package is on the PUBLIC npm registry). Fixes may land here when that is the fastest way to unblock the loop, but every decision-making module in this bundle is migration debt: record it in the ADR-004 ledger and move it to the plane (HQ roadmap Phase 11). Do not add new decision logic here without a ledger row.
4
+
3
5
  AlgoHQ MCP server — the open protocol surface that exposes HQ's consensus and ratchet tool family to any MCP-capable LLM client (Claude Code, Claude Desktop, Cursor, Continue, Codex, etc.).
4
6
 
5
7
  The live AlgoHQ whiteboard is available to every MCP client through
package/dist/cli.js CHANGED
@@ -1023,7 +1023,16 @@ function assertWithinByteCap(toolName, fieldName, value, maxBytes) {
1023
1023
  );
1024
1024
  }
1025
1025
  }
1026
+ function subjectFromEnv(env = process.env) {
1027
+ const code_task_id = (env["VO_CODE_TASK_ID"] ?? "").trim().slice(0, 120);
1028
+ const repo = (env["VO_CODE_TASK_REPO"] ?? "").trim();
1029
+ const subject = {};
1030
+ if (code_task_id) subject.code_task_id = code_task_id;
1031
+ if (repo && REPO_RE.test(repo) && repo.length <= 200) subject.repo = repo;
1032
+ return subject.code_task_id || subject.repo ? subject : null;
1033
+ }
1026
1034
  function buildBaseEvent(args) {
1035
+ const subject = args.subject === void 0 ? subjectFromEnv() : args.subject;
1027
1036
  return {
1028
1037
  schema_version: 1,
1029
1038
  event_id: args.eventId ?? randomUUID(),
@@ -1049,7 +1058,12 @@ function buildBaseEvent(args) {
1049
1058
  downstream_outcome: null,
1050
1059
  vo_mcp_version: VO_MCP_VERSION,
1051
1060
  consensus_engine_version: null,
1052
- cache_hit: false
1061
+ cache_hit: false,
1062
+ // OMIT the key when there is no subject (rather than `subject: null`): the
1063
+ // ingest schema is `.strict()`, so an event with no subject stays valid on a
1064
+ // sink that has not learned the field yet — only subject-carrying events
1065
+ // depend on the sink being current (deploy ordering: sink before producer).
1066
+ ...subject ? { subject } : {}
1053
1067
  };
1054
1068
  }
1055
1069
  function jsonContent(value) {
@@ -1115,6 +1129,16 @@ function aggregateEventTokenUsage(src, engineUsage) {
1115
1129
  total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
1116
1130
  };
1117
1131
  }
1132
+ function trajectoryFromEngine(result, known = {}) {
1133
+ const rounds = typeof result.token_usage?.rounds_counted === "number" && Number.isInteger(result.token_usage.rounds_counted) && result.token_usage.rounds_counted >= 0 ? result.token_usage.rounds_counted : null;
1134
+ const called = result.fan_out_diagnostics?.models_called;
1135
+ const turns = typeof called === "number" && Number.isInteger(called) && called >= 0 ? called : rounds !== null && rounds > 0 ? result.per_model_verdicts.length * rounds : null;
1136
+ const sourceGrounded = result.citation_grade !== void 0 || result.low_confidence_sources !== void 0;
1137
+ const tool_calls = typeof known.tool_calls === "number" && Number.isInteger(known.tool_calls) && known.tool_calls >= 0 ? known.tool_calls : sourceGrounded ? null : 0;
1138
+ if (rounds === null && turns === null && tool_calls === null) return {};
1139
+ const trajectory = { rounds, turns, tool_calls };
1140
+ return { trajectory };
1141
+ }
1118
1142
  function toEventSynthesizedVerdict(src) {
1119
1143
  return {
1120
1144
  verdict: src.verdict,
@@ -1122,12 +1146,13 @@ function toEventSynthesizedVerdict(src) {
1122
1146
  reasoning_excerpt: sanitizeExcerpt(src.reasoning_excerpt)
1123
1147
  };
1124
1148
  }
1125
- var VO_MCP_VERSION;
1149
+ var VO_MCP_VERSION, REPO_RE;
1126
1150
  var init_common = __esm({
1127
1151
  "src/tools/common.ts"() {
1128
1152
  "use strict";
1129
1153
  init_events_writer();
1130
1154
  VO_MCP_VERSION = readVoMcpVersion();
1155
+ REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
1131
1156
  }
1132
1157
  });
1133
1158
 
@@ -2431,9 +2456,9 @@ var init_sync_config = __esm({
2431
2456
  });
2432
2457
 
2433
2458
  // src/cli.ts
2434
- import { homedir as homedir8, hostname as hostname2 } from "node:os";
2459
+ import { homedir as homedir9, hostname as hostname2 } from "node:os";
2435
2460
  import { randomUUID as randomUUID6 } from "node:crypto";
2436
- import { join as join14 } from "node:path";
2461
+ import { join as join15 } from "node:path";
2437
2462
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2438
2463
 
2439
2464
  // src/server.ts
@@ -2815,7 +2840,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
2815
2840
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2816
2841
  duration_ms: engineResult.duration_ms,
2817
2842
  consensus_engine_version: engineResult.engine_version,
2818
- ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
2843
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
2844
+ ...trajectoryFromEngine(engineResult)
2819
2845
  };
2820
2846
  const payload = {
2821
2847
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2999,7 +3025,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
2999
3025
  consensus_confidence: engineResult.synthesized_verdict.confidence,
3000
3026
  duration_ms: engineResult.duration_ms,
3001
3027
  consensus_engine_version: engineResult.engine_version,
3002
- ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
3028
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
3029
+ ...trajectoryFromEngine(engineResult)
3003
3030
  };
3004
3031
  const payload = {
3005
3032
  verdict: engineResult.synthesized_verdict.verdict,
@@ -3268,7 +3295,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
3268
3295
  consensus_engine_version: engineResult.engine_version,
3269
3296
  per_model_verdicts: perModelForEvent,
3270
3297
  synthesized_verdict: synthForEvent,
3271
- ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
3298
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
3299
+ ...trajectoryFromEngine(engineResult)
3272
3300
  };
3273
3301
  const payload = {
3274
3302
  verdict: engineResult.synthesized_verdict.verdict,
@@ -3483,7 +3511,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
3483
3511
  consensus_confidence: engineResult.synthesized_verdict.confidence,
3484
3512
  duration_ms: engineResult.duration_ms,
3485
3513
  consensus_engine_version: engineResult.engine_version,
3486
- ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
3514
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
3515
+ ...trajectoryFromEngine(engineResult)
3487
3516
  };
3488
3517
  const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
3489
3518
  const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
@@ -4971,7 +5000,8 @@ Produce the JSON dispatch plan now.`;
4971
5000
  consensus_engine_version: engineResult.engine_version,
4972
5001
  per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
4973
5002
  synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
4974
- ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
5003
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
5004
+ ...trajectoryFromEngine(engineResult)
4975
5005
  };
4976
5006
  deps.events.append(enrichedEvent);
4977
5007
  return jsonContent(envelope);
@@ -5673,6 +5703,7 @@ async function handleRejectAndRetry(deps, rawInput, _signal) {
5673
5703
  init_common();
5674
5704
  var TOOL_NAME18 = "vo_review_merge";
5675
5705
  var LIST_PATH = "/api/v1/admin/pr/list";
5706
+ var DEFAULT_REVIEW_REPO = "Algosuite-ai/Nexus";
5676
5707
  var ENGINE_GATE = "final-deep-verify";
5677
5708
  var EVENT_GATE = "merge-review";
5678
5709
  var UNAVAILABLE_REASON = "vo_review_merge needs cloud mode to fetch PR context \u2014 set VO_CONTROL_PLANE_URL + VO_CONTROL_PLANE_ADMIN_TOKEN in the MCP env. (It is read-only; it never merges.)";
@@ -5737,6 +5768,11 @@ async function handleReviewMerge(deps, rawInput, signal) {
5737
5768
  if (rawInput.notes !== void 0) normalizedInput.notes = rawInput.notes;
5738
5769
  const inputJson = JSON.stringify(normalizedInput);
5739
5770
  const key = deps.cache.keyFor(TOOL_NAME18, normalizedInput);
5771
+ const subject = {
5772
+ ...subjectFromEnv() ?? {},
5773
+ repo: subjectFromEnv()?.repo ?? DEFAULT_REVIEW_REPO,
5774
+ pr_number: prNumber
5775
+ };
5740
5776
  const baseEvent = buildBaseEvent({
5741
5777
  tool: TOOL_NAME18,
5742
5778
  gateType: EVENT_GATE,
@@ -5744,7 +5780,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
5744
5780
  inputExcerpt: inputJson.slice(0, 300),
5745
5781
  inputSizeBytes: bytesOf(inputJson),
5746
5782
  session: deps.session,
5747
- now: deps.now()
5783
+ now: deps.now(),
5784
+ subject
5748
5785
  });
5749
5786
  const emit = (payload2, eventExtra) => {
5750
5787
  deps.events.append(eventExtra ? { ...baseEvent, ...eventExtra } : baseEvent);
@@ -5846,7 +5883,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
5846
5883
  consensus_engine_version: result.engine_version,
5847
5884
  per_model_verdicts: perModel,
5848
5885
  synthesized_verdict: synth,
5849
- ...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
5886
+ ...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage),
5887
+ ...trajectoryFromEngine(result)
5850
5888
  });
5851
5889
  }
5852
5890
 
@@ -8396,6 +8434,109 @@ function createConsensusFallbackClient(primary, fallback, options = {}) {
8396
8434
  };
8397
8435
  }
8398
8436
 
8437
+ // src/consensus/shadow-client.ts
8438
+ import { appendFileSync as appendFileSync2, chmodSync as chmodSync4, mkdirSync as mkdirSync8, renameSync, statSync as statSync7 } from "node:fs";
8439
+ import { homedir as homedir8 } from "node:os";
8440
+ import { dirname as dirname7, join as join14 } from "node:path";
8441
+ var DEFAULT_SHADOW_TIMEOUT_MS = 2e4;
8442
+ var SHADOW_RECEIPT_MAX_BYTES = 20 * 1024 * 1024;
8443
+ function defaultShadowReceiptPath(env = process.env) {
8444
+ const p = (env["VO_MCP_MOAT_SHADOW_PATH"] ?? "").trim();
8445
+ return p || join14(homedir8(), ".claude", "vo-mcp-moat-shadow.jsonl");
8446
+ }
8447
+ function appendShadowReceipt(receipt, path3 = defaultShadowReceiptPath()) {
8448
+ try {
8449
+ mkdirSync8(dirname7(path3), { recursive: true, mode: 448 });
8450
+ try {
8451
+ if (statSync7(path3).size > SHADOW_RECEIPT_MAX_BYTES) renameSync(path3, `${path3}.1`);
8452
+ } catch {
8453
+ }
8454
+ appendFileSync2(path3, `${JSON.stringify(receipt)}
8455
+ `, "utf8");
8456
+ try {
8457
+ chmodSync4(path3, 384);
8458
+ } catch {
8459
+ }
8460
+ } catch {
8461
+ }
8462
+ }
8463
+ function summarize(result) {
8464
+ if (result.ok) {
8465
+ return {
8466
+ ok: true,
8467
+ verdict: result.synthesized_verdict.verdict,
8468
+ confidence: result.synthesized_verdict.confidence,
8469
+ ...result.receipt_id ? { receipt_id: result.receipt_id } : {}
8470
+ };
8471
+ }
8472
+ return { ok: false, reason: result.reason };
8473
+ }
8474
+ async function runMoat(shadow, request, timeoutMs, linkCallerSignal) {
8475
+ const callerSignal = linkCallerSignal ? request.signal : void 0;
8476
+ if (callerSignal?.aborted) return { ok: false, reason: CANCELLED_REASON };
8477
+ const controller = new AbortController();
8478
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8479
+ const onCallerAbort = () => controller.abort();
8480
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
8481
+ try {
8482
+ const { signal: _ignored, ...rest } = request;
8483
+ void _ignored;
8484
+ return await shadow.run({ ...rest, signal: controller.signal });
8485
+ } catch (err) {
8486
+ return { ok: false, reason: `moat-shadow-threw: ${err instanceof Error ? err.message : String(err)}`.slice(0, 200) };
8487
+ } finally {
8488
+ clearTimeout(timer);
8489
+ callerSignal?.removeEventListener("abort", onCallerAbort);
8490
+ }
8491
+ }
8492
+ function createConsensusShadowClient(primary, shadow, options = {}) {
8493
+ const authoritative = options.authoritative === true;
8494
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SHADOW_TIMEOUT_MS;
8495
+ const now = options.now ?? (() => Date.now());
8496
+ const random = options.random ?? Math.random;
8497
+ const samplePct = Math.min(100, Math.max(0, options.samplePct ?? 100));
8498
+ const onReceipt = options.onReceipt ?? ((r) => appendShadowReceipt(r));
8499
+ function receiptFor(request, local, moat, startedAt) {
8500
+ const l = summarize(local);
8501
+ const m = summarize(moat);
8502
+ return {
8503
+ ts: new Date(now()).toISOString(),
8504
+ gate_type: request.gate_type,
8505
+ mode: authoritative ? "authoritative" : "shadow",
8506
+ local: l,
8507
+ moat: m,
8508
+ agree: l.ok && m.ok ? l.verdict === m.verdict : null,
8509
+ duration_ms: Math.max(0, now() - startedAt)
8510
+ };
8511
+ }
8512
+ return {
8513
+ async run(request) {
8514
+ const startedAt = now();
8515
+ const primaryResult = await primary.run(request);
8516
+ if (request.signal?.aborted || !primaryResult.ok && primaryResult.reason === CANCELLED_REASON) {
8517
+ return primaryResult;
8518
+ }
8519
+ if (!primaryResult.ok || primaryResult.receipt_id) return primaryResult;
8520
+ if (authoritative) {
8521
+ const moatResult = await runMoat(shadow, request, timeoutMs, true);
8522
+ try {
8523
+ onReceipt(receiptFor(request, primaryResult, moatResult, startedAt));
8524
+ } catch {
8525
+ }
8526
+ return moatResult.ok ? moatResult : primaryResult;
8527
+ }
8528
+ if (samplePct < 100 && random() * 100 >= samplePct) return primaryResult;
8529
+ void runMoat(shadow, request, timeoutMs, false).then((moatResult) => {
8530
+ try {
8531
+ onReceipt(receiptFor(request, primaryResult, moatResult, startedAt));
8532
+ } catch {
8533
+ }
8534
+ });
8535
+ return primaryResult;
8536
+ }
8537
+ };
8538
+ }
8539
+
8399
8540
  // src/consensus/local-credential-env.ts
8400
8541
  import { createRequire as createRequire2 } from "node:module";
8401
8542
  var require2 = createRequire2(import.meta.url);
@@ -8629,7 +8770,7 @@ init_common();
8629
8770
  function defaultCacheDbPath() {
8630
8771
  const env = process.env["VO_MCP_DB_PATH"];
8631
8772
  if (env && env.length > 0) return env;
8632
- return join14(homedir8(), ".claude", "vo-mcp-cache.db");
8773
+ return join15(homedir9(), ".claude", "vo-mcp-cache.db");
8633
8774
  }
8634
8775
  async function probeEngineVersion() {
8635
8776
  try {
@@ -8717,6 +8858,16 @@ async function main() {
8717
8858
  consensus = createConsensusFallbackClient(localConsensus, cloudConsensus, {
8718
8859
  onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
8719
8860
  });
8861
+ const moatAuthoritative = process.env["VO_CONSENSUS_MOAT_AUTHORITATIVE"] === "1";
8862
+ if (moatAuthoritative || process.env["VO_CONSENSUS_MOAT_SHADOW"] === "1") {
8863
+ const pctRaw = Number(process.env["VO_CONSENSUS_MOAT_SHADOW_PCT"] ?? "100");
8864
+ const samplePct = Number.isFinite(pctRaw) ? Math.min(100, Math.max(0, pctRaw)) : 100;
8865
+ const shadowedLocal = createConsensusShadowClient(localConsensus, cloudConsensus, { authoritative: moatAuthoritative, samplePct });
8866
+ consensus = createConsensusFallbackClient(shadowedLocal, cloudConsensus, {
8867
+ onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
8868
+ });
8869
+ console.error(`[vo-mcp] moat ${moatAuthoritative ? "AUTHORITATIVE" : `SHADOW (${samplePct}%)`} active \u2014 local verdicts also reach the moat verify path (receipts: ${defaultShadowReceiptPath()})`);
8870
+ }
8720
8871
  } else if (!testClient && cloudConsensus) {
8721
8872
  console.error("[vo-mcp] fewer than 2 linked local providers; cloud moat consensus active");
8722
8873
  consensus = cloudConsensus;
@@ -8785,12 +8936,12 @@ if (process.argv[2] === "login") {
8785
8936
  const sessionId = randomUUID6();
8786
8937
  const appendSyncLog = async (line) => {
8787
8938
  try {
8788
- const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync8 } = await import("node:fs");
8789
- const { join: join15 } = await import("node:path");
8790
- const { homedir: homedir9 } = await import("node:os");
8791
- const dir = join15(homedir9(), ".claude");
8792
- mkdirSync8(dir, { recursive: true });
8793
- appendFileSync2(join15(dir, "vo-mcp-sync.log"), `${line}
8939
+ const { appendFileSync: appendFileSync3, mkdirSync: mkdirSync9 } = await import("node:fs");
8940
+ const { join: join16 } = await import("node:path");
8941
+ const { homedir: homedir10 } = await import("node:os");
8942
+ const dir = join16(homedir10(), ".claude");
8943
+ mkdirSync9(dir, { recursive: true });
8944
+ appendFileSync3(join16(dir, "vo-mcp-sync.log"), `${line}
8794
8945
  `, "utf8");
8795
8946
  } catch {
8796
8947
  }