@algosuite/vo-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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/dist/cli.js CHANGED
@@ -203,24 +203,24 @@ function defaultOverridePath() {
203
203
  return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
204
204
  }
205
205
  function loadTenantOverride(opts = {}) {
206
- const path3 = opts.path ?? defaultOverridePath();
207
- if (!existsSync2(path3)) {
206
+ const path4 = opts.path ?? defaultOverridePath();
207
+ if (!existsSync2(path4)) {
208
208
  return { override: null, source_path: null };
209
209
  }
210
- const raw = readFileSync2(path3, "utf8");
210
+ const raw = readFileSync2(path4, "utf8");
211
211
  let parsed;
212
212
  try {
213
213
  parsed = JSON.parse(raw);
214
214
  } catch (err) {
215
215
  const m = err instanceof Error ? err.message : String(err);
216
- throw new Error(`vo-arch-defaults: invalid JSON in override ${path3}: ${m}`, { cause: err });
216
+ throw new Error(`vo-arch-defaults: invalid JSON in override ${path4}: ${m}`, { cause: err });
217
217
  }
218
218
  try {
219
219
  const override = parseOverride(parsed);
220
- return { override, source_path: path3 };
220
+ return { override, source_path: path4 };
221
221
  } catch (err) {
222
222
  const m = err instanceof Error ? err.message : String(err);
223
- throw new Error(`vo-arch-defaults: override schema validation failed for ${path3}: ${m}`, { cause: err });
223
+ throw new Error(`vo-arch-defaults: override schema validation failed for ${path4}: ${m}`, { cause: err });
224
224
  }
225
225
  }
226
226
  var init_load_override = __esm({
@@ -371,9 +371,9 @@ function globToRegExp(glob) {
371
371
  }
372
372
  return new RegExp("^" + out + "$");
373
373
  }
374
- function matchesAnyGlob(path3, globs) {
374
+ function matchesAnyGlob(path4, globs) {
375
375
  for (const g of globs) {
376
- if (globToRegExp(g).test(path3)) return true;
376
+ if (globToRegExp(g).test(path4)) return true;
377
377
  }
378
378
  return false;
379
379
  }
@@ -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) {
@@ -1079,6 +1093,52 @@ function toEventPerModelVerdicts(src) {
1079
1093
  };
1080
1094
  });
1081
1095
  }
1096
+ function aggregateEventTokenUsage(src, engineUsage) {
1097
+ if (engineUsage !== void 0) {
1098
+ const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
1099
+ const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
1100
+ return {
1101
+ per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
1102
+ per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
1103
+ total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
1104
+ };
1105
+ }
1106
+ const tokensIn = {};
1107
+ const tokensOut = {};
1108
+ let anyTokensIn = false;
1109
+ let anyTokensOut = false;
1110
+ let costMicroUsd = 0;
1111
+ let anyCost = false;
1112
+ for (const v of src) {
1113
+ if (typeof v.input_tokens === "number") {
1114
+ tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
1115
+ anyTokensIn = true;
1116
+ }
1117
+ if (typeof v.output_tokens === "number") {
1118
+ tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
1119
+ anyTokensOut = true;
1120
+ }
1121
+ if (typeof v.cost_micro_usd === "number") {
1122
+ costMicroUsd += v.cost_micro_usd;
1123
+ anyCost = true;
1124
+ }
1125
+ }
1126
+ return {
1127
+ per_model_tokens_in: anyTokensIn ? tokensIn : null,
1128
+ per_model_tokens_out: anyTokensOut ? tokensOut : null,
1129
+ total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
1130
+ };
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
+ }
1082
1142
  function toEventSynthesizedVerdict(src) {
1083
1143
  return {
1084
1144
  verdict: src.verdict,
@@ -1086,12 +1146,13 @@ function toEventSynthesizedVerdict(src) {
1086
1146
  reasoning_excerpt: sanitizeExcerpt(src.reasoning_excerpt)
1087
1147
  };
1088
1148
  }
1089
- var VO_MCP_VERSION;
1149
+ var VO_MCP_VERSION, REPO_RE;
1090
1150
  var init_common = __esm({
1091
1151
  "src/tools/common.ts"() {
1092
1152
  "use strict";
1093
1153
  init_events_writer();
1094
1154
  VO_MCP_VERSION = readVoMcpVersion();
1155
+ REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
1095
1156
  }
1096
1157
  });
1097
1158
 
@@ -1261,6 +1322,7 @@ __export(credential_store_exports, {
1261
1322
  KEYCHAIN_LOCATION: () => KEYCHAIN_LOCATION,
1262
1323
  credentialPath: () => credentialPath,
1263
1324
  readStoredCredential: () => readStoredCredential,
1325
+ readStoredCredentialKeychainOnly: () => readStoredCredentialKeychainOnly,
1264
1326
  writeStoredCredential: () => writeStoredCredential
1265
1327
  });
1266
1328
  import { homedir as homedir3 } from "node:os";
@@ -1319,6 +1381,11 @@ function readStoredCredential(env = process.env, keychain = realKeychain) {
1319
1381
  }
1320
1382
  return readFromFile(env);
1321
1383
  }
1384
+ function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
1385
+ if (!keychainEnabled(env, keychain)) return null;
1386
+ const raw = keychain.get();
1387
+ return raw ? deserialize(raw) : null;
1388
+ }
1322
1389
  function deleteFile(env) {
1323
1390
  try {
1324
1391
  rmSync(credentialPath(env), { force: true });
@@ -1393,43 +1460,480 @@ var init_safe_memory_file = __esm({
1393
1460
  }
1394
1461
  });
1395
1462
 
1396
- // src/tools/memory/sync-config.ts
1397
- var sync_config_exports = {};
1398
- __export(sync_config_exports, {
1399
- TOOL_NAME: () => TOOL_NAME22,
1400
- deriveProjectSlug: () => deriveProjectSlug,
1401
- description: () => description22,
1402
- getMemoryDir: () => getMemoryDir,
1403
- handleSyncConfig: () => handleSyncConfig,
1404
- inputSchema: () => inputSchema22,
1405
- isNoopSyncReason: () => isNoopSyncReason,
1406
- runMemorySync: () => runMemorySync
1463
+ // src/tools/memory/sync-lock-liveness.ts
1464
+ import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
1465
+ function defaultIsProcessAlive(pid) {
1466
+ try {
1467
+ process.kill(pid, 0);
1468
+ return true;
1469
+ } catch (err) {
1470
+ return err.code === "EPERM";
1471
+ }
1472
+ }
1473
+ function toPayload(parsed) {
1474
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1475
+ const record = parsed;
1476
+ const token = record["token"];
1477
+ const host = record["hostname"];
1478
+ if (typeof token !== "string" || token.length === 0) return null;
1479
+ const pid = record["pid"];
1480
+ const acquiredAtMs = record["acquiredAtMs"];
1481
+ return {
1482
+ pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
1483
+ hostname: typeof host === "string" ? host : "",
1484
+ sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
1485
+ token,
1486
+ acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
1487
+ acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
1488
+ };
1489
+ }
1490
+ function readLockRecord(path4) {
1491
+ let raw;
1492
+ try {
1493
+ raw = readFileSync8(path4, "utf8");
1494
+ } catch {
1495
+ return null;
1496
+ }
1497
+ try {
1498
+ return { raw, payload: toPayload(JSON.parse(raw)) };
1499
+ } catch {
1500
+ return { raw, payload: null };
1501
+ }
1502
+ }
1503
+ function lockAgeMs(record, path4, nowMs) {
1504
+ let startedMs = Number.NaN;
1505
+ if (record.payload) {
1506
+ if (Number.isFinite(record.payload.acquiredAtMs)) {
1507
+ startedMs = record.payload.acquiredAtMs;
1508
+ } else if (record.payload.acquiredAt) {
1509
+ startedMs = Date.parse(record.payload.acquiredAt);
1510
+ }
1511
+ }
1512
+ if (!Number.isFinite(startedMs)) {
1513
+ try {
1514
+ startedMs = statSync5(path4).mtimeMs;
1515
+ } catch {
1516
+ return null;
1517
+ }
1518
+ }
1519
+ const age = nowMs - startedMs;
1520
+ return Number.isFinite(age) && age >= 0 ? age : null;
1521
+ }
1522
+ function classifyHolderLiveness(record, isProcessAlive, thisHost) {
1523
+ const payload = record.payload;
1524
+ if (payload === null) return "unknown";
1525
+ if (payload.pid <= 0) return "unknown";
1526
+ if (thisHost.length === 0) return "unknown";
1527
+ if (payload.hostname !== thisHost) return "unknown";
1528
+ return isProcessAlive(payload.pid) ? "alive" : "dead";
1529
+ }
1530
+ function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
1531
+ const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
1532
+ if (liveness === "alive") return false;
1533
+ if (liveness === "dead") return true;
1534
+ return ageMs !== null && ageMs > ttlMs;
1535
+ }
1536
+ var init_sync_lock_liveness = __esm({
1537
+ "src/tools/memory/sync-lock-liveness.ts"() {
1538
+ "use strict";
1539
+ }
1407
1540
  });
1408
- import { homedir as homedir5 } from "node:os";
1409
- import { join as join7 } from "node:path";
1410
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "node:fs";
1411
- function isToolInput22(v) {
1412
- if (typeof v !== "object" || v === null) return false;
1413
- const o = v;
1414
- if (o["action"] !== "pull" && o["action"] !== "push") return false;
1415
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
1416
- return true;
1541
+
1542
+ // src/tools/memory/sync-lock.ts
1543
+ import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
1544
+ import { hostname } from "node:os";
1545
+ import { join as join8 } from "node:path";
1546
+ import { randomUUID as randomUUID2 } from "node:crypto";
1547
+ function positiveOr(value, fallback) {
1548
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
1417
1549
  }
1418
- function deriveProjectSlug(cwd) {
1419
- return cwd.replace(/\\/g, "/").replace(/\/+$/g, "").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
1550
+ function createExclusive2(path4, contents) {
1551
+ let fd;
1552
+ try {
1553
+ fd = openSync3(path4, "wx");
1554
+ } catch (err) {
1555
+ const code = err.code;
1556
+ return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
1557
+ }
1558
+ try {
1559
+ writeFileSync4(fd, contents, "utf8");
1560
+ } catch (err) {
1561
+ closeSync3(fd);
1562
+ try {
1563
+ unlinkSync2(path4);
1564
+ } catch {
1565
+ }
1566
+ return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
1567
+ }
1568
+ closeSync3(fd);
1569
+ return { ok: true };
1420
1570
  }
1421
- function getMemoryDir(cwd) {
1422
- const slug = deriveProjectSlug(cwd);
1423
- return join7(homedir5(), ".claude", "projects", slug, "memory");
1571
+ function removeAbandoned(path4, expectedRaw) {
1572
+ let current;
1573
+ try {
1574
+ current = readFileSync9(path4, "utf8");
1575
+ } catch {
1576
+ return;
1577
+ }
1578
+ if (current !== expectedRaw) return;
1579
+ try {
1580
+ unlinkSync2(path4);
1581
+ } catch {
1582
+ }
1583
+ }
1584
+ function makeRelease(path4, token) {
1585
+ let released = false;
1586
+ return () => {
1587
+ if (released) return;
1588
+ released = true;
1589
+ let raw;
1590
+ try {
1591
+ raw = readFileSync9(path4, "utf8");
1592
+ } catch {
1593
+ return;
1594
+ }
1595
+ let stillOurs;
1596
+ try {
1597
+ stillOurs = toPayload(JSON.parse(raw))?.token === token;
1598
+ } catch {
1599
+ stillOurs = false;
1600
+ }
1601
+ if (!stillOurs) return;
1602
+ try {
1603
+ unlinkSync2(path4);
1604
+ } catch {
1605
+ }
1606
+ };
1607
+ }
1608
+ function describeHolder(record) {
1609
+ const payload = record?.payload;
1610
+ if (!payload) return "an unreadable lock file";
1611
+ return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
1612
+ }
1613
+ async function acquireMemorySyncLock(options) {
1614
+ const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
1615
+ const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
1616
+ const now = options.now ?? Date.now;
1617
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
1618
+ setTimeout(resolve3, ms);
1619
+ }));
1620
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
1621
+ const thisHost = hostname();
1622
+ const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
1623
+ if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
1624
+ const deadline = now() + waitMs;
1625
+ let backoffMs = INITIAL_BACKOFF_MS;
1626
+ let tookOverFrom = null;
1627
+ let holderDescription = "another session";
1628
+ for (; ; ) {
1629
+ const acquiredAtMs = now();
1630
+ const payload = {
1631
+ pid: process.pid,
1632
+ hostname: thisHost,
1633
+ sessionId: options.sessionId ?? null,
1634
+ token: randomUUID2(),
1635
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
1636
+ acquiredAtMs
1637
+ };
1638
+ const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
1639
+ `);
1640
+ if (created.ok) {
1641
+ return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
1642
+ }
1643
+ if (!created.exists) {
1644
+ throw new Error(
1645
+ `memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
1646
+ );
1647
+ }
1648
+ const record = readLockRecord(path4);
1649
+ let reclaimed = false;
1650
+ if (record) {
1651
+ holderDescription = describeHolder(record);
1652
+ const age = lockAgeMs(record, path4, now());
1653
+ if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
1654
+ tookOverFrom = record.payload;
1655
+ removeAbandoned(path4, record.raw);
1656
+ reclaimed = true;
1657
+ }
1658
+ }
1659
+ if (now() >= deadline) {
1660
+ throw new Error(
1661
+ `memory sync lock ${path4} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
1662
+ );
1663
+ }
1664
+ if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
1665
+ await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
1666
+ if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
1667
+ }
1668
+ }
1669
+ async function withMemorySyncLock(options, fn) {
1670
+ const handle = await acquireMemorySyncLock(options);
1671
+ try {
1672
+ return await fn(handle);
1673
+ } finally {
1674
+ handle.release();
1675
+ }
1676
+ }
1677
+ var MEMORY_SYNC_LOCK_FILE, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_WAIT_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_FACTOR;
1678
+ var init_sync_lock = __esm({
1679
+ "src/tools/memory/sync-lock.ts"() {
1680
+ "use strict";
1681
+ init_sync_lock_liveness();
1682
+ init_sync_lock_liveness();
1683
+ MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
1684
+ DEFAULT_LOCK_TTL_MS = 15 * 6e4;
1685
+ DEFAULT_LOCK_WAIT_MS = 1e4;
1686
+ INITIAL_BACKOFF_MS = 25;
1687
+ MAX_BACKOFF_MS = 500;
1688
+ BACKOFF_FACTOR = 1.6;
1689
+ }
1690
+ });
1691
+
1692
+ // src/tools/memory/memory-index-merge.ts
1693
+ function isMemoryIndexFile(fileName) {
1694
+ return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
1695
+ }
1696
+ function indexRowKey(line) {
1697
+ const match = INDEX_ROW_RE.exec(line);
1698
+ if (!match) return null;
1699
+ let target = match[1].trim();
1700
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
1701
+ target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
1702
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
1703
+ target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
1704
+ target = target.replace(/^(?:\.\/)+/, "");
1705
+ }
1706
+ return target.length > 0 ? target.toLowerCase() : null;
1707
+ }
1708
+ function mergeMemoryIndex(localContent, cloudContent) {
1709
+ if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
1710
+ return { content: localContent, addedFromCloud: [] };
1711
+ }
1712
+ const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
1713
+ const localLines = localContent.split(/\r?\n/);
1714
+ const localKeys = /* @__PURE__ */ new Set();
1715
+ let lastLocalRowIndex = -1;
1716
+ for (let i = 0; i < localLines.length; i++) {
1717
+ const key = indexRowKey(localLines[i]);
1718
+ if (key === null) continue;
1719
+ localKeys.add(key);
1720
+ lastLocalRowIndex = i;
1721
+ }
1722
+ const addedFromCloud = [];
1723
+ const seenCloudKeys = /* @__PURE__ */ new Set();
1724
+ for (const rawLine of cloudContent.split(/\r?\n/)) {
1725
+ const key = indexRowKey(rawLine);
1726
+ if (key === null) continue;
1727
+ if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
1728
+ seenCloudKeys.add(key);
1729
+ addedFromCloud.push(rawLine.replace(/\r$/, ""));
1730
+ }
1731
+ if (addedFromCloud.length === 0) {
1732
+ return { content: localContent, addedFromCloud: [] };
1733
+ }
1734
+ const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
1735
+ return { content: merged.join(eol), addedFromCloud };
1736
+ }
1737
+ var MEMORY_INDEX_FILE, INDEX_ROW_RE;
1738
+ var init_memory_index_merge = __esm({
1739
+ "src/tools/memory/memory-index-merge.ts"() {
1740
+ "use strict";
1741
+ MEMORY_INDEX_FILE = "MEMORY.md";
1742
+ INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
1743
+ }
1744
+ });
1745
+
1746
+ // src/tools/memory/bounded-sync.ts
1747
+ function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
1748
+ const startedAt = now();
1749
+ return {
1750
+ check() {
1751
+ const elapsed = now() - startedAt;
1752
+ if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
1753
+ },
1754
+ remainingMs() {
1755
+ return Math.max(0, budgetMs - (now() - startedAt));
1756
+ }
1757
+ };
1758
+ }
1759
+ async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
1760
+ let timer;
1761
+ try {
1762
+ return await Promise.race([
1763
+ run(),
1764
+ new Promise((_resolve, reject) => {
1765
+ timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
1766
+ timer.unref?.();
1767
+ })
1768
+ ]);
1769
+ } finally {
1770
+ if (timer) clearTimeout(timer);
1771
+ }
1772
+ }
1773
+ async function mapWithConcurrency(items, limit, fn) {
1774
+ const results = new Array(items.length);
1775
+ const width = Math.max(1, Math.min(limit, items.length));
1776
+ let next = 0;
1777
+ async function worker() {
1778
+ for (; ; ) {
1779
+ const index = next++;
1780
+ if (index >= items.length) return;
1781
+ try {
1782
+ results[index] = { ok: true, value: await fn(items[index], index) };
1783
+ } catch (error) {
1784
+ results[index] = { ok: false, error };
1785
+ }
1786
+ }
1787
+ }
1788
+ await Promise.all(Array.from({ length: width }, () => worker()));
1789
+ return results;
1790
+ }
1791
+ var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
1792
+ var init_bounded_sync = __esm({
1793
+ "src/tools/memory/bounded-sync.ts"() {
1794
+ "use strict";
1795
+ REQUEST_TIMEOUT_MS = 15e3;
1796
+ SYNC_DEADLINE_MS = 12e4;
1797
+ PUSH_CONCURRENCY = 6;
1798
+ SyncDeadlineExceededError = class extends Error {
1799
+ constructor(elapsedMs, budgetMs) {
1800
+ super(
1801
+ `memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
1802
+ );
1803
+ this.name = "SyncDeadlineExceededError";
1804
+ }
1805
+ };
1806
+ RequestTimeoutError = class extends Error {
1807
+ constructor(url, budgetMs) {
1808
+ super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
1809
+ this.name = "RequestTimeoutError";
1810
+ }
1811
+ };
1812
+ }
1813
+ });
1814
+
1815
+ // src/tools/memory/memory-push-cache.ts
1816
+ import { createHash as createHash3 } from "node:crypto";
1817
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
1818
+ import { join as join9 } from "node:path";
1819
+ function sha256(content) {
1820
+ return createHash3("sha256").update(content, "utf8").digest("hex");
1821
+ }
1822
+ function statePath(memoryDir) {
1823
+ return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
1824
+ }
1825
+ function readPushCache(memoryDir, controlPlaneUrl) {
1826
+ const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
1827
+ let raw;
1828
+ try {
1829
+ raw = readFileSync10(statePath(memoryDir), "utf8");
1830
+ } catch {
1831
+ return empty;
1832
+ }
1833
+ let parsed;
1834
+ try {
1835
+ parsed = JSON.parse(raw);
1836
+ } catch {
1837
+ return empty;
1838
+ }
1839
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
1840
+ const obj = parsed;
1841
+ if (obj["version"] !== STATE_VERSION) return empty;
1842
+ if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
1843
+ const files = obj["entries"];
1844
+ if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
1845
+ const entries = /* @__PURE__ */ new Map();
1846
+ for (const [name, value] of Object.entries(files)) {
1847
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
1848
+ const row = value;
1849
+ const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
1850
+ const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
1851
+ if (memoryHash === void 0 && knowledgeHash === void 0) continue;
1852
+ entries.set(name, {
1853
+ ...memoryHash !== void 0 ? { memoryHash } : {},
1854
+ ...knowledgeHash !== void 0 ? { knowledgeHash } : {}
1855
+ });
1856
+ }
1857
+ const sweptAt = obj["knowledgeSweptAtMs"];
1858
+ return {
1859
+ controlPlaneUrl,
1860
+ entries,
1861
+ // An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
1862
+ // sweep — the fail-closed direction (more upserts, never fewer).
1863
+ knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
1864
+ };
1865
+ }
1866
+ function writePushCache(memoryDir, cache) {
1867
+ const entries = {};
1868
+ for (const [name, value] of cache.entries) entries[name] = value;
1869
+ try {
1870
+ writeFileSync5(
1871
+ statePath(memoryDir),
1872
+ `${JSON.stringify(
1873
+ {
1874
+ version: STATE_VERSION,
1875
+ controlPlaneUrl: cache.controlPlaneUrl,
1876
+ knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
1877
+ entries
1878
+ },
1879
+ null,
1880
+ 2
1881
+ )}
1882
+ `,
1883
+ "utf8"
1884
+ );
1885
+ } catch {
1886
+ }
1887
+ }
1888
+ function recordMemoryPush(cache, fileName, payloadHash) {
1889
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
1424
1890
  }
1891
+ function recordKnowledgePush(cache, fileName, contentHash) {
1892
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
1893
+ }
1894
+ function pruneMissing(cache, presentFileNames) {
1895
+ const present = new Set(presentFileNames);
1896
+ for (const name of [...cache.entries.keys()]) {
1897
+ if (!present.has(name)) cache.entries.delete(name);
1898
+ }
1899
+ }
1900
+ function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
1901
+ if (!serverHasEntry) return true;
1902
+ return cache.entries.get(fileName)?.memoryHash !== payloadHash;
1903
+ }
1904
+ function knowledgeSweepDue(cache, nowMs = Date.now()) {
1905
+ const swept = cache.knowledgeSweptAtMs;
1906
+ if (swept === null || !Number.isFinite(swept)) return true;
1907
+ const age = nowMs - swept;
1908
+ return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
1909
+ }
1910
+ function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
1911
+ if (sweepDue) return true;
1912
+ return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
1913
+ }
1914
+ var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
1915
+ var init_memory_push_cache = __esm({
1916
+ "src/tools/memory/memory-push-cache.ts"() {
1917
+ "use strict";
1918
+ MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
1919
+ STATE_VERSION = 1;
1920
+ KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
1921
+ }
1922
+ });
1923
+
1924
+ // src/tools/memory/memory-sync-http.ts
1925
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
1425
1926
  async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
1426
1927
  const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1427
- const response = await fetchFn(url, {
1428
- method: "GET",
1429
- headers: {
1430
- authorization: `Bearer ${token}`
1431
- }
1432
- });
1928
+ const response = await withRequestTimeout(
1929
+ url,
1930
+ () => fetchFn(url, {
1931
+ method: "GET",
1932
+ headers: {
1933
+ authorization: `Bearer ${token}`
1934
+ }
1935
+ })
1936
+ );
1433
1937
  if (response.status !== 200) {
1434
1938
  const text = await response.text();
1435
1939
  throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
@@ -1442,137 +1946,468 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
1442
1946
  entry,
1443
1947
  filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
1444
1948
  }));
1445
- mkdirSync4(memoryDir, { recursive: true });
1949
+ mkdirSync6(memoryDir, { recursive: true });
1446
1950
  const files = [];
1447
1951
  for (const { entry, filePath } of writes) {
1448
- writeFileSync3(filePath, entry.content, "utf8");
1952
+ writeFileSync6(filePath, entry.content, "utf8");
1449
1953
  files.push(entry.file_name);
1450
1954
  }
1451
1955
  return { pulled: data.entries.length, files };
1452
1956
  }
1453
- async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
1454
- if (!existsSync5(memoryDir)) {
1455
- return { pushed: 0, created: 0, updated: 0 };
1957
+ function listPushableFiles(memoryDir) {
1958
+ return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
1959
+ }
1960
+ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
1961
+ deadline.check();
1962
+ if (item.memoryId !== null) {
1963
+ const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
1964
+ const updateBody = { content: item.content, session_id: sessionId };
1965
+ const updateResponse = await withRequestTimeout(
1966
+ updateUrl,
1967
+ () => fetchFn(updateUrl, {
1968
+ method: "PUT",
1969
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1970
+ body: JSON.stringify(updateBody)
1971
+ })
1972
+ );
1973
+ if (updateResponse.status !== 200) {
1974
+ const text = await updateResponse.text();
1975
+ throw new Error(
1976
+ `PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
1977
+ );
1978
+ }
1979
+ const updateData = JSON.parse(await updateResponse.text());
1980
+ if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
1981
+ return "updated";
1982
+ }
1983
+ const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1984
+ const createBody = {
1985
+ entry_type: item.entryType,
1986
+ file_name: item.fileName,
1987
+ content: item.content,
1988
+ session_id: sessionId
1989
+ };
1990
+ const createResponse = await withRequestTimeout(
1991
+ createUrl,
1992
+ () => fetchFn(createUrl, {
1993
+ method: "POST",
1994
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1995
+ body: JSON.stringify(createBody)
1996
+ })
1997
+ );
1998
+ if (createResponse.status !== 200 && createResponse.status !== 201) {
1999
+ const text = await createResponse.text();
2000
+ throw new Error(
2001
+ `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
2002
+ );
2003
+ }
2004
+ const createData = JSON.parse(await createResponse.text());
2005
+ if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
2006
+ return "created";
2007
+ }
2008
+ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
2009
+ const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
2010
+ if (!existsSync6(memoryDir)) {
2011
+ return empty;
1456
2012
  }
1457
- const localFiles = readdirSync4(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
2013
+ const localFiles = listPushableFiles(memoryDir).map((f) => ({
1458
2014
  file_name: f,
1459
- content: readFileSync7(resolveMemoryFilePath(memoryDir, f), "utf8"),
1460
- entry_type: f === "MEMORY.md" ? "index" : "topic"
2015
+ content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
2016
+ entry_type: isMemoryIndexFile(f) ? "index" : "topic"
1461
2017
  }));
1462
2018
  if (localFiles.length === 0) {
1463
- return { pushed: 0, created: 0, updated: 0 };
2019
+ return empty;
1464
2020
  }
2021
+ const deadline = options.deadline ?? createSyncDeadline();
2022
+ const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
2023
+ deadline.check();
1465
2024
  const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1466
- const getResponse = await fetchFn(getUrl, {
1467
- method: "GET",
1468
- headers: {
1469
- authorization: `Bearer ${token}`
1470
- }
1471
- });
2025
+ const getResponse = await withRequestTimeout(
2026
+ getUrl,
2027
+ () => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
2028
+ );
1472
2029
  const existingMap = /* @__PURE__ */ new Map();
1473
2030
  if (getResponse.status === 200) {
1474
2031
  const getData = JSON.parse(await getResponse.text());
1475
2032
  if (getData.ok && Array.isArray(getData.entries)) {
1476
2033
  for (const entry of getData.entries) {
1477
- existingMap.set(entry.file_name, entry.memory_id);
2034
+ existingMap.set(entry.file_name, {
2035
+ memoryId: entry.memory_id,
2036
+ content: typeof entry.content === "string" ? entry.content : ""
2037
+ });
1478
2038
  }
1479
2039
  }
1480
2040
  }
1481
- let created = 0;
1482
- let updated = 0;
2041
+ const toUpload = [];
2042
+ let skipped = 0;
1483
2043
  for (const localFile of localFiles) {
1484
- const memoryId = existingMap.get(localFile.file_name);
1485
- if (memoryId) {
1486
- const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${memoryId}`;
1487
- const updateBody = {
1488
- content: localFile.content,
1489
- session_id: sessionId
1490
- };
1491
- const updateResponse = await fetchFn(updateUrl, {
1492
- method: "PUT",
1493
- headers: {
1494
- authorization: `Bearer ${token}`,
1495
- "content-type": "application/json"
1496
- },
1497
- body: JSON.stringify(updateBody)
1498
- });
1499
- if (updateResponse.status !== 200) {
1500
- const text = await updateResponse.text();
1501
- throw new Error(
1502
- `PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
1503
- );
1504
- }
1505
- const updateData = JSON.parse(await updateResponse.text());
1506
- if (!updateData.ok) {
1507
- throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
1508
- }
1509
- updated++;
1510
- } else {
1511
- const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1512
- const createBody = {
1513
- entry_type: localFile.entry_type,
1514
- file_name: localFile.file_name,
1515
- content: localFile.content,
1516
- session_id: sessionId
1517
- };
1518
- const createResponse = await fetchFn(createUrl, {
1519
- method: "POST",
1520
- headers: {
1521
- authorization: `Bearer ${token}`,
1522
- "content-type": "application/json"
1523
- },
1524
- body: JSON.stringify(createBody)
1525
- });
1526
- if (createResponse.status !== 200 && createResponse.status !== 201) {
1527
- const text = await createResponse.text();
1528
- throw new Error(
1529
- `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
1530
- );
1531
- }
1532
- const createData = JSON.parse(await createResponse.text());
1533
- if (!createData.ok) {
1534
- throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
1535
- }
1536
- created++;
2044
+ const existing = existingMap.get(localFile.file_name);
2045
+ let content = localFile.content;
2046
+ let rowsPreserved = 0;
2047
+ if (localFile.entry_type === "index") {
2048
+ const merged = mergeMemoryIndex(localFile.content, existing?.content);
2049
+ content = merged.content;
2050
+ rowsPreserved = merged.addedFromCloud.length;
2051
+ }
2052
+ const payloadHash = sha256(content);
2053
+ if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
2054
+ skipped++;
2055
+ continue;
1537
2056
  }
2057
+ toUpload.push({
2058
+ fileName: localFile.file_name,
2059
+ entryType: localFile.entry_type,
2060
+ content,
2061
+ payloadHash,
2062
+ memoryId: existing?.memoryId ?? null,
2063
+ rowsPreserved
2064
+ });
1538
2065
  }
1539
- return { pushed: localFiles.length, created, updated };
1540
- }
1541
- function isNoopSyncReason(reason) {
1542
- if (!reason) return false;
1543
- return /not set|No auth configured|Failed to obtain auth token/.test(reason);
1544
- }
1545
- async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
1546
- const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
1547
- if (!controlPlaneUrl) {
1548
- return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
1549
- }
1550
- const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
1551
- const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
1552
- const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
1553
- if (!tokenSource) {
1554
- return { synced: false, reason: "No auth configured. Run `vo-mcp login` to authenticate as an operator." };
1555
- }
1556
- const token = await tokenSource.getToken();
1557
- if (!token) {
1558
- return { synced: false, reason: "Failed to obtain auth token. Run `vo-mcp login` to re-authenticate." };
2066
+ const outcomes = await mapWithConcurrency(
2067
+ toUpload,
2068
+ options.concurrency ?? PUSH_CONCURRENCY,
2069
+ (item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
2070
+ );
2071
+ let created = 0;
2072
+ let updated = 0;
2073
+ let indexRowsPreserved = 0;
2074
+ let firstError;
2075
+ for (let i = 0; i < outcomes.length; i++) {
2076
+ const outcome = outcomes[i];
2077
+ const item = toUpload[i];
2078
+ if (outcome.ok) {
2079
+ if (outcome.value === "created") created++;
2080
+ else updated++;
2081
+ indexRowsPreserved += item.rowsPreserved;
2082
+ recordMemoryPush(cache, item.fileName, item.payloadHash);
2083
+ } else if (firstError === void 0) {
2084
+ firstError = outcome.error;
2085
+ }
2086
+ }
2087
+ pruneMissing(cache, localFiles.map((f) => f.file_name));
2088
+ if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
2089
+ if (firstError !== void 0) throw firstError;
2090
+ return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
2091
+ }
2092
+ var init_memory_sync_http = __esm({
2093
+ "src/tools/memory/memory-sync-http.ts"() {
2094
+ "use strict";
2095
+ init_safe_memory_file();
2096
+ init_sync_lock();
2097
+ init_memory_index_merge();
2098
+ init_bounded_sync();
2099
+ init_memory_push_cache();
1559
2100
  }
1560
- const memoryDir = getMemoryDir(cwd);
1561
- const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
2101
+ });
2102
+
2103
+ // src/tools/memory/sync-kill-switch.ts
2104
+ import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
2105
+ import { homedir as homedir6 } from "node:os";
2106
+ import { join as join10 } from "node:path";
2107
+ function memorySyncSentinelPath(home) {
2108
+ return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
2109
+ }
2110
+ function isKillSwitchValueOn(raw) {
2111
+ if (raw === void 0 || raw === null) return false;
2112
+ const v = raw.trim().toLowerCase();
2113
+ if (v === "") return false;
2114
+ return !NEGATIONS.has(v);
2115
+ }
2116
+ function clip(raw) {
2117
+ const v = raw.trim();
2118
+ return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
2119
+ }
2120
+ function evaluateMemorySyncKillSwitch(deps = {}) {
2121
+ const env = deps.env ?? process.env;
2122
+ const home = deps.home ?? homedir6();
2123
+ const fileExists = deps.fileExists ?? existsSync7;
2124
+ const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
2125
+ const fired = [];
2126
+ const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
2127
+ if (isKillSwitchValueOn(rawEnv)) {
2128
+ fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
2129
+ }
2130
+ const sentinel = memorySyncSentinelPath(home);
2131
+ let sentinelPresent;
1562
2132
  try {
1563
- if (action === "pull") {
1564
- const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
1565
- return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
2133
+ sentinelPresent = fileExists(sentinel);
2134
+ } catch {
2135
+ sentinelPresent = false;
2136
+ }
2137
+ if (sentinelPresent) {
2138
+ let contents = "";
2139
+ let readable = true;
2140
+ try {
2141
+ contents = readFile3(sentinel);
2142
+ } catch {
2143
+ readable = false;
1566
2144
  }
1567
- const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
2145
+ if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
2146
+ fired.push(`sentinel file ${sentinel}`);
2147
+ }
2148
+ }
2149
+ if (fired.length === 0) return { disabled: false, reason: null };
2150
+ return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
2151
+ }
2152
+ var MEMORY_SYNC_DISABLE_ENV, MEMORY_SYNC_DISABLE_SENTINEL, NEGATIONS, MAX_LOGGED_VALUE;
2153
+ var init_sync_kill_switch = __esm({
2154
+ "src/tools/memory/sync-kill-switch.ts"() {
2155
+ "use strict";
2156
+ MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
2157
+ MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
2158
+ NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
2159
+ MAX_LOGGED_VALUE = 32;
2160
+ }
2161
+ });
2162
+
2163
+ // src/tools/memory/memory-knowledge-bridge.ts
2164
+ var memory_knowledge_bridge_exports = {};
2165
+ __export(memory_knowledge_bridge_exports, {
2166
+ extractMemoryTitle: () => extractMemoryTitle,
2167
+ upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
2168
+ });
2169
+ import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
2170
+ function extractMemoryTitle(fileName, content) {
2171
+ const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2172
+ if (frontmatter) {
2173
+ const description24 = frontmatter[1].match(/^description:\s*(.+)$/m);
2174
+ if (description24 && description24[1].trim()) return description24[1].trim().slice(0, 200);
2175
+ }
2176
+ const heading = content.match(/^#\s+(.+)$/m);
2177
+ if (heading && heading[1].trim()) return heading[1].trim().slice(0, 200);
2178
+ return fileName;
2179
+ }
2180
+ async function upsertMemoryFilesAsKnowledge(options) {
2181
+ const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
2182
+ let files;
2183
+ try {
2184
+ if (!existsSync8(memoryDir)) {
2185
+ return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
2186
+ }
2187
+ files = readdirSync5(memoryDir).filter(
2188
+ (f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
2189
+ );
2190
+ } catch (err) {
2191
+ return {
2192
+ attempted: 0,
2193
+ upserted: 0,
2194
+ failed: 1,
2195
+ skipped: 0,
2196
+ failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
2197
+ };
2198
+ }
2199
+ const sweepDue = cache ? knowledgeSweepDue(cache) : true;
2200
+ const candidates = [];
2201
+ const failures = [];
2202
+ let skipped = 0;
2203
+ for (const fileName of files) {
2204
+ try {
2205
+ const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
2206
+ if (content.length > CONTENT_HARD_LIMIT) {
2207
+ failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
2208
+ continue;
2209
+ }
2210
+ const hash = sha256(content);
2211
+ if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
2212
+ skipped += 1;
2213
+ continue;
2214
+ }
2215
+ candidates.push({ fileName, content, hash });
2216
+ } catch (err) {
2217
+ failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
2218
+ }
2219
+ }
2220
+ const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
2221
+ const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
2222
+ deadline?.check();
2223
+ const base = {
2224
+ knowledge_class: "memory",
2225
+ source_path: `memory/${candidate.fileName}`,
2226
+ title: extractMemoryTitle(candidate.fileName, candidate.content),
2227
+ content: candidate.content
2228
+ };
2229
+ const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
2230
+ method: "POST",
2231
+ headers: {
2232
+ authorization: `Bearer ${token}`,
2233
+ "content-type": "application/json"
2234
+ },
2235
+ body: JSON.stringify(body)
2236
+ }));
2237
+ let response = await post({
2238
+ ...base,
2239
+ provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
2240
+ });
2241
+ if (response.status === 400) {
2242
+ response = await post(base);
2243
+ }
2244
+ if (response.status >= 200 && response.status < 300) return true;
2245
+ const text = await response.text();
2246
+ throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
2247
+ });
2248
+ let upserted = 0;
2249
+ for (let i = 0; i < outcomes.length; i++) {
2250
+ const outcome = outcomes[i];
2251
+ const candidate = candidates[i];
2252
+ if (outcome.ok) {
2253
+ upserted += 1;
2254
+ if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
2255
+ } else {
2256
+ const error = outcome.error;
2257
+ failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
2258
+ }
2259
+ }
2260
+ if (cache && sweepDue && failures.length === 0) {
2261
+ cache.knowledgeSweptAtMs = Date.now();
2262
+ }
2263
+ return {
2264
+ // Every memory file this run considered. `attempted === upserted + skipped
2265
+ // + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
2266
+ attempted: files.length,
2267
+ upserted,
2268
+ failed: failures.length,
2269
+ skipped,
2270
+ failures: failures.slice(0, 5)
2271
+ };
2272
+ }
2273
+ var CONTENT_HARD_LIMIT;
2274
+ var init_memory_knowledge_bridge = __esm({
2275
+ "src/tools/memory/memory-knowledge-bridge.ts"() {
2276
+ "use strict";
2277
+ init_safe_memory_file();
2278
+ init_bounded_sync();
2279
+ init_memory_push_cache();
2280
+ CONTENT_HARD_LIMIT = 5e5;
2281
+ }
2282
+ });
2283
+
2284
+ // src/tools/memory/sync-config.ts
2285
+ var sync_config_exports = {};
2286
+ __export(sync_config_exports, {
2287
+ TOOL_NAME: () => TOOL_NAME23,
2288
+ deriveProjectSlug: () => deriveProjectSlug,
2289
+ description: () => description23,
2290
+ getMemoryDir: () => getMemoryDir,
2291
+ handleSyncConfig: () => handleSyncConfig,
2292
+ inputSchema: () => inputSchema23,
2293
+ isNoopSyncReason: () => isNoopSyncReason,
2294
+ runMemorySync: () => runMemorySync
2295
+ });
2296
+ import { existsSync as existsSync9 } from "node:fs";
2297
+ import { homedir as homedir7 } from "node:os";
2298
+ import { join as join11 } from "node:path";
2299
+ function isToolInput22(v) {
2300
+ if (typeof v !== "object" || v === null) return false;
2301
+ const o = v;
2302
+ if (o["action"] !== "pull" && o["action"] !== "push") return false;
2303
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
2304
+ return true;
2305
+ }
2306
+ function deriveProjectSlug(cwd) {
2307
+ return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
2308
+ }
2309
+ function getMemoryDir(cwd) {
2310
+ const slug = deriveProjectSlug(cwd);
2311
+ return join11(homedir7(), ".claude", "projects", slug, "memory");
2312
+ }
2313
+ function isNoopSyncReason(reason) {
2314
+ if (!reason) return false;
2315
+ return /not set|No auth configured|Failed to obtain auth token|DISABLED by/.test(reason);
2316
+ }
2317
+ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
2318
+ const killSwitch = evaluateMemorySyncKillSwitch();
2319
+ if (killSwitch.disabled) {
2320
+ return { synced: false, reason: killSwitch.reason };
2321
+ }
2322
+ const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
2323
+ if (!controlPlaneUrl) {
2324
+ return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
2325
+ }
2326
+ const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
2327
+ const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
2328
+ const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
2329
+ if (!tokenSource) {
2330
+ return { synced: false, reason: "No auth configured. Run `vo-mcp login` to authenticate as an operator." };
2331
+ }
2332
+ const token = await tokenSource.getToken();
2333
+ if (!token) {
2334
+ return { synced: false, reason: "Failed to obtain auth token. Run `vo-mcp login` to re-authenticate." };
2335
+ }
2336
+ const memoryDir = getMemoryDir(cwd);
2337
+ const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
2338
+ if (action === "push" && !existsSync9(memoryDir)) {
1568
2339
  return {
1569
2340
  synced: true,
1570
2341
  action: "push",
1571
- pushed: result.pushed,
1572
- created: result.created,
1573
- updated: result.updated,
2342
+ pushed: 0,
2343
+ created: 0,
2344
+ updated: 0,
2345
+ skipped: 0,
2346
+ index_rows_preserved: 0,
2347
+ knowledge_upserted: 0,
2348
+ knowledge_failed: 0,
1574
2349
  memory_dir: memoryDir
1575
2350
  };
2351
+ }
2352
+ try {
2353
+ return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
2354
+ const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
2355
+ if (action === "pull") {
2356
+ const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
2357
+ return {
2358
+ synced: true,
2359
+ action: "pull",
2360
+ pulled: result2.pulled,
2361
+ files: result2.files,
2362
+ memory_dir: memoryDir,
2363
+ ...takeover
2364
+ };
2365
+ }
2366
+ const deadline = createSyncDeadline();
2367
+ const cache = readPushCache(memoryDir, baseUrl);
2368
+ let result;
2369
+ try {
2370
+ result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
2371
+ } catch (err) {
2372
+ writePushCache(memoryDir, cache);
2373
+ throw err;
2374
+ }
2375
+ let bridge;
2376
+ try {
2377
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
2378
+ bridge = await upsertMemoryFilesAsKnowledge2({
2379
+ controlPlaneUrl: baseUrl,
2380
+ token,
2381
+ memoryDir,
2382
+ fetchFn,
2383
+ cache,
2384
+ deadline
2385
+ });
2386
+ } catch (err) {
2387
+ bridge = {
2388
+ upserted: 0,
2389
+ failed: 1,
2390
+ skipped: 0,
2391
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
2392
+ };
2393
+ }
2394
+ writePushCache(memoryDir, cache);
2395
+ return {
2396
+ synced: true,
2397
+ action: "push",
2398
+ pushed: result.pushed,
2399
+ created: result.created,
2400
+ updated: result.updated,
2401
+ skipped: result.skipped,
2402
+ index_rows_preserved: result.indexRowsPreserved,
2403
+ memory_dir: memoryDir,
2404
+ knowledge_upserted: bridge.upserted,
2405
+ knowledge_failed: bridge.failed,
2406
+ knowledge_skipped: bridge.skipped,
2407
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
2408
+ ...takeover
2409
+ };
2410
+ });
1576
2411
  } catch (err) {
1577
2412
  const message = err instanceof Error ? err.message : String(err);
1578
2413
  return { synced: false, reason: `Sync failed: ${message}` };
@@ -1581,22 +2416,26 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
1581
2416
  async function handleSyncConfig(deps, rawInput, _signal, fetchFn = globalThis.fetch) {
1582
2417
  if (!isToolInput22(rawInput)) {
1583
2418
  throw invalidParams(
1584
- TOOL_NAME22,
2419
+ TOOL_NAME23,
1585
2420
  'invalid input. Required: { action: "pull" | "push" }. Optional: { cwd: "<path>" }.'
1586
2421
  );
1587
2422
  }
1588
2423
  const cwd = rawInput.cwd?.trim() || process.cwd();
1589
2424
  const result = await runMemorySync(rawInput.action, cwd, deps.session.sessionId, fetchFn);
1590
- return jsonContent({ tool: TOOL_NAME22, schema_version: 1, payload: result });
2425
+ return jsonContent({ tool: TOOL_NAME23, schema_version: 1, payload: result });
1591
2426
  }
1592
- var TOOL_NAME22, inputSchema22, description22;
2427
+ var TOOL_NAME23, inputSchema23, description23;
1593
2428
  var init_sync_config = __esm({
1594
2429
  "src/tools/memory/sync-config.ts"() {
1595
2430
  "use strict";
1596
2431
  init_common();
1597
- init_safe_memory_file();
1598
- TOOL_NAME22 = "vo_sync_config";
1599
- inputSchema22 = {
2432
+ init_memory_sync_http();
2433
+ init_bounded_sync();
2434
+ init_memory_push_cache();
2435
+ init_sync_lock();
2436
+ init_sync_kill_switch();
2437
+ TOOL_NAME23 = "vo_sync_config";
2438
+ inputSchema23 = {
1600
2439
  type: "object",
1601
2440
  properties: {
1602
2441
  action: {
@@ -1612,19 +2451,19 @@ var init_sync_config = __esm({
1612
2451
  required: ["action"],
1613
2452
  additionalProperties: false
1614
2453
  };
1615
- description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed.";
2454
+ description23 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed. Serialized across concurrent sessions by an exclusive lock in the memory dir.";
1616
2455
  }
1617
2456
  });
1618
2457
 
1619
2458
  // src/cli.ts
1620
- import { homedir as homedir6, hostname } from "node:os";
1621
- import { randomUUID as randomUUID5 } from "node:crypto";
1622
- import { join as join8 } from "node:path";
2459
+ import { homedir as homedir9, hostname as hostname2 } from "node:os";
2460
+ import { randomUUID as randomUUID6 } from "node:crypto";
2461
+ import { join as join15 } from "node:path";
1623
2462
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1624
2463
 
1625
2464
  // src/server.ts
1626
2465
  init_common();
1627
- import { randomUUID as randomUUID2 } from "node:crypto";
2466
+ import { randomUUID as randomUUID3 } from "node:crypto";
1628
2467
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1629
2468
  import {
1630
2469
  CallToolRequestSchema,
@@ -2000,7 +2839,9 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
2000
2839
  synthesized_verdict: synthForEvent,
2001
2840
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2002
2841
  duration_ms: engineResult.duration_ms,
2003
- consensus_engine_version: engineResult.engine_version
2842
+ consensus_engine_version: engineResult.engine_version,
2843
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
2844
+ ...trajectoryFromEngine(engineResult)
2004
2845
  };
2005
2846
  const payload = {
2006
2847
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2183,7 +3024,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
2183
3024
  synthesized_verdict: synthForEvent,
2184
3025
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2185
3026
  duration_ms: engineResult.duration_ms,
2186
- consensus_engine_version: engineResult.engine_version
3027
+ consensus_engine_version: engineResult.engine_version,
3028
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
3029
+ ...trajectoryFromEngine(engineResult)
2187
3030
  };
2188
3031
  const payload = {
2189
3032
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2192,6 +3035,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
2192
3035
  synthesized_verdict: synthForEvent,
2193
3036
  engine_version: engineResult.engine_version,
2194
3037
  degraded: engineResult.degraded,
3038
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
3039
+ // The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
3040
+ ...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
2195
3041
  gate_type: gateType,
2196
3042
  ...kbResult.error !== null ? { kb_unavailable: true } : {},
2197
3043
  ...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
@@ -2448,7 +3294,9 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2448
3294
  duration_ms: engineResult.duration_ms,
2449
3295
  consensus_engine_version: engineResult.engine_version,
2450
3296
  per_model_verdicts: perModelForEvent,
2451
- synthesized_verdict: synthForEvent
3297
+ synthesized_verdict: synthForEvent,
3298
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
3299
+ ...trajectoryFromEngine(engineResult)
2452
3300
  };
2453
3301
  const payload = {
2454
3302
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2457,6 +3305,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2457
3305
  synthesized_verdict: synthForEvent,
2458
3306
  engine_version: engineResult.engine_version,
2459
3307
  degraded: engineResult.degraded,
3308
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
3309
+ // The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
3310
+ // NOTE: a content-hash cache hit replays the ORIGINAL call's receipt_id (same claim, same verdict, no new spend) —
3311
+ // a receipt asserts the stage ran for this claim, not one-receipt-per-call.
3312
+ ...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
2460
3313
  gate_type: gateType,
2461
3314
  // ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
2462
3315
  // Feature 2 (calibrated-confidence) — ON by default; the engine attaches
@@ -2475,7 +3328,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2475
3328
  ...engineResult.low_confidence_sources !== void 0 ? { low_confidence_sources: engineResult.low_confidence_sources } : {},
2476
3329
  // Escalation (from citation grade or human-tiebreak synthesizer).
2477
3330
  ...engineResult.escalation_required !== void 0 ? { escalation_required: engineResult.escalation_required } : {},
2478
- ...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {}
3331
+ ...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {},
3332
+ // Critique-uptake (2026-07-20 red-team fix) — the engine computes this
3333
+ // on every call; this spread closes the gap where the visibility report
3334
+ // was itself silently dropped at the payload boundary.
3335
+ ...engineResult.critique_uptake !== void 0 ? { critique_uptake: engineResult.critique_uptake } : {}
2479
3336
  };
2480
3337
  const envelope = {
2481
3338
  tool: TOOL_NAME4,
@@ -2653,7 +3510,9 @@ async function handleArchitectureReview(deps, rawInput, signal) {
2653
3510
  synthesized_verdict: synthForEvent,
2654
3511
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2655
3512
  duration_ms: engineResult.duration_ms,
2656
- consensus_engine_version: engineResult.engine_version
3513
+ consensus_engine_version: engineResult.engine_version,
3514
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
3515
+ ...trajectoryFromEngine(engineResult)
2657
3516
  };
2658
3517
  const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
2659
3518
  const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
@@ -4140,7 +4999,9 @@ Produce the JSON dispatch plan now.`;
4140
4999
  duration_ms: engineResult.duration_ms,
4141
5000
  consensus_engine_version: engineResult.engine_version,
4142
5001
  per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
4143
- synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
5002
+ synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
5003
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
5004
+ ...trajectoryFromEngine(engineResult)
4144
5005
  };
4145
5006
  deps.events.append(enrichedEvent);
4146
5007
  return jsonContent(envelope);
@@ -4153,15 +5014,27 @@ init_common();
4153
5014
  init_auth_token_source();
4154
5015
  init_credential_store();
4155
5016
  var AdminCallableError = class extends Error {
4156
- constructor(status, path3, message) {
5017
+ constructor(status, path4, message, body) {
4157
5018
  super(message);
4158
5019
  this.status = status;
4159
- this.path = path3;
5020
+ this.path = path4;
5021
+ this.body = body;
4160
5022
  this.name = "AdminCallableError";
4161
5023
  }
4162
5024
  status;
4163
5025
  path;
5026
+ body;
4164
5027
  };
5028
+ function parseJsonObject(text) {
5029
+ try {
5030
+ const parsed = JSON.parse(text);
5031
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
5032
+ return parsed;
5033
+ }
5034
+ } catch {
5035
+ }
5036
+ return void 0;
5037
+ }
4165
5038
  var HttpAdminCallableClient = class {
4166
5039
  baseUrl;
4167
5040
  tokenSource;
@@ -4183,35 +5056,39 @@ var HttpAdminCallableClient = class {
4183
5056
  this.fetchFn = config.fetchFn ?? globalThis.fetch;
4184
5057
  this.readOnly = config.readOnly ?? false;
4185
5058
  }
4186
- async invoke(path3, body, opts) {
4187
- if (!path3.startsWith("/")) {
5059
+ async invoke(path4, body, opts) {
5060
+ if (!path4.startsWith("/")) {
4188
5061
  throw new Error(
4189
- `HttpAdminCallableClient.invoke: path must start with '/', got '${path3}'`
5062
+ `HttpAdminCallableClient.invoke: path must start with '/', got '${path4}'`
4190
5063
  );
4191
5064
  }
4192
5065
  const token = await this.tokenSource.getToken();
4193
5066
  if (!token) {
4194
5067
  throw new AdminCallableError(
4195
5068
  401,
4196
- path3,
4197
- `admin proxy ${path3}: no auth token available (check VO_USER_REFRESH_TOKEN / VO_USER_ID_TOKEN / VO_CONTROL_PLANE_ADMIN_TOKEN)`
5069
+ path4,
5070
+ `admin proxy ${path4}: no auth token available (check VO_USER_REFRESH_TOKEN / VO_USER_ID_TOKEN / VO_CONTROL_PLANE_ADMIN_TOKEN)`
4198
5071
  );
4199
5072
  }
4200
- const url = `${this.baseUrl}${path3}`;
4201
- const response = await this.fetchFn(url, {
4202
- method: "POST",
5073
+ const method = opts?.method ?? "POST";
5074
+ const queryString = opts?.query ? new URLSearchParams(Object.entries(opts.query)).toString() : "";
5075
+ const url = `${this.baseUrl}${path4}${queryString ? `?${queryString}` : ""}`;
5076
+ const init = method === "GET" ? { method, headers: { "authorization": `Bearer ${token}` } } : {
5077
+ method,
4203
5078
  headers: {
4204
5079
  "authorization": `Bearer ${token}`,
4205
5080
  "content-type": "application/json"
4206
5081
  },
4207
5082
  body: JSON.stringify(body)
4208
- });
5083
+ };
5084
+ const response = await this.fetchFn(url, init);
4209
5085
  const text = await response.text();
4210
5086
  if (response.status < 200 || response.status >= 300) {
4211
5087
  throw new AdminCallableError(
4212
5088
  response.status,
4213
- path3,
4214
- `admin proxy ${path3} returned HTTP ${response.status}: ${text.slice(0, 200)}`
5089
+ path4,
5090
+ `admin proxy ${path4} returned HTTP ${response.status}: ${text.slice(0, 200)}`,
5091
+ parseJsonObject(text)
4215
5092
  );
4216
5093
  }
4217
5094
  let parsed;
@@ -4220,23 +5097,24 @@ var HttpAdminCallableClient = class {
4220
5097
  } catch {
4221
5098
  throw new AdminCallableError(
4222
5099
  response.status,
4223
- path3,
4224
- `admin proxy ${path3} returned non-JSON body`
5100
+ path4,
5101
+ `admin proxy ${path4} returned non-JSON body`
4225
5102
  );
4226
5103
  }
4227
5104
  if (typeof parsed !== "object" || parsed === null) {
4228
5105
  throw new AdminCallableError(
4229
5106
  response.status,
4230
- path3,
4231
- `admin proxy ${path3} response not an object`
5107
+ path4,
5108
+ `admin proxy ${path4} response not an object`
4232
5109
  );
4233
5110
  }
4234
5111
  const obj = parsed;
4235
5112
  if (obj["ok"] !== true) {
4236
5113
  throw new AdminCallableError(
4237
5114
  response.status,
4238
- path3,
4239
- `admin proxy ${path3} returned ok=false: ${JSON.stringify(obj).slice(0, 200)}`
5115
+ path4,
5116
+ `admin proxy ${path4} returned ok=false: ${JSON.stringify(obj).slice(0, 200)}`,
5117
+ obj
4240
5118
  );
4241
5119
  }
4242
5120
  if (opts?.rawEnvelope) {
@@ -4249,8 +5127,8 @@ var HttpAdminCallableClient = class {
4249
5127
  if (!("result" in obj)) {
4250
5128
  throw new AdminCallableError(
4251
5129
  response.status,
4252
- path3,
4253
- `admin proxy ${path3} response missing .result field`
5130
+ path4,
5131
+ `admin proxy ${path4} response missing .result field`
4254
5132
  );
4255
5133
  }
4256
5134
  return obj["result"];
@@ -4310,11 +5188,12 @@ async function buildCloudOrStubResponse(args) {
4310
5188
  args.deps.events.append(event);
4311
5189
  return jsonContent(envelope);
4312
5190
  }
5191
+ const invokeOptions = args.rawEnvelope || args.invokeOptions ? { ...args.rawEnvelope ? { rawEnvelope: true } : {}, ...args.invokeOptions } : void 0;
4313
5192
  try {
4314
5193
  const result = await args.deps.adminCallables.invoke(
4315
5194
  args.adminPath,
4316
5195
  args.cloudBody ?? args.normalizedInput,
4317
- args.rawEnvelope ? { rawEnvelope: true } : void 0
5196
+ invokeOptions
4318
5197
  );
4319
5198
  const payload = {
4320
5199
  verdict: "pass",
@@ -4334,11 +5213,13 @@ async function buildCloudOrStubResponse(args) {
4334
5213
  } catch (err) {
4335
5214
  const status = err instanceof AdminCallableError ? err.status : void 0;
4336
5215
  const message = err instanceof Error ? err.message : String(err);
5216
+ const errorBody = err instanceof AdminCallableError && err.body !== void 0 ? err.body : void 0;
4337
5217
  const payload = {
4338
5218
  verdict: "fail",
4339
5219
  reason: status !== void 0 ? `vo-control-plane returned HTTP ${status}: ${message.slice(0, 300)}` : `cloud invocation failed: ${message.slice(0, 300)}`,
4340
5220
  callable: args.callableName,
4341
- normalized_input: args.normalizedInput
5221
+ normalized_input: args.normalizedInput,
5222
+ ...errorBody ? { response_data: errorBody } : {}
4342
5223
  };
4343
5224
  const envelope = {
4344
5225
  tool: args.toolName,
@@ -4353,7 +5234,7 @@ async function buildCloudOrStubResponse(args) {
4353
5234
 
4354
5235
  // src/tools/heal/common-heal.ts
4355
5236
  init_common();
4356
- var HEAL_STUB_REASON = 'cloud-mode not yet wired; tool surface is live, admin-callable wiring pending vo-cloud-tenant-model dispatch (see packages/vo-mcp/src/modes/cloud.ts + EXTRACTION_AUDIT.md "Stub remaining")';
5237
+ var HEAL_STUB_REASON = "cloud mode is not configured on this session, so the admin callable was not reached. The wiring exists (see buildCloudOrStubResponse); sign in with `vo-mcp login` to route this tool to the control plane. Note that /api/v1/admin/* additionally requires a founding-operator credential.";
4357
5238
  var HEAL_GATE_TYPE = "admin-action";
4358
5239
 
4359
5240
  // src/tools/heal/trigger-heal.ts
@@ -4374,7 +5255,7 @@ var inputSchema8 = {
4374
5255
  },
4375
5256
  additionalProperties: false
4376
5257
  };
4377
- var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` with structured normalized_input \u2014 cloud-mode wiring is pending. The contract is locked; consumers can call this tool today and get the correct surface without working execution.";
5258
+ var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope with structured normalized_input when cloud mode is not configured.";
4378
5259
  function isToolInput8(v) {
4379
5260
  if (typeof v !== "object" || v === null) return false;
4380
5261
  const o = v;
@@ -4432,7 +5313,7 @@ var inputSchema9 = {
4432
5313
  },
4433
5314
  additionalProperties: false
4434
5315
  };
4435
- var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5316
+ var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4436
5317
  function isToolInput9(v) {
4437
5318
  if (typeof v !== "object" || v === null) return false;
4438
5319
  const o = v;
@@ -4520,7 +5401,7 @@ var inputSchema10 = {
4520
5401
  required: ["attempt_id"],
4521
5402
  additionalProperties: false
4522
5403
  };
4523
- var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5404
+ var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4524
5405
  function isToolInput10(v) {
4525
5406
  if (typeof v !== "object" || v === null) return false;
4526
5407
  const o = v;
@@ -4568,7 +5449,7 @@ var inputSchema11 = {
4568
5449
  required: ["run_id"],
4569
5450
  additionalProperties: false
4570
5451
  };
4571
- var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5452
+ var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4572
5453
  function isToolInput11(v) {
4573
5454
  if (typeof v !== "object" || v === null) return false;
4574
5455
  const o = v;
@@ -4614,7 +5495,7 @@ var inputSchema12 = {
4614
5495
  properties: {},
4615
5496
  additionalProperties: false
4616
5497
  };
4617
- var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5498
+ var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4618
5499
  function isToolInput12(v) {
4619
5500
  if (typeof v !== "object" || v === null) return false;
4620
5501
  return true;
@@ -4641,7 +5522,7 @@ init_common();
4641
5522
 
4642
5523
  // src/tools/pr/common-pr.ts
4643
5524
  init_common();
4644
- var PR_STUB_REASON = 'cloud-mode not yet wired; tool surface is live, admin-callable wiring pending vo-cloud-tenant-model dispatch (see packages/vo-mcp/src/modes/cloud.ts + EXTRACTION_AUDIT.md "Stub remaining")';
5525
+ var PR_STUB_REASON = "cloud mode is not configured on this session, so the admin callable was not reached. The wiring exists (see buildCloudOrStubResponse); sign in with `vo-mcp login` to route this tool to the control plane. Note that /api/v1/admin/* additionally requires a founding-operator credential.";
4645
5526
  var PR_GATE_TYPE = "admin-action";
4646
5527
 
4647
5528
  // src/tools/pr/list-pending-prs.ts
@@ -4653,7 +5534,7 @@ var inputSchema13 = {
4653
5534
  properties: {},
4654
5535
  additionalProperties: false
4655
5536
  };
4656
- var description13 = "Lists open VO-source pull requests with blocker / source / tester / specialist-context metadata. Read-only diagnostic for Command Center reads. Wraps `voListPendingPRs` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5537
+ var description13 = "Lists open AlgoHQ-source pull requests with blocker / source / tester / specialist-context metadata. Read-only diagnostic for Command Center reads. Wraps `voListPendingPRs` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4657
5538
  function isToolInput13(v) {
4658
5539
  return typeof v === "object" && v !== null;
4659
5540
  }
@@ -4690,7 +5571,7 @@ var inputSchema14 = {
4690
5571
  required: ["pr_number"],
4691
5572
  additionalProperties: false
4692
5573
  };
4693
- var description14 = "Approves + merges a single VO-source pull request by number. Wraps `voMergePR` admin Cloud Function (server-side refuses non-VO PRs with permission-denied). V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5574
+ var description14 = "Approves + merges a single AlgoHQ-source pull request by number. Wraps `voMergePR` admin Cloud Function (server-side refuses non-AlgoHQ PRs with permission-denied). Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4694
5575
  function isToolInput14(v) {
4695
5576
  if (typeof v !== "object" || v === null) return false;
4696
5577
  const o = v;
@@ -4735,7 +5616,7 @@ var inputSchema15 = {
4735
5616
  required: ["pr_number"],
4736
5617
  additionalProperties: false
4737
5618
  };
4738
- var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5619
+ var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4739
5620
  function isToolInput15(v) {
4740
5621
  if (typeof v !== "object" || v === null) return false;
4741
5622
  const o = v;
@@ -4774,7 +5655,7 @@ var inputSchema16 = {
4774
5655
  properties: {},
4775
5656
  additionalProperties: false
4776
5657
  };
4777
- var description16 = "Iterates all open VO-source pull requests and merges (or arms auto-merge) on each. Returns counts of merged / accepted / total plus per-PR results. Wraps `voApproveAllFixes` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5658
+ var description16 = "Iterates all open AlgoHQ-source pull requests and merges (or arms auto-merge) on each. Returns counts of merged / accepted / total plus per-PR results. Wraps `voApproveAllFixes` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4778
5659
  function isToolInput16(v) {
4779
5660
  return typeof v === "object" && v !== null;
4780
5661
  }
@@ -4809,7 +5690,7 @@ var inputSchema17 = {
4809
5690
  required: ["pr_number"],
4810
5691
  additionalProperties: false
4811
5692
  };
4812
- var description17 = "Closes a VO pull request and dispatches a self-heal pass to retry the same focus page. Cloud callable refuses non-VO PRs and respects the self-heal kill switch + per-PR retry block. Wraps `voRejectAndRetry` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
5693
+ var description17 = "Closes an AlgoHQ pull request and dispatches a self-heal pass to retry the same focus page. Cloud callable refuses non-AlgoHQ PRs and respects the self-heal kill switch + per-PR retry block. Wraps `voRejectAndRetry` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
4813
5694
  function isToolInput17(v) {
4814
5695
  if (typeof v !== "object" || v === null) return false;
4815
5696
  const o = v;
@@ -4842,6 +5723,7 @@ async function handleRejectAndRetry(deps, rawInput, _signal) {
4842
5723
  init_common();
4843
5724
  var TOOL_NAME18 = "vo_review_merge";
4844
5725
  var LIST_PATH = "/api/v1/admin/pr/list";
5726
+ var DEFAULT_REVIEW_REPO = "Algosuite-ai/Nexus";
4845
5727
  var ENGINE_GATE = "final-deep-verify";
4846
5728
  var EVENT_GATE = "merge-review";
4847
5729
  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.)";
@@ -4886,7 +5768,7 @@ function buildPrompt4(pr, notes) {
4886
5768
  const lines = [
4887
5769
  "You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
4888
5770
  "Recommend exactly one of: merge / hold / reject. Be conservative \u2014 this is a high-stakes irreversible action.",
4889
- "Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate VO-source change or has no clear purpose. MERGE only if it looks complete, scoped, and unblocked.",
5771
+ "Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate AlgoHQ-source change or has no clear purpose. MERGE only if it looks complete, scoped, and unblocked.",
4890
5772
  "",
4891
5773
  `PR #${pr.number}: ${pr.title}`,
4892
5774
  `Source: ${pr.source ?? "unknown"}`,
@@ -4906,6 +5788,11 @@ async function handleReviewMerge(deps, rawInput, signal) {
4906
5788
  if (rawInput.notes !== void 0) normalizedInput.notes = rawInput.notes;
4907
5789
  const inputJson = JSON.stringify(normalizedInput);
4908
5790
  const key = deps.cache.keyFor(TOOL_NAME18, normalizedInput);
5791
+ const subject = {
5792
+ ...subjectFromEnv() ?? {},
5793
+ repo: subjectFromEnv()?.repo ?? DEFAULT_REVIEW_REPO,
5794
+ pr_number: prNumber
5795
+ };
4909
5796
  const baseEvent = buildBaseEvent({
4910
5797
  tool: TOOL_NAME18,
4911
5798
  gateType: EVENT_GATE,
@@ -4913,7 +5800,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
4913
5800
  inputExcerpt: inputJson.slice(0, 300),
4914
5801
  inputSizeBytes: bytesOf(inputJson),
4915
5802
  session: deps.session,
4916
- now: deps.now()
5803
+ now: deps.now(),
5804
+ subject
4917
5805
  });
4918
5806
  const emit = (payload2, eventExtra) => {
4919
5807
  deps.events.append(eventExtra ? { ...baseEvent, ...eventExtra } : baseEvent);
@@ -4954,7 +5842,7 @@ async function handleReviewMerge(deps, rawInput, signal) {
4954
5842
  }
4955
5843
  if (pr === null) {
4956
5844
  return emit(
4957
- emptyPayload("hold", `PR #${prNumber} is not among open VO PRs (already merged/closed, or not a VO-source PR).`, null)
5845
+ emptyPayload("hold", `PR #${prNumber} is not among open AlgoHQ PRs (already merged/closed, or not an AlgoHQ-source PR).`, null)
4958
5846
  );
4959
5847
  }
4960
5848
  const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
@@ -5014,7 +5902,139 @@ async function handleReviewMerge(deps, rawInput, signal) {
5014
5902
  duration_ms: result.duration_ms,
5015
5903
  consensus_engine_version: result.engine_version,
5016
5904
  per_model_verdicts: perModel,
5017
- synthesized_verdict: synth
5905
+ synthesized_verdict: synth,
5906
+ ...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage),
5907
+ ...trajectoryFromEngine(result)
5908
+ });
5909
+ }
5910
+
5911
+ // src/tools/runner/prepared-job-mode.ts
5912
+ init_common();
5913
+ var TOOL_NAME19 = "vo_prepared_job_mode";
5914
+ var CALLABLE_NAME10 = "GET|POST /api/v1/runner/prepared-job-mode";
5915
+ var PLANE_PATH = "/api/v1/runner/prepared-job-mode";
5916
+ var PREPARED_JOB_MODE_GATE_TYPE = "admin-action";
5917
+ var PREPARED_JOB_MODE_STUB_REASON = "cloud mode is not configured on this session, so vo-control-plane was not reached. Sign in with `vo-mcp login` to route this tool to the plane. This route is NOT under /api/v1/admin/*, so a tenant-scoped operator credential is the CORRECT principal for it \u2014 the target operator is derived from that principal, never from this input.";
5918
+ var PREPARED_JOB_MODES = ["off", "shadow", "prepared"];
5919
+ var MIN_REASON_CHARS = 10;
5920
+ var MAX_REASON_CHARS = 500;
5921
+ var MAX_RUNNER_ID_CHARS = 100;
5922
+ var inputSchema19 = {
5923
+ type: "object",
5924
+ properties: {
5925
+ action: {
5926
+ type: "string",
5927
+ enum: ["get", "set"],
5928
+ description: "Required. 'get' reads the delivered mode (read-only; stays live under VO_ADMIN_CALLABLES_READONLY). 'set' writes it (a write; gated to the stub in read-only mode)."
5929
+ },
5930
+ runner_id: {
5931
+ type: "string",
5932
+ minLength: 1,
5933
+ maxLength: MAX_RUNNER_ID_CHARS,
5934
+ description: "Required. Runner to read/configure, e.g. 'vo-code-runner-JacksPC'. Must belong to the authenticated operator \u2014 the plane scopes by principal, so another operator's runner simply reads as unset."
5935
+ },
5936
+ prepared_job_mode: {
5937
+ type: "string",
5938
+ enum: [...PREPARED_JOB_MODES],
5939
+ description: "Required for action='set'. 'off' = the runner's machine-local env rules; 'shadow' = compare the plane's prepared job without consuming it. 'prepared' is recognized but REFUSED by the plane with 409 prepared_mode_not_flippable (ADR-004 11.1c owns the flip)."
5940
+ },
5941
+ expected_revision: {
5942
+ type: "integer",
5943
+ minimum: 0,
5944
+ description: "Required for action='set'. Optimistic-concurrency revision read from a prior 'get' (use 0 when no config exists). A stale value returns 409 stale_prepared_job_revision carrying the authoritative current_revision."
5945
+ },
5946
+ reason: {
5947
+ type: "string",
5948
+ minLength: MIN_REASON_CHARS,
5949
+ maxLength: MAX_REASON_CHARS,
5950
+ description: `Required for action='set'. Audit reason stored on the config-change record; at least ${MIN_REASON_CHARS} characters after trimming.`
5951
+ }
5952
+ },
5953
+ required: ["action", "runner_id"],
5954
+ additionalProperties: false
5955
+ };
5956
+ var description19 = "Gets or sets a paired runner's plane-delivered prepared-job mode (ADR-004 \xA7 11.1) via vo-control-plane GET/POST /api/v1/runner/prepared-job-mode. action='get' returns the stored config ({prepared_job_mode, revision, reason, updated_at, updated_by}, or null when unset); action='set' writes it with optimistic concurrency and returns the new config plus an audit_id. THE TARGET OPERATOR IS DERIVED FROM THE AUTHENTICATED PRINCIPAL, never from this input \u2014 an admin token therefore writes under operator 'admin' and does NOT reach a scoped operator's runner, so use the operator credential from `vo-mcp login`. 'prepared' is refused by the plane with 409 prepared_mode_not_flippable; 'get' is read-only while 'set' is a write and is gated to a stub under VO_ADMIN_CALLABLES_READONLY. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
5957
+ var SHAPE_HINT = `invalid input. Shape: { action: 'get' | 'set', runner_id: non-empty string \u2264${MAX_RUNNER_ID_CHARS} chars }. For action='set' also required: prepared_job_mode ('${PREPARED_JOB_MODES.join("' | '")}'), expected_revision (integer \u2265 0), reason (string \u2265${MIN_REASON_CHARS} chars after trim). Those three fields are NOT accepted with action='get'.`;
5958
+ function parseInput(v) {
5959
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
5960
+ const o = v;
5961
+ const action = o["action"];
5962
+ if (action !== "get" && action !== "set") return null;
5963
+ const rawRunnerId = o["runner_id"];
5964
+ if (typeof rawRunnerId !== "string") return null;
5965
+ const runner_id = rawRunnerId.trim();
5966
+ if (runner_id.length === 0 || runner_id.length > MAX_RUNNER_ID_CHARS) return null;
5967
+ const setOnly = ["prepared_job_mode", "expected_revision", "reason"];
5968
+ if (action === "get") {
5969
+ if (setOnly.some((key) => o[key] !== void 0)) return null;
5970
+ return { action, runner_id };
5971
+ }
5972
+ const mode = o["prepared_job_mode"];
5973
+ if (typeof mode !== "string") return null;
5974
+ if (!PREPARED_JOB_MODES.includes(mode)) return null;
5975
+ const revision = o["expected_revision"];
5976
+ if (typeof revision !== "number") return null;
5977
+ if (!Number.isInteger(revision) || revision < 0) return null;
5978
+ const rawReason = o["reason"];
5979
+ if (typeof rawReason !== "string") return null;
5980
+ const reason = rawReason.trim();
5981
+ if (reason.length < MIN_REASON_CHARS || reason.length > MAX_REASON_CHARS) return null;
5982
+ return {
5983
+ action,
5984
+ runner_id,
5985
+ prepared_job_mode: mode,
5986
+ expected_revision: revision,
5987
+ reason
5988
+ };
5989
+ }
5990
+ async function handlePreparedJobMode(deps, rawInput, _signal) {
5991
+ const input = parseInput(rawInput);
5992
+ if (!input) {
5993
+ throw invalidParams(TOOL_NAME19, SHAPE_HINT);
5994
+ }
5995
+ if (input.action === "get") {
5996
+ return buildCloudOrStubResponse({
5997
+ toolName: TOOL_NAME19,
5998
+ callableName: CALLABLE_NAME10,
5999
+ adminPath: PLANE_PATH,
6000
+ normalizedInput: { action: "get", runner_id: input.runner_id },
6001
+ // A GET carries no body; the runner id rides the query string.
6002
+ cloudBody: {},
6003
+ invokeOptions: { method: "GET", query: { runner_id: input.runner_id } },
6004
+ // The plane answers `{ok, config}` — not the `{ok, callable, result}`
6005
+ // admin-proxy envelope — so take the whole object with `ok` stripped.
6006
+ rawEnvelope: true,
6007
+ gateType: PREPARED_JOB_MODE_GATE_TYPE,
6008
+ stubReason: PREPARED_JOB_MODE_STUB_REASON,
6009
+ readOnly: true,
6010
+ deps
6011
+ });
6012
+ }
6013
+ return buildCloudOrStubResponse({
6014
+ toolName: TOOL_NAME19,
6015
+ callableName: CALLABLE_NAME10,
6016
+ adminPath: PLANE_PATH,
6017
+ normalizedInput: {
6018
+ action: "set",
6019
+ runner_id: input.runner_id,
6020
+ prepared_job_mode: input.prepared_job_mode,
6021
+ expected_revision: input.expected_revision,
6022
+ reason: input.reason
6023
+ },
6024
+ // Exactly the four fields of `updateRunnerPreparedJobConfigInputSchema`
6025
+ // (.strict()) — no `operator_id`, which the route would ignore anyway.
6026
+ cloudBody: {
6027
+ runner_id: input.runner_id,
6028
+ prepared_job_mode: input.prepared_job_mode,
6029
+ expected_revision: input.expected_revision,
6030
+ reason: input.reason
6031
+ },
6032
+ rawEnvelope: true,
6033
+ gateType: PREPARED_JOB_MODE_GATE_TYPE,
6034
+ stubReason: PREPARED_JOB_MODE_STUB_REASON,
6035
+ // A write: stays gated behind VO_ADMIN_CALLABLES_READONLY.
6036
+ readOnly: false,
6037
+ deps
5018
6038
  });
5019
6039
  }
5020
6040
 
@@ -5054,12 +6074,12 @@ function suggestedHandoffPath(session_id, isoTimestamp) {
5054
6074
  // src/tools/session/report-session-state.ts
5055
6075
  init_auth_token_source();
5056
6076
  init_credential_store();
5057
- var TOOL_NAME19 = "vo_report_session_state";
6077
+ var TOOL_NAME20 = "vo_report_session_state";
5058
6078
  var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
5059
6079
  var MAX_GOAL_CHARS = 500;
5060
6080
  var MAX_RECENT_FILES = 20;
5061
6081
  var MAX_RECENT_TOOLS = 50;
5062
- var inputSchema19 = {
6082
+ var inputSchema20 = {
5063
6083
  type: "object",
5064
6084
  properties: {
5065
6085
  operator_id: {
@@ -5104,7 +6124,7 @@ var inputSchema19 = {
5104
6124
  required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
5105
6125
  additionalProperties: false
5106
6126
  };
5107
- var description19 = "Reports per-session context-window utilization to VO and returns a directive: 'continue' (under 70%), 'prepare_handoff' (70-84%), or 'execute_handoff_now' (\u226585%). Implements V1 launch gate #9 (fleet context lifecycle management) per the official VO roadmap. Cloud-control-plane mode when VO_CONTROL_PLANE_URL plus a user/scoped VO credential (or legacy admin token) is available; auto-allocates the session on first report so interactive agents (Claude Code, Cursor, Codex, Continue) appear on the live fleet whiteboard. Stub-local fallback when cloud config is absent or fails. The response shape stays stable across modes (`backend_mode` field in the payload tells the caller which mode produced the verdict).";
6127
+ var description20 = "Reports per-session context-window utilization to AlgoHQ and returns a directive: 'continue' (under 70%), 'prepare_handoff' (70-84%), or 'execute_handoff_now' (\u226585%). Implements V1 launch gate #9 (fleet context lifecycle management) per the official AlgoHQ roadmap. Cloud-control-plane mode when VO_CONTROL_PLANE_URL plus a user/scoped HQ credential (or legacy admin token) is available; auto-allocates the session on first report so interactive agents (Claude Code, Cursor, Codex, Continue) appear on the live fleet whiteboard. Stub-local fallback when cloud config is absent or fails. The response shape stays stable across modes (`backend_mode` field in the payload tells the caller which mode produced the verdict).";
5108
6128
  function isStringArray2(v, maxItems) {
5109
6129
  if (!Array.isArray(v)) return false;
5110
6130
  if (v.length > maxItems) return false;
@@ -5242,7 +6262,7 @@ async function tryCloudReportState(cloud, input, fetchFn = fetch) {
5242
6262
  async function handleReportSessionState(deps, rawInput, _signal) {
5243
6263
  if (!isToolInput19(rawInput)) {
5244
6264
  throw invalidParams(
5245
- TOOL_NAME19,
6265
+ TOOL_NAME20,
5246
6266
  `invalid input. Required fields: operator_id (non-empty string), session_id (non-empty string), agent_type (one of: ${VALID_AGENT_TYPES.join(" | ")}), context_used_pct (number 0-100). Optional: current_goal (string \u2264${MAX_GOAL_CHARS} chars), recent_files_touched (string[] \u2264${MAX_RECENT_FILES}), recent_tool_uses (string[] \u2264${MAX_RECENT_TOOLS}).`
5247
6267
  );
5248
6268
  }
@@ -5273,125 +6293,770 @@ async function handleReportSessionState(deps, rawInput, _signal) {
5273
6293
  // src/tools/session/spawn-successor.ts
5274
6294
  init_common();
5275
6295
  import { spawn } from "node:child_process";
5276
- import { homedir as homedir4 } from "node:os";
5277
- import { join as join6 } from "node:path";
5278
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5279
- var TOOL_NAME20 = "vo_spawn_successor";
5280
- var MAX_HANDOFF_BYTES = 64e3;
5281
- var inputSchema20 = {
5282
- type: "object",
5283
- properties: {
5284
- handoff_path: {
5285
- type: "string",
5286
- description: "Path to the handoff doc to pre-inject. Default: the newest .md in ~/.vo/handoffs/."
5287
- },
5288
- goal: {
5289
- type: "string",
5290
- description: "Optional one-line goal override appended after the handoff."
5291
- },
5292
- cwd: {
5293
- type: "string",
5294
- description: "Working directory for the successor (default: the repo the handoff names, else process cwd)."
5295
- },
5296
- max_turns: {
5297
- type: "number",
5298
- description: "Optional --max-turns bound for the successor."
5299
- }
5300
- },
5301
- required: [],
5302
- additionalProperties: false
5303
- };
5304
- var description20 = "Mode B auto-handoff (roadmap \xA73.4): spawn a DETACHED headless `claude -p` successor with a handoff doc pre-injected into its prompt. Defaults to the newest handoff in ~/.vo/handoffs/. Returns {spawned, pid, log_path, handoff_path}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.";
5305
- function isToolInput20(v) {
5306
- if (typeof v !== "object" || v === null) return false;
5307
- const o = v;
5308
- if (o["handoff_path"] !== void 0 && typeof o["handoff_path"] !== "string") return false;
5309
- if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
5310
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
5311
- if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
5312
- return true;
6296
+ import { homedir as homedir5 } from "node:os";
6297
+ import { join as join7 } from "node:path";
6298
+ import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
6299
+
6300
+ // src/swarm/tier-binding.ts
6301
+ var SWARM_TIERS = Object.freeze([
6302
+ "tier1_subscription",
6303
+ "tier1_local",
6304
+ "tier2_user_key",
6305
+ "tier3_platform_key",
6306
+ "refused",
6307
+ "unresolved"
6308
+ ]);
6309
+ var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
6310
+ "tier1_subscription",
6311
+ "tier1_local",
6312
+ "tier2_user_key",
6313
+ "tier3_platform_key"
6314
+ ]);
6315
+ var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
6316
+ var MAX_BOUND_SUBAGENTS = 20;
6317
+ function isPositiveCap(cap) {
6318
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
6319
+ }
6320
+ function unresolvedBinding(swarmId, nowIso, reason) {
6321
+ return {
6322
+ schema_version: 1,
6323
+ swarm_id: swarmId,
6324
+ tier: "unresolved",
6325
+ agent: null,
6326
+ reason,
6327
+ exhausted_agents: [],
6328
+ subagent_budget: 0,
6329
+ spend_cap_usd: null,
6330
+ resolved_at: nowIso
6331
+ };
5313
6332
  }
5314
- function newestHandoff(dir = join6(homedir4(), ".vo", "handoffs")) {
6333
+ function serializeSwarmTierBinding(binding) {
6334
+ return JSON.stringify(binding);
6335
+ }
6336
+ function parseSwarmTierBinding(raw, nowIso) {
6337
+ if (typeof raw !== "string" || raw.trim().length === 0) {
6338
+ return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
6339
+ }
6340
+ let parsed;
5315
6341
  try {
5316
- const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join6(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
5317
- return entries.length > 0 && entries[0] ? join6(dir, entries[0].f) : null;
6342
+ parsed = JSON.parse(raw);
5318
6343
  } catch {
5319
- return null;
6344
+ return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
6345
+ }
6346
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
6347
+ return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
6348
+ }
6349
+ const o = parsed;
6350
+ const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
6351
+ if (o["schema_version"] !== 1) {
6352
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
6353
+ }
6354
+ const tier = o["tier"];
6355
+ if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
6356
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
6357
+ }
6358
+ const budget = o["subagent_budget"];
6359
+ const cap = o["spend_cap_usd"];
6360
+ const capNum = isPositiveCap(cap) ? cap : null;
6361
+ if (tier === "tier3_platform_key" && capNum === null) {
6362
+ return unresolvedBinding(
6363
+ swarmId,
6364
+ nowIso,
6365
+ "inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
6366
+ );
5320
6367
  }
6368
+ return {
6369
+ schema_version: 1,
6370
+ swarm_id: swarmId,
6371
+ tier,
6372
+ agent: typeof o["agent"] === "string" ? o["agent"] : null,
6373
+ reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
6374
+ exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
6375
+ subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
6376
+ spend_cap_usd: capNum,
6377
+ resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
6378
+ };
5321
6379
  }
5322
- var MANDATORY_READS = [
5323
- "CLAUDE.md + AGENTS.md + README.md (repo root)",
5324
- "docs/current/virtual-office-agent-charter.md, -operating-model.md, -test-architect.md",
5325
- "docs/current/evidence-grounded-consensus-testing.md",
5326
- "docs/vo/ADR-001-* (verify + sign, human approves merge; no autonomous bot-merge / headless triggers) + docs/vo/vo-adr-002-two-plane-moat.md",
5327
- "docs/vo/vo-roadmap-2026-05-26.md (read the Change log tail for current state)",
5328
- "the operator memory index ~/.claude/projects/C--Users-greyl/memory/MEMORY.md"
5329
- ];
5330
- function buildSuccessorPrompt(handoffMarkdown, goal) {
5331
- const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
5332
- const lines = [
5333
- "You are the SUCCESSOR agent for a Virtual Office lane. The previous session",
5334
- "exhausted its context and wrote the handoff below. Read it fully, verify its",
5335
- '"verification needed" items against live state (a handoff is a claim, not',
5336
- "evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
5337
- "",
5338
- "MANDATORY READS before writing any code (NOT all auto-loaded \u2014 open them):",
5339
- reads,
5340
- "",
5341
- "NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
5342
- "(verified-answer-only, no fake green); verify-before-act + human merge approval;",
5343
- "never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
5344
- "branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and VO changes update",
5345
- "the roadmap in the same PR.",
5346
- "",
5347
- "--- HANDOFF ---",
5348
- handoffMarkdown,
5349
- "--- END HANDOFF ---"
5350
- ];
5351
- if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
5352
- return lines.join("\n");
6380
+ function inheritSwarmTierBinding(env, nowIso) {
6381
+ return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
5353
6382
  }
5354
- function buildSuccessorArgs(maxTurns) {
5355
- const args = ["-p", "--permission-mode", "acceptEdits"];
5356
- if (Number.isInteger(maxTurns) && maxTurns > 0) {
5357
- args.push("--max-turns", String(maxTurns));
5358
- }
5359
- return args;
6383
+ function bindingEnvFragment(binding) {
6384
+ return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
5360
6385
  }
5361
- async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
5362
- if (!isToolInput20(rawInput)) {
5363
- throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
5364
- }
5365
- const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
5366
- if (!handoffPath || !existsSync4(handoffPath)) {
5367
- return jsonContent({
5368
- tool: TOOL_NAME20,
5369
- schema_version: 1,
5370
- payload: {
5371
- spawned: false,
5372
- reason: rawInput.handoff_path ? `handoff not found: ${rawInput.handoff_path}` : "no handoff docs in ~/.vo/handoffs \u2014 write one first (the 85% directive does this)"
5373
- }
5374
- });
6386
+ function childBindingEnvFragment(binding, allocatedCapUsd = null) {
6387
+ return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
6388
+ }
6389
+ function admitSubagentSpawn(binding, spawnsSoFar = 0) {
6390
+ if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
6391
+ return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
5375
6392
  }
5376
- const handoff = readFileSync6(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
6393
+ if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
6394
+ return {
6395
+ allowed: false,
6396
+ reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
6397
+ };
6398
+ }
6399
+ if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
6400
+ return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
6401
+ }
6402
+ if (spawnsSoFar >= binding.subagent_budget) {
6403
+ return {
6404
+ allowed: false,
6405
+ reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
6406
+ };
6407
+ }
6408
+ return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
6409
+ }
6410
+ function childBinding(binding, allocatedCapUsd = null) {
6411
+ const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
6412
+ const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
6413
+ return {
6414
+ ...binding,
6415
+ subagent_budget: Math.max(0, binding.subagent_budget - 1),
6416
+ // A child never carries more than its parent, whatever the ledger says: a
6417
+ // forged or hand-edited pool cannot inflate a descendant above the binding
6418
+ // it descends from.
6419
+ spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
6420
+ };
6421
+ }
6422
+ function agentBindingRefusal(binding, requestedAgent) {
6423
+ const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
6424
+ if (requested.length === 0) return null;
6425
+ if (binding.agent !== null && requested === binding.agent) return null;
6426
+ return `swarm '${binding.swarm_id}' is bound to agent '${binding.agent ?? "none"}' under tier '${binding.tier}'; a caller-supplied agent '${requested}' would move this fan-out onto a different payer \u2014 refusing (the tier is decided once, at admission, and an inherited binding cannot be renegotiated)`;
6427
+ }
6428
+
6429
+ // src/swarm/successor-launch.ts
6430
+ var AGENT_LAUNCH_SHAPES = Object.freeze({
6431
+ claude: {
6432
+ bin: "claude",
6433
+ baseArgs: ["-p", "--permission-mode", "acceptEdits"],
6434
+ enforcesMaxTurns: true,
6435
+ maxTurnsFlag: "--max-turns",
6436
+ windowsShellSafe: true
6437
+ },
6438
+ codex: {
6439
+ bin: "codex",
6440
+ baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
6441
+ enforcesMaxTurns: false,
6442
+ // `-` makes codex read the prompt from stdin (injection-safe), matching how
6443
+ // codex-runner.mjs already spawns it.
6444
+ trailingArgs: ["-"],
6445
+ // `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
6446
+ // unverified, so win32 refuses rather than risking a mangled sandbox flag.
6447
+ windowsShellSafe: false
6448
+ }
6449
+ });
6450
+ function resolveSuccessorLaunch(input) {
6451
+ const agent = typeof input.agent === "string" ? input.agent.trim() : "";
6452
+ if (!agent) {
6453
+ return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
6454
+ }
6455
+ const shape = AGENT_LAUNCH_SHAPES[agent];
6456
+ if (!shape) {
6457
+ const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
6458
+ return {
6459
+ ok: false,
6460
+ reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
6461
+ };
6462
+ }
6463
+ const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
6464
+ if (wantsMaxTurns && !shape.enforcesMaxTurns) {
6465
+ return {
6466
+ ok: false,
6467
+ reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
6468
+ };
6469
+ }
6470
+ const platform = input.platform ?? process.platform;
6471
+ if (platform === "win32" && !shape.windowsShellSafe) {
6472
+ return {
6473
+ ok: false,
6474
+ reason: `agent '${agent}' has an argv whose behaviour under Windows cmd.exe re-parsing is unverified \u2014 refusing rather than emitting a command line that may mean something else`
6475
+ };
6476
+ }
6477
+ const args = [...shape.baseArgs];
6478
+ if (wantsMaxTurns && shape.maxTurnsFlag) {
6479
+ args.push(shape.maxTurnsFlag, String(input.maxTurns));
6480
+ }
6481
+ if (shape.trailingArgs) args.push(...shape.trailingArgs);
6482
+ return { ok: true, agent, bin: shape.bin, args };
6483
+ }
6484
+
6485
+ // src/swarm/spawn-ledger.ts
6486
+ import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
6487
+ import { homedir as homedir4 } from "node:os";
6488
+ import { join as join6 } from "node:path";
6489
+ var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
6490
+ function resolveLedgerDir(env) {
6491
+ const override = env[SWARM_LEDGER_DIR_ENV];
6492
+ if (typeof override === "string" && override.trim().length > 0) return override.trim();
6493
+ return join6(homedir4(), ".vo", "swarm-ledger");
6494
+ }
6495
+ function sanitizeSwarmId(raw) {
6496
+ if (typeof raw !== "string") return null;
6497
+ const id = raw.trim();
6498
+ if (id.length === 0 || id.length > 128) return null;
6499
+ if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
6500
+ if (id === "." || id === "..") return null;
6501
+ return id;
6502
+ }
6503
+ var CEILING_FILE = "ceiling.json";
6504
+ function createExclusive(path4, contents) {
6505
+ let fd;
6506
+ try {
6507
+ fd = openSync(path4, "wx");
6508
+ } catch {
6509
+ return false;
6510
+ }
6511
+ try {
6512
+ writeFileSync3(fd, contents, "utf8");
6513
+ } finally {
6514
+ closeSync(fd);
6515
+ }
6516
+ return true;
6517
+ }
6518
+ function capToCents(cap) {
6519
+ return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
6520
+ }
6521
+ function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
6522
+ const path4 = join6(swarmDir, CEILING_FILE);
6523
+ const head = JSON.stringify({
6524
+ ceiling: proposedCeiling,
6525
+ cap_cents: proposedCapCents,
6526
+ recorded_at: nowIso
6527
+ });
6528
+ if (createExclusive(path4, head)) {
6529
+ return { ceiling: proposedCeiling, capCents: proposedCapCents };
6530
+ }
6531
+ let parsed;
6532
+ try {
6533
+ parsed = JSON.parse(readFileSync6(path4, "utf8"));
6534
+ } catch {
6535
+ return null;
6536
+ }
6537
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
6538
+ const record = parsed;
6539
+ const recorded = record["ceiling"];
6540
+ if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
6541
+ const recordedCap = record["cap_cents"];
6542
+ const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
6543
+ return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
6544
+ }
6545
+ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
6546
+ const id = sanitizeSwarmId(swarmId);
6547
+ if (id === null) {
6548
+ return {
6549
+ ok: false,
6550
+ reason: `swarm id ${JSON.stringify(swarmId)} is absent or unusable as a ledger key \u2014 refusing a spawn that cannot be counted against a fan-out ceiling`
6551
+ };
6552
+ }
6553
+ const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
6554
+ if (proposed < 1) {
6555
+ return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
6556
+ }
6557
+ const swarmDir = join6(dir, id);
6558
+ try {
6559
+ mkdirSync3(swarmDir, { recursive: true });
6560
+ } catch (err) {
6561
+ return {
6562
+ ok: false,
6563
+ reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
6564
+ };
6565
+ }
6566
+ const wantedCents = capToCents(proposedCapUsd);
6567
+ const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
6568
+ if (head === null) {
6569
+ return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
6570
+ }
6571
+ const { ceiling, capCents } = head;
6572
+ const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
6573
+ if (wantedCents > 0 && shareCents < 1) {
6574
+ return {
6575
+ ok: false,
6576
+ reason: `swarm '${id}' has no spend allowance left to debit (recorded pool $${(capCents / 100).toFixed(2)} across a ceiling of ${ceiling} leaves under one cent per spawn) \u2014 refusing a platform-billed spawn it cannot fund`
6577
+ };
6578
+ }
6579
+ for (let slot = 0; slot < ceiling; slot++) {
6580
+ const debitedCents = shareCents;
6581
+ const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
6582
+ const claimed = createExclusive(
6583
+ join6(swarmDir, `slot-${slot}.json`),
6584
+ JSON.stringify({
6585
+ slot,
6586
+ ceiling,
6587
+ pid: process.pid,
6588
+ claimed_at: nowIso,
6589
+ // The debit record. Durable and atomic with the claim: this file is
6590
+ // created with O_EXCL, so exactly one claimant ever writes this line.
6591
+ cap_cents_pool: capCents,
6592
+ cap_cents_debited: debitedCents,
6593
+ cap_cents_remaining: remainingCents
6594
+ })
6595
+ );
6596
+ if (claimed) {
6597
+ return {
6598
+ ok: true,
6599
+ slot,
6600
+ ceiling,
6601
+ remaining: ceiling - slot - 1,
6602
+ capUsd: debitedCents > 0 ? debitedCents / 100 : null,
6603
+ capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
6604
+ };
6605
+ }
6606
+ }
6607
+ return {
6608
+ ok: false,
6609
+ reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
6610
+ };
6611
+ };
6612
+
6613
+ // src/swarm/spawn-plan.ts
6614
+ function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
6615
+ const rawBinding = env[SWARM_TIER_BINDING_ENV];
6616
+ const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
6617
+ if (!hasBinding) {
6618
+ const explicit = input.agent?.trim();
6619
+ const resolved2 = resolveSuccessorLaunch({ agent: explicit || "claude", maxTurns: input.max_turns, platform });
6620
+ if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
6621
+ return {
6622
+ ok: true,
6623
+ bin: resolved2.bin,
6624
+ args: resolved2.args,
6625
+ agent: resolved2.agent,
6626
+ tier: "unbound",
6627
+ bound: false,
6628
+ env: {},
6629
+ slot: null,
6630
+ capUsd: null,
6631
+ capRemainingUsd: null
6632
+ };
6633
+ }
6634
+ const binding = inheritSwarmTierBinding(env, nowIso);
6635
+ const admission = admitSubagentSpawn(binding);
6636
+ if (!admission.allowed) {
6637
+ return { ok: false, reason: admission.reason, tier: binding.tier };
6638
+ }
6639
+ const agentRefusal = agentBindingRefusal(binding, input.agent);
6640
+ if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
6641
+ const resolved = resolveSuccessorLaunch({
6642
+ agent: binding.agent,
6643
+ maxTurns: input.max_turns,
6644
+ platform
6645
+ });
6646
+ if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
6647
+ const slot = claim({
6648
+ swarmId: binding.swarm_id,
6649
+ proposedCeiling: binding.subagent_budget,
6650
+ // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
6651
+ // child's cap is DEBITED from it below, not recomputed from this binding.
6652
+ proposedCapUsd: binding.spend_cap_usd,
6653
+ dir: resolveLedgerDir(env),
6654
+ nowIso
6655
+ });
6656
+ if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
6657
+ return {
6658
+ ok: true,
6659
+ bin: resolved.bin,
6660
+ args: resolved.args,
6661
+ agent: resolved.agent,
6662
+ tier: binding.tier,
6663
+ bound: true,
6664
+ // Re-export the same TIER with a DECREMENTED budget and the spend cap the
6665
+ // ledger just DEBITED. Exporting the binding verbatim (what this did before
6666
+ // #9312) meant the child re-read the full budget and every generation
6667
+ // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
6668
+ // bounded a chain but not a tree: three siblings each re-halved the parent's
6669
+ // untouched $50 and walked away with $75 between them.
6670
+ env: childBindingEnvFragment(binding, slot.capUsd),
6671
+ slot: slot.slot,
6672
+ capUsd: slot.capUsd,
6673
+ capRemainingUsd: slot.capRemainingUsd
6674
+ };
6675
+ }
6676
+
6677
+ // ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
6678
+ import { existsSync as existsSync4, realpathSync } from "node:fs";
6679
+ import { win32 as path3 } from "node:path";
6680
+ import { spawnSync as spawnSync2 } from "node:child_process";
6681
+ var NATIVE_CLAUDE_PARTS = [
6682
+ "node_modules",
6683
+ "@anthropic-ai",
6684
+ "claude-code",
6685
+ "bin",
6686
+ "claude.exe"
6687
+ ];
6688
+ function pathValue(env) {
6689
+ for (const key of ["Path", "PATH", "path"]) {
6690
+ if (typeof env?.[key] === "string") return env[key];
6691
+ }
6692
+ return "";
6693
+ }
6694
+ function cleanPathSegment(value) {
6695
+ const trimmed = String(value || "").trim();
6696
+ return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
6697
+ }
6698
+ function envValue(env, name) {
6699
+ const exact = env?.[name];
6700
+ if (typeof exact === "string") return exact.trim();
6701
+ const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
6702
+ return typeof env?.[key] === "string" ? env[key].trim() : "";
6703
+ }
6704
+ function userClaudeCandidates(bin, env) {
6705
+ if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
6706
+ const userProfile = envValue(env, "USERPROFILE");
6707
+ const appData = envValue(env, "APPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Roaming") : "");
6708
+ const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Local") : "");
6709
+ const candidates = [];
6710
+ if (appData) {
6711
+ const npmBin = path3.join(appData, "npm");
6712
+ candidates.push(
6713
+ path3.join(npmBin, "claude.exe"),
6714
+ path3.join(npmBin, "claude.cmd"),
6715
+ path3.join(npmBin, "claude.ps1"),
6716
+ path3.join(npmBin, "claude"),
6717
+ path3.join(npmBin, ...NATIVE_CLAUDE_PARTS)
6718
+ );
6719
+ }
6720
+ if (userProfile) candidates.push(path3.join(userProfile, ".local", "bin", "claude.exe"));
6721
+ if (localAppData) {
6722
+ candidates.push(
6723
+ path3.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
6724
+ path3.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
6725
+ );
6726
+ }
6727
+ return candidates;
6728
+ }
6729
+ function pathCandidates(bin, env) {
6730
+ if (path3.isAbsolute(bin) || /[\\/]/u.test(bin)) {
6731
+ return [path3.resolve(bin)];
6732
+ }
6733
+ const extension = path3.extname(bin);
6734
+ const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path3.join(directory, bin)] : [
6735
+ path3.join(directory, `${bin}.exe`),
6736
+ path3.join(directory, `${bin}.cmd`),
6737
+ path3.join(directory, `${bin}.ps1`),
6738
+ path3.join(directory, bin)
6739
+ ]);
6740
+ const seen = /* @__PURE__ */ new Set();
6741
+ return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
6742
+ const key = candidate.toLowerCase();
6743
+ if (seen.has(key)) return false;
6744
+ seen.add(key);
6745
+ return true;
6746
+ });
6747
+ }
6748
+ function canonicalExistingPath(candidate, exists, canonicalize2) {
6749
+ if (!exists(candidate)) return null;
6750
+ try {
6751
+ return canonicalize2(candidate);
6752
+ } catch {
6753
+ return null;
6754
+ }
6755
+ }
6756
+ function resolveWindowsClaudeExecutable({
6757
+ bin = "claude",
6758
+ env = process.env,
6759
+ exists = existsSync4,
6760
+ canonicalize: canonicalize2 = realpathSync
6761
+ } = {}) {
6762
+ const requested = String(bin || "").trim();
6763
+ if (!requested || requested.includes("\0")) {
6764
+ throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
6765
+ }
6766
+ for (const candidate of pathCandidates(requested, env)) {
6767
+ const found = canonicalExistingPath(candidate, exists, canonicalize2);
6768
+ if (!found) continue;
6769
+ if (path3.extname(found).toLowerCase() === ".exe") return found;
6770
+ const native = path3.join(path3.dirname(found), ...NATIVE_CLAUDE_PARTS);
6771
+ const resolvedNative = canonicalExistingPath(native, exists, canonicalize2);
6772
+ if (resolvedNative) return resolvedNative;
6773
+ }
6774
+ const error = new Error(
6775
+ `Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
6776
+ );
6777
+ error.code = "ENOENT";
6778
+ throw error;
6779
+ }
6780
+
6781
+ // src/swarm/successor-windows-exe.ts
6782
+ var resolveWindowsClaudeExe = (bin, env) => resolveWindowsClaudeExecutable({ bin, env });
6783
+ function resolveNativeWindowsExecutable(bin, env = process.env, resolve3 = resolveWindowsClaudeExe) {
6784
+ try {
6785
+ return { ok: true, bin: resolve3(bin, env) };
6786
+ } catch (error) {
6787
+ const message = error instanceof Error ? error.message : String(error);
6788
+ return {
6789
+ ok: false,
6790
+ reason: `could not resolve a native Windows executable for '${bin}': ${message}`
6791
+ };
6792
+ }
6793
+ }
6794
+
6795
+ // src/swarm/successor-liveness.ts
6796
+ import { statSync as statSync3 } from "node:fs";
6797
+ var DEFAULT_EARLY_EXIT_SEC = 10;
6798
+ var DEFAULT_NO_OUTPUT_SEC = 0;
6799
+ var DEFAULT_POLL_MS = 200;
6800
+ var KILL_ESCALATION_MS = 2e3;
6801
+ function positiveSeconds(raw, fallback) {
6802
+ if (raw === void 0) return fallback;
6803
+ const n = Number(raw.trim());
6804
+ return Number.isFinite(n) && n > 0 ? n : fallback;
6805
+ }
6806
+ function resolveLivenessConfigFromEnv(env = process.env, explicitOverride = {}) {
6807
+ const earlyExitMs = explicitOverride.earlyExitMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_EXIT_CHECK_SEC"], DEFAULT_EARLY_EXIT_SEC) * 1e3;
6808
+ const noOutputMs = explicitOverride.noOutputMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_OUTPUT_CHECK_SEC"], DEFAULT_NO_OUTPUT_SEC) * 1e3;
6809
+ return { earlyExitMs, noOutputMs };
6810
+ }
6811
+ function defaultStatLogBytes(path4) {
6812
+ try {
6813
+ return statSync3(path4).size;
6814
+ } catch {
6815
+ return 0;
6816
+ }
6817
+ }
6818
+ function defaultKillChild(child) {
6819
+ try {
6820
+ child.kill("SIGTERM");
6821
+ } catch {
6822
+ }
6823
+ const escalation = setTimeout(() => {
6824
+ try {
6825
+ child.kill("SIGKILL");
6826
+ } catch {
6827
+ }
6828
+ }, KILL_ESCALATION_MS);
6829
+ escalation.unref();
6830
+ }
6831
+ function checkSuccessorLiveness(child, logPath, config, deps = {}) {
6832
+ const statLogBytes = deps.statLogBytes ?? defaultStatLogBytes;
6833
+ const now = deps.now ?? Date.now;
6834
+ const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS;
6835
+ const killChild = deps.killChild ?? defaultKillChild;
6836
+ const outputGateEnabled = Number.isFinite(config.noOutputMs) && config.noOutputMs > 0;
6837
+ const startedAt = now();
6838
+ return new Promise((resolve3) => {
6839
+ let settled = false;
6840
+ let pollTimer = null;
6841
+ const onExit = (code, signal) => {
6842
+ const elapsedMs = now() - startedAt;
6843
+ finish({
6844
+ ok: false,
6845
+ reason: `child_exited_early: exit code ${code ?? "null"} signal ${signal ?? "none"} after ${elapsedMs}ms`,
6846
+ detail: { exitCode: code, signal, elapsedMs }
6847
+ });
6848
+ };
6849
+ const cleanup = () => {
6850
+ if (pollTimer !== null) clearInterval(pollTimer);
6851
+ child.off?.("exit", onExit);
6852
+ };
6853
+ const finish = (result) => {
6854
+ if (settled) return;
6855
+ settled = true;
6856
+ cleanup();
6857
+ if (!result.ok) {
6858
+ try {
6859
+ killChild(child);
6860
+ } catch {
6861
+ }
6862
+ }
6863
+ resolve3(result);
6864
+ };
6865
+ if (typeof child.exitCode === "number" || typeof child.signalCode === "string" && child.signalCode.length > 0) {
6866
+ finish({
6867
+ ok: false,
6868
+ reason: `child_exited_early: exit code ${child.exitCode ?? "null"} signal ${child.signalCode ?? "none"} before the liveness watch attached`,
6869
+ detail: { exitCode: child.exitCode ?? null, signal: child.signalCode ?? null, elapsedMs: 0 }
6870
+ });
6871
+ return;
6872
+ }
6873
+ child.on("exit", onExit);
6874
+ const tick = () => {
6875
+ if (settled) return;
6876
+ const elapsedMs = now() - startedAt;
6877
+ const outputSeen = outputGateEnabled ? statLogBytes(logPath) > 0 : true;
6878
+ if (outputSeen && elapsedMs >= config.earlyExitMs) {
6879
+ finish({ ok: true });
6880
+ return;
6881
+ }
6882
+ if (outputGateEnabled && !outputSeen && elapsedMs >= config.noOutputMs) {
6883
+ finish({
6884
+ ok: false,
6885
+ reason: `no_output: log stayed empty for ${elapsedMs}ms (limit ${config.noOutputMs}ms)`,
6886
+ detail: { elapsedMs, logPath }
6887
+ });
6888
+ }
6889
+ };
6890
+ pollTimer = setInterval(tick, pollIntervalMs);
6891
+ tick();
6892
+ });
6893
+ }
6894
+
6895
+ // src/tools/session/spawn-successor.ts
6896
+ var TOOL_NAME21 = "vo_spawn_successor";
6897
+ var MAX_HANDOFF_BYTES = 64e3;
6898
+ var inputSchema21 = {
6899
+ type: "object",
6900
+ properties: {
6901
+ handoff_path: {
6902
+ type: "string",
6903
+ description: "Path to the handoff doc to pre-inject. Default: the newest .md in ~/.vo/handoffs/."
6904
+ },
6905
+ goal: {
6906
+ type: "string",
6907
+ description: "Optional one-line goal override appended after the handoff."
6908
+ },
6909
+ cwd: {
6910
+ type: "string",
6911
+ description: "Working directory for the successor (default: the repo the handoff names, else process cwd)."
6912
+ },
6913
+ max_turns: {
6914
+ type: "number",
6915
+ description: "Optional --max-turns bound for the successor."
6916
+ },
6917
+ agent: {
6918
+ type: "string",
6919
+ description: `Which agent to spawn ('claude' | 'codex'). Normally omitted: the agent comes from the swarm tier binding inherited via ${SWARM_TIER_BINDING_ENV}. When a binding IS inherited this may only RESTATE the bound agent \u2014 an agent that contradicts the binding is REFUSED, because a different agent is a different payer and the payer was decided once, at admission.`
6920
+ }
6921
+ },
6922
+ required: [],
6923
+ additionalProperties: false
6924
+ };
6925
+ var RETIRED_COUNTER_INPUT = "spawns_so_far";
6926
+ var description21 = "Mode B auto-handoff (roadmap \xA73.4): spawn a DETACHED headless `claude -p` successor with a handoff doc pre-injected into its prompt. Defaults to the newest handoff in ~/.vo/handoffs/. Verifies the child is actually alive (survives an early-exit window; an optional log-output window is off by default) before reporting success. Returns {spawned, pid, log_path, handoff_path}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.";
6927
+ function isToolInput20(v) {
6928
+ if (typeof v !== "object" || v === null) return false;
6929
+ const o = v;
6930
+ if (o["handoff_path"] !== void 0 && typeof o["handoff_path"] !== "string") return false;
6931
+ if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
6932
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
6933
+ if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
6934
+ if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
6935
+ return true;
6936
+ }
6937
+ function retiredCounterRefusal(v) {
6938
+ if (typeof v !== "object" || v === null) return null;
6939
+ if (!(RETIRED_COUNTER_INPUT in v)) return null;
6940
+ return `\`${RETIRED_COUNTER_INPUT}\` is no longer accepted: a spawn counter supplied by the process being bounded bounds nothing, and an absent one read as zero. The fan-out ceiling is now enforced by the durable per-swarm spawn ledger; remove the field.`;
6941
+ }
6942
+ function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
6943
+ try {
6944
+ const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync4(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
6945
+ return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
6946
+ } catch {
6947
+ return null;
6948
+ }
6949
+ }
6950
+ var MANDATORY_READS = [
6951
+ "CLAUDE.md + AGENTS.md + README.md (repo root)",
6952
+ "docs/current/virtual-office-agent-charter.md, -operating-model.md, -test-architect.md",
6953
+ "docs/current/evidence-grounded-consensus-testing.md",
6954
+ "docs/vo/ADR-001-* (verify + sign, human approves merge; no autonomous bot-merge / headless triggers) + docs/vo/vo-adr-002-two-plane-moat.md",
6955
+ "docs/vo/vo-roadmap-2026-05-26.md (read the Change log tail for current state)",
6956
+ "the operator memory index ~/.claude/projects/C--Users-greyl/memory/MEMORY.md"
6957
+ ];
6958
+ function buildSuccessorPrompt(handoffMarkdown, goal) {
6959
+ const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
6960
+ const lines = [
6961
+ "You are the SUCCESSOR agent for an AlgoHQ lane. The previous session",
6962
+ "exhausted its context and wrote the handoff below. Read it fully, verify its",
6963
+ '"verification needed" items against live state (a handoff is a claim, not',
6964
+ "evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
6965
+ "",
6966
+ "MANDATORY READS before writing any code (NOT all auto-loaded \u2014 open them):",
6967
+ reads,
6968
+ "",
6969
+ "NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
6970
+ "(verified-answer-only, no fake green); verify-before-act + human merge approval;",
6971
+ "never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
6972
+ "branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and AlgoHQ changes update",
6973
+ "the roadmap in the same PR.",
6974
+ "",
6975
+ "--- HANDOFF ---",
6976
+ handoffMarkdown,
6977
+ "--- END HANDOFF ---"
6978
+ ];
6979
+ if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
6980
+ return lines.join("\n");
6981
+ }
6982
+ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
6983
+ const retired = retiredCounterRefusal(rawInput);
6984
+ if (retired !== null) throw invalidParams(TOOL_NAME21, retired);
6985
+ if (!isToolInput20(rawInput)) {
6986
+ throw invalidParams(TOOL_NAME21, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
6987
+ }
6988
+ const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
6989
+ if (!handoffPath || !existsSync5(handoffPath)) {
6990
+ return jsonContent({
6991
+ tool: TOOL_NAME21,
6992
+ schema_version: 1,
6993
+ payload: {
6994
+ spawned: false,
6995
+ reason: rawInput.handoff_path ? `handoff not found: ${rawInput.handoff_path}` : "no handoff docs in ~/.vo/handoffs \u2014 write one first (the 85% directive does this)"
6996
+ }
6997
+ });
6998
+ }
6999
+ const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
5377
7000
  const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
5378
- const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join6(homedir4(), ".vo", "successors");
5379
- mkdirSync3(logDir, { recursive: true });
5380
- const logPath = join6(logDir, `successor-${Date.now()}.log`);
5381
- const logFd = openSync(logPath, "a");
5382
- const child = spawnImpl("claude", buildSuccessorArgs(rawInput.max_turns), {
7001
+ const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
7002
+ if (!plan.ok) {
7003
+ return jsonContent({
7004
+ tool: TOOL_NAME21,
7005
+ schema_version: 1,
7006
+ payload: {
7007
+ spawned: false,
7008
+ reason: `swarm tier binding refused this spawn: ${plan.reason}`,
7009
+ tier: plan.tier,
7010
+ handoff_path: handoffPath
7011
+ }
7012
+ });
7013
+ }
7014
+ const platform = overrides.platform ?? process.platform;
7015
+ let resolvedBin = plan.bin;
7016
+ if (platform === "win32") {
7017
+ const resolution = resolveNativeWindowsExecutable(plan.bin, process.env, overrides.resolveWindowsExecutable);
7018
+ if (!resolution.ok) {
7019
+ return jsonContent({
7020
+ tool: TOOL_NAME21,
7021
+ schema_version: 1,
7022
+ payload: {
7023
+ spawned: false,
7024
+ reason: resolution.reason,
7025
+ agent: plan.agent,
7026
+ tier: plan.tier,
7027
+ handoff_path: handoffPath
7028
+ }
7029
+ });
7030
+ }
7031
+ resolvedBin = resolution.bin;
7032
+ }
7033
+ const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
7034
+ mkdirSync4(logDir, { recursive: true });
7035
+ const logPath = join7(logDir, `successor-${Date.now()}.log`);
7036
+ const logFd = openSync2(logPath, "a");
7037
+ const child = spawnImpl(resolvedBin, [...plan.args], {
5383
7038
  cwd: rawInput.cwd?.trim() || process.cwd(),
5384
7039
  detached: true,
5385
7040
  stdio: ["pipe", logFd, logFd],
5386
- // Windows: `claude` is a .cmd shim needs a shell to resolve. The prompt
5387
- // goes via STDIN below, never argv, so the shell never sees it.
5388
- shell: process.platform === "win32",
5389
- windowsHide: true
7041
+ // Never a shell: `resolvedBin` is either the bare platform-neutral name
7042
+ // (POSIX, resolved by the OS via PATH + shebang) or the native win32 exe
7043
+ // resolved above — routing either through cmd.exe/sh is the extra layer
7044
+ // a detached, unref'd child can lose silently (2026-08-16 incident).
7045
+ shell: false,
7046
+ windowsHide: true,
7047
+ windowsVerbatimArguments: false,
7048
+ // Carry the SAME binding to the child. Without this the successor inherits
7049
+ // no tier and re-resolves its own — which is the split-payer defect one
7050
+ // generation down.
7051
+ ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
5390
7052
  });
7053
+ closeSync2(logFd);
5391
7054
  let spawnError = null;
5392
7055
  child.on("error", (e) => {
5393
7056
  spawnError = e.message;
5394
7057
  });
7058
+ child.stdin.on?.("error", () => {
7059
+ });
5395
7060
  try {
5396
7061
  child.stdin.write(prompt);
5397
7062
  child.stdin.end();
@@ -5399,10 +7064,56 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
5399
7064
  }
5400
7065
  child.unref();
5401
7066
  await new Promise((r) => setTimeout(r, 150));
7067
+ if (spawnError) {
7068
+ return jsonContent({
7069
+ tool: TOOL_NAME21,
7070
+ schema_version: 1,
7071
+ payload: {
7072
+ spawned: false,
7073
+ reason: `spawn failed: ${spawnError}`,
7074
+ agent: plan.agent,
7075
+ tier: plan.tier,
7076
+ handoff_path: handoffPath
7077
+ }
7078
+ });
7079
+ }
7080
+ const checkLiveness = overrides.checkLiveness ?? ((c, p) => checkSuccessorLiveness(c, p, resolveLivenessConfigFromEnv(process.env, overrides.livenessConfig), overrides.livenessDeps));
7081
+ const liveness = await checkLiveness(child, logPath);
7082
+ if (!liveness.ok) {
7083
+ return jsonContent({
7084
+ tool: TOOL_NAME21,
7085
+ schema_version: 1,
7086
+ payload: {
7087
+ spawned: false,
7088
+ reason: liveness.reason,
7089
+ pid: child.pid ?? null,
7090
+ log_path: logPath,
7091
+ agent: plan.agent,
7092
+ tier: plan.tier,
7093
+ handoff_path: handoffPath
7094
+ }
7095
+ });
7096
+ }
5402
7097
  return jsonContent({
5403
- tool: TOOL_NAME20,
7098
+ tool: TOOL_NAME21,
5404
7099
  schema_version: 1,
5405
- payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, handoff_path: handoffPath } : { spawned: true, pid: child.pid ?? null, log_path: logPath, handoff_path: handoffPath }
7100
+ payload: {
7101
+ spawned: true,
7102
+ pid: child.pid ?? null,
7103
+ log_path: logPath,
7104
+ handoff_path: handoffPath,
7105
+ agent: plan.agent,
7106
+ tier: plan.tier,
7107
+ tier_bound: plan.bound,
7108
+ ledger_slot: plan.slot,
7109
+ // The debit, surfaced so an operator can reconcile a fan-out's spend
7110
+ // against the pool without reading the ledger directory by hand.
7111
+ ledger_cap_usd: plan.capUsd,
7112
+ ledger_cap_remaining_usd: plan.capRemainingUsd,
7113
+ // Additive (2026-08-17): true only once the child survived the
7114
+ // early-exit window (and the output window, when that gate is enabled).
7115
+ verified_alive: true
7116
+ }
5406
7117
  });
5407
7118
  }
5408
7119
 
@@ -5427,10 +7138,10 @@ function isKnownConciergePack(value) {
5427
7138
  }
5428
7139
 
5429
7140
  // src/tools/concierge/dispatch.ts
5430
- var TOOL_NAME21 = "vo_concierge_dispatch";
5431
- var CALLABLE_NAME10 = "voConciergeDispatch";
7141
+ var TOOL_NAME22 = "vo_concierge_dispatch";
7142
+ var CALLABLE_NAME11 = "voConciergeDispatch";
5432
7143
  var ADMIN_PATH10 = "/api/v1/admin/concierge/dispatch";
5433
- var inputSchema21 = {
7144
+ var inputSchema22 = {
5434
7145
  type: "object",
5435
7146
  properties: {
5436
7147
  pack: {
@@ -5445,7 +7156,7 @@ var inputSchema21 = {
5445
7156
  },
5446
7157
  additionalProperties: false
5447
7158
  };
5448
- var description21 = "Dispatches a provider-scoped knowledge pack (gcp | firebase | aws | cloudflare | vercel | netlify | tax | hybrid). Cross-vendor MCP equivalent of the /vo-concierge Claude-Code slash command. Route explicitly via `pack`, or via tenant.cloud_provider by passing `tenant_id`. Returns the pack's README (`readme_markdown`) + file index. In cloud mode, dispatches via vo-control-plane and returns `verdict: 'pass'` with the pack/directory data; without cloud config, returns `verdict: 'unimplemented'`.";
7159
+ var description22 = "Dispatches a provider-scoped knowledge pack (gcp | firebase | aws | cloudflare | vercel | netlify | tax | hybrid). Cross-vendor MCP equivalent of the /vo-concierge Claude-Code slash command. Route explicitly via `pack`, or via tenant.cloud_provider by passing `tenant_id`. Returns the pack's README (`readme_markdown`) + file index. In cloud mode, dispatches via vo-control-plane and returns `verdict: 'pass'` with the pack/directory data; without cloud config, returns `verdict: 'unimplemented'`.";
5449
7160
  function isToolInput21(v) {
5450
7161
  if (typeof v !== "object" || v === null) return false;
5451
7162
  const obj = v;
@@ -5456,13 +7167,13 @@ function isToolInput21(v) {
5456
7167
  async function handleConciergeDispatch(deps, rawInput, _signal) {
5457
7168
  if (!isToolInput21(rawInput)) {
5458
7169
  throw invalidParams(
5459
- TOOL_NAME21,
7170
+ TOOL_NAME22,
5460
7171
  "invalid input. Expected { pack?: string, tenant_id?: string }."
5461
7172
  );
5462
7173
  }
5463
7174
  if (rawInput.pack !== void 0 && rawInput.pack !== "" && !isKnownConciergePack(rawInput.pack)) {
5464
7175
  throw invalidParams(
5465
- TOOL_NAME21,
7176
+ TOOL_NAME22,
5466
7177
  `unknown pack: ${JSON.stringify(rawInput.pack)}. Known packs: ${KNOWN_CONCIERGE_PACKS.join(", ")}.`
5467
7178
  );
5468
7179
  }
@@ -5473,8 +7184,8 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
5473
7184
  if (rawInput.pack) cloudBody.pack = rawInput.pack;
5474
7185
  if (rawInput.tenant_id) cloudBody.tenantId = rawInput.tenant_id;
5475
7186
  return buildCloudOrStubResponse({
5476
- toolName: TOOL_NAME21,
5477
- callableName: CALLABLE_NAME10,
7187
+ toolName: TOOL_NAME22,
7188
+ callableName: CALLABLE_NAME11,
5478
7189
  adminPath: ADMIN_PATH10,
5479
7190
  normalizedInput,
5480
7191
  cloudBody,
@@ -5490,106 +7201,509 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
5490
7201
  });
5491
7202
  }
5492
7203
 
5493
- // src/server.ts
5494
- init_sync_config();
5495
-
5496
- // src/tools/memory/private-knowledge.ts
7204
+ // src/server.ts
7205
+ init_sync_config();
7206
+
7207
+ // src/tools/memory/private-knowledge.ts
7208
+ init_common();
7209
+ var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
7210
+ var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
7211
+ var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
7212
+ var STALE_TOOL_NAME = "vo_private_knowledge_stale";
7213
+ var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
7214
+ var PRECISION_CHAR_BUDGET = 12e3;
7215
+ var upsertInputSchema = {
7216
+ type: "object",
7217
+ properties: {
7218
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
7219
+ source_path: { type: "string", description: "Stable private source identifier; not exposed to other users." },
7220
+ title: { type: "string", description: 'Descriptive, retrieval-friendly title (e.g. "AlgoTax OCR redaction architecture", not "notes") \u2014 retrieval matches on it.' },
7221
+ content: { type: "string", description: "Private knowledge text to store server-side. Keep each entry tight and focused (~1-3 pages, under ~12k chars); split larger corpora into separate entries." }
7222
+ },
7223
+ required: ["knowledge_class", "source_path", "title", "content"],
7224
+ additionalProperties: false
7225
+ };
7226
+ var contextInputSchema = {
7227
+ type: "object",
7228
+ properties: {
7229
+ query: { type: "string" },
7230
+ limit: { type: "number", minimum: 1, maximum: 50 },
7231
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES }
7232
+ },
7233
+ required: ["query"],
7234
+ additionalProperties: false
7235
+ };
7236
+ var invalidateInputSchema = {
7237
+ type: "object",
7238
+ properties: {
7239
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
7240
+ source_path: { type: "string", minLength: 1, maxLength: 400, description: "Stable private source identifier of the entry to invalidate \u2014 must match the source_path used at upsert." }
7241
+ },
7242
+ required: ["knowledge_class", "source_path"],
7243
+ additionalProperties: false
7244
+ };
7245
+ var staleInputSchema = {
7246
+ type: "object",
7247
+ properties: {
7248
+ days: { type: "number", minimum: 1, maximum: 3650, description: "Window in days (default 90): entries at least this old that were never recalled into an agent context, or not within the window." },
7249
+ limit: { type: "number", minimum: 1, maximum: 500 }
7250
+ },
7251
+ additionalProperties: false
7252
+ };
7253
+ var staleDescription = `The FORGETTING REPORT: lists the authenticated operator\u2019s live private-knowledge entries that are at least N days old and have never been recalled into an agent context (or not within N days). Metadata only. SURFACES ONLY \u2014 never auto-invalidates or auto-merges: two memories that disagree may both have been right in different contexts, so you decide. Act on a candidate deliberately with ${INVALIDATE_TOOL_NAME}; recall counts come from ${CONTEXT_TOOL_NAME} reads that actually placed the entry into returned context.`;
7254
+ var upsertDescription = "Uploads or refreshes the authenticated operator\u2019s private cloud knowledge. Works for Claude, Codex, Cursor, and cowork clients via the same vo-mcp login credential. Returns metadata only, not raw stored content. PRECISION DISCIPLINE: keep each entry tight and focused (~1-3 pages) with a descriptive retrieval-friendly title \u2014 retrieval surfaces whole entries, so small dense entries beat bulk dumps. Split large corpora into focused entries, then run a retrieval self-test via vo_private_knowledge_context before relying on the knowledge.";
7255
+ var contextDescription = "Retrieves prompt-ready private knowledge context for the authenticated operator. Returns snippets/context only; no raw corpus download. Also the retrieval self-test surface: after upserting critical knowledge, query for it here and confirm the entry surfaces before trusting it in downstream work.";
7256
+ var invalidateDescription = `Soft-deletes one private-knowledge entry for the authenticated operator: closes the live entry\u2019s validity window (bi-temporal) so it stops surfacing in retrieval. Never destroys data \u2014 invalidated versions remain queryable server-side via include_invalidated. Identify the entry by the same { knowledge_class, source_path } used at upsert; a not_found response means no live entry matches. After invalidating, self-test via ${CONTEXT_TOOL_NAME} to confirm the entry no longer surfaces.`;
7257
+ function isStaleInput(value) {
7258
+ if (value === void 0 || value === null) return true;
7259
+ if (typeof value !== "object") return false;
7260
+ const input = value;
7261
+ if (input["days"] !== void 0 && typeof input["days"] !== "number") return false;
7262
+ if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
7263
+ return true;
7264
+ }
7265
+ function isKnowledgeClass(value) {
7266
+ return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
7267
+ }
7268
+ function isUpsertInput(value) {
7269
+ if (typeof value !== "object" || value === null) return false;
7270
+ const input = value;
7271
+ return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
7272
+ }
7273
+ function isInvalidateInput(value) {
7274
+ if (typeof value !== "object" || value === null) return false;
7275
+ const input = value;
7276
+ return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
7277
+ }
7278
+ function isContextInput(value) {
7279
+ if (typeof value !== "object" || value === null) return false;
7280
+ const input = value;
7281
+ if (typeof input["query"] !== "string") return false;
7282
+ if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
7283
+ if (input["knowledge_class"] !== void 0 && !isKnowledgeClass(input["knowledge_class"])) return false;
7284
+ return true;
7285
+ }
7286
+ async function getCloudAuth(fetchFn) {
7287
+ const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.replace(/\/+$/, "");
7288
+ if (!controlPlaneUrl) {
7289
+ return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
7290
+ }
7291
+ const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
7292
+ const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
7293
+ const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
7294
+ if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
7295
+ const token = await tokenSource.getToken();
7296
+ if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
7297
+ return { ok: true, controlPlaneUrl, token };
7298
+ }
7299
+ async function callPrivateKnowledge(path4, body, fetchFn) {
7300
+ const auth = await getCloudAuth(fetchFn);
7301
+ if (!auth.ok) return { ok: false, reason: auth.reason };
7302
+ const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
7303
+ method: "POST",
7304
+ headers: {
7305
+ authorization: `Bearer ${auth.token}`,
7306
+ "content-type": "application/json"
7307
+ },
7308
+ body: JSON.stringify(body)
7309
+ });
7310
+ const text = await response.text();
7311
+ let parsed;
7312
+ try {
7313
+ parsed = text ? JSON.parse(text) : null;
7314
+ } catch {
7315
+ parsed = null;
7316
+ }
7317
+ if (response.status < 200 || response.status >= 300) {
7318
+ return { ok: false, status: response.status, response: parsed ?? text };
7319
+ }
7320
+ return parsed;
7321
+ }
7322
+ async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
7323
+ if (!isUpsertInput(rawInput)) {
7324
+ throw invalidParams(UPSERT_TOOL_NAME, "expected { knowledge_class, source_path, title, content }.");
7325
+ }
7326
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private", rawInput, fetchFn);
7327
+ const envelope = {
7328
+ tool: UPSERT_TOOL_NAME,
7329
+ schema_version: 1,
7330
+ payload
7331
+ };
7332
+ if (rawInput.content.length > PRECISION_CHAR_BUDGET) {
7333
+ envelope.precision_note = `content is ${rawInput.content.length} chars (> ${PRECISION_CHAR_BUDGET}). Tight 1-3 page entries retrieve better \u2014 consider splitting into focused entries, then re-test retrieval via ${CONTEXT_TOOL_NAME}.`;
7334
+ }
7335
+ return jsonContent(envelope);
7336
+ }
7337
+ async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
7338
+ if (!isInvalidateInput(rawInput)) {
7339
+ throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
7340
+ }
7341
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
7342
+ return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
7343
+ }
7344
+ async function handlePrivateKnowledgeStale(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
7345
+ if (!isStaleInput(rawInput)) {
7346
+ throw invalidParams(STALE_TOOL_NAME, "expected { optional days, optional limit }.");
7347
+ }
7348
+ const auth = await getCloudAuth(fetchFn);
7349
+ if (!auth.ok) return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload: { ok: false, reason: auth.reason } });
7350
+ const params = new URLSearchParams();
7351
+ if (rawInput?.days !== void 0) params.set("days", String(Math.trunc(rawInput.days)));
7352
+ if (rawInput?.limit !== void 0) params.set("limit", String(Math.trunc(rawInput.limit)));
7353
+ const qs = params.toString();
7354
+ const response = await fetchFn(`${auth.controlPlaneUrl}/api/v1/knowledge/private/stale${qs ? `?${qs}` : ""}`, {
7355
+ method: "GET",
7356
+ headers: { authorization: `Bearer ${auth.token}` }
7357
+ });
7358
+ const text = await response.text();
7359
+ let parsed;
7360
+ try {
7361
+ parsed = text ? JSON.parse(text) : null;
7362
+ } catch {
7363
+ parsed = null;
7364
+ }
7365
+ const payload = response.status < 200 || response.status >= 300 ? { ok: false, status: response.status, response: parsed ?? text } : parsed;
7366
+ return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload });
7367
+ }
7368
+ async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
7369
+ if (!isContextInput(rawInput)) {
7370
+ throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
7371
+ }
7372
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private/context", rawInput, fetchFn);
7373
+ return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
7374
+ }
7375
+
7376
+ // src/tools/hq/whiteboard.ts
7377
+ init_auth_token_source();
7378
+ init_credential_store();
7379
+ init_common();
7380
+ var POST_TOOL_NAME = "hq_whiteboard_post";
7381
+ var READ_TOOL_NAME = "hq_whiteboard_read";
7382
+ var postDescription = "Post an append-only coordination note to the live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login; operator and tenant ownership are derived by the server.";
7383
+ var readDescription = "Read recent coordination notes from the caller's live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login and cannot widen tenant scope.";
7384
+ var postInputSchema = {
7385
+ type: "object",
7386
+ properties: {
7387
+ from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
7388
+ type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
7389
+ content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
7390
+ targetAgent: { type: "string", maxLength: 100 },
7391
+ tester: { type: "string", maxLength: 100 },
7392
+ tier: { type: "string", maxLength: 32 }
7393
+ },
7394
+ required: ["from", "type", "content"],
7395
+ additionalProperties: false
7396
+ };
7397
+ var readInputSchema = {
7398
+ type: "object",
7399
+ properties: {
7400
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
7401
+ since: { type: "string", description: "Optional ISO-8601 lower bound." },
7402
+ type: { type: "string", minLength: 1, maxLength: 64 }
7403
+ },
7404
+ additionalProperties: false
7405
+ };
7406
+ function resolveTimeoutMs() {
7407
+ const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
7408
+ return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
7409
+ }
7410
+ function isRecord(value) {
7411
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7412
+ }
7413
+ function onlyKeys(value, allowed) {
7414
+ return Object.keys(value).every((key) => allowed.includes(key));
7415
+ }
7416
+ function isBoundedString(value, min, max) {
7417
+ return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
7418
+ }
7419
+ function parsePostInput(value) {
7420
+ if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
7421
+ if (!isBoundedString(value["from"], 1, 100)) return null;
7422
+ if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
7423
+ if (!isBoundedString(value["content"], 1, 500)) return null;
7424
+ for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
7425
+ if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
7426
+ }
7427
+ return {
7428
+ from: value["from"].trim(),
7429
+ type: value["type"].trim(),
7430
+ content: value["content"].trim(),
7431
+ ...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
7432
+ ...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
7433
+ ...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
7434
+ };
7435
+ }
7436
+ function parseReadInput(value) {
7437
+ if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
7438
+ if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
7439
+ if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
7440
+ if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
7441
+ return {
7442
+ ...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
7443
+ ...typeof value["since"] === "string" ? { since: value["since"] } : {},
7444
+ ...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
7445
+ };
7446
+ }
7447
+ async function resolveCloud(fetchFn) {
7448
+ const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
7449
+ if (!url) return null;
7450
+ try {
7451
+ const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
7452
+ const token = await source?.getToken();
7453
+ return token ? { url, token } : null;
7454
+ } catch {
7455
+ return null;
7456
+ }
7457
+ }
7458
+ async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
7459
+ const cloud = await resolveCloud(fetchFn);
7460
+ if (!cloud) {
7461
+ return {
7462
+ ok: false,
7463
+ error: "hq_whiteboard_not_configured",
7464
+ message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
7465
+ };
7466
+ }
7467
+ const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
7468
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
7469
+ const query = new URLSearchParams();
7470
+ if (method === "GET") {
7471
+ const input = bodyOrQuery;
7472
+ query.set("limit", String(input.limit ?? 25));
7473
+ if (input.since) query.set("since", input.since);
7474
+ if (input.type) query.set("type", input.type);
7475
+ }
7476
+ try {
7477
+ const response = await fetchFn(
7478
+ `${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
7479
+ {
7480
+ method,
7481
+ headers: {
7482
+ Authorization: `Bearer ${cloud.token}`,
7483
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
7484
+ },
7485
+ ...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
7486
+ signal: requestSignal
7487
+ }
7488
+ );
7489
+ const text = await response.text();
7490
+ let payload;
7491
+ try {
7492
+ payload = JSON.parse(text);
7493
+ } catch {
7494
+ payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
7495
+ }
7496
+ if (!response.ok) {
7497
+ return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
7498
+ }
7499
+ return payload;
7500
+ } catch (error) {
7501
+ return {
7502
+ ok: false,
7503
+ error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
7504
+ message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
7505
+ };
7506
+ }
7507
+ }
7508
+ async function handleHqWhiteboardPost(_deps, rawInput, signal) {
7509
+ const input = parsePostInput(rawInput);
7510
+ if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
7511
+ return jsonContent(await callWhiteboard("POST", input, signal));
7512
+ }
7513
+ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
7514
+ const input = parseReadInput(rawInput);
7515
+ if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
7516
+ return jsonContent(await callWhiteboard("GET", input, signal));
7517
+ }
7518
+
7519
+ // src/tools/skills/skill-corpus.ts
7520
+ import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
7521
+ import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
7522
+
7523
+ // ../skill-registry/src/loader.ts
7524
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
7525
+ import { join as join12 } from "node:path";
7526
+ var InvalidSkillFrontmatterError = class extends Error {
7527
+ constructor(skillFile, reason) {
7528
+ super(`Invalid frontmatter in ${skillFile}: ${reason}`);
7529
+ this.skillFile = skillFile;
7530
+ this.reason = reason;
7531
+ }
7532
+ skillFile;
7533
+ reason;
7534
+ name = "InvalidSkillFrontmatterError";
7535
+ };
7536
+ var FRONTMATTER_DELIMITER = "---";
7537
+ function parseFrontmatter(rawInput, sourcePath) {
7538
+ const raw = rawInput.replace(/\r\n/g, "\n");
7539
+ if (!raw.startsWith(`${FRONTMATTER_DELIMITER}
7540
+ `)) {
7541
+ throw new InvalidSkillFrontmatterError(sourcePath, 'file does not start with frontmatter delimiter "---"');
7542
+ }
7543
+ const afterFirst = raw.slice(FRONTMATTER_DELIMITER.length + 1);
7544
+ const closingIdx = afterFirst.indexOf(`
7545
+ ${FRONTMATTER_DELIMITER}
7546
+ `);
7547
+ if (closingIdx === -1) {
7548
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing closing frontmatter delimiter "---"');
7549
+ }
7550
+ const frontmatterText = afterFirst.slice(0, closingIdx);
7551
+ const body = afterFirst.slice(closingIdx + `
7552
+ ${FRONTMATTER_DELIMITER}
7553
+ `.length);
7554
+ let name = "";
7555
+ let description24 = "";
7556
+ for (const line of frontmatterText.split("\n")) {
7557
+ const trimmed = line.trim();
7558
+ if (trimmed.length === 0) continue;
7559
+ const colonIdx = trimmed.indexOf(":");
7560
+ if (colonIdx === -1) continue;
7561
+ const key = trimmed.slice(0, colonIdx).trim();
7562
+ const value = trimmed.slice(colonIdx + 1).trim();
7563
+ if (key === "name") name = value;
7564
+ else if (key === "description") description24 = value;
7565
+ }
7566
+ if (name.length === 0) {
7567
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "name"');
7568
+ }
7569
+ if (description24.length === 0) {
7570
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "description"');
7571
+ }
7572
+ return { name, description: description24, body };
7573
+ }
7574
+ function loadSkillsFromDir(skillsDir) {
7575
+ const entries = readdirSync6(skillsDir);
7576
+ const skills = [];
7577
+ for (const entry of entries) {
7578
+ const entryPath = join12(skillsDir, entry);
7579
+ let stat;
7580
+ try {
7581
+ stat = statSync6(entryPath);
7582
+ } catch {
7583
+ continue;
7584
+ }
7585
+ if (!stat.isDirectory()) continue;
7586
+ const skillFile = join12(entryPath, "SKILL.md");
7587
+ let raw;
7588
+ try {
7589
+ raw = readFileSync14(skillFile, "utf8");
7590
+ } catch {
7591
+ continue;
7592
+ }
7593
+ const { name, description: description24, body } = parseFrontmatter(raw, skillFile);
7594
+ skills.push({ name, description: description24, body, sourcePath: skillFile });
7595
+ }
7596
+ return [...skills].sort((a, b) => a.name.localeCompare(b.name));
7597
+ }
7598
+
7599
+ // src/tools/skills/skill-corpus.ts
5497
7600
  init_common();
5498
- var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
5499
- var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
5500
- var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
5501
- var PRECISION_CHAR_BUDGET = 12e3;
5502
- var upsertInputSchema = {
7601
+ var LIST_TOOL_NAME = "vo_skill_list";
7602
+ var GET_TOOL_NAME = "vo_skill_get";
7603
+ var listDescription = "List the Algosuite skill corpus (name + trigger description for every skill). Call once near session start to learn which skills exist; then fetch the full instructions for a relevant skill with vo_skill_get. This is the same corpus Claude Code loads natively from .claude/skills \u2014 served over MCP so every vendor works from identical playbooks. Pass refresh:true to re-scan from disk.";
7604
+ var getDescription = "Fetch the full markdown instructions of one Algosuite skill by name. Follow the returned instructions for the current task the same way a native skill invocation would. Use vo_skill_list to discover skill names.";
7605
+ var listInputSchema = {
5503
7606
  type: "object",
5504
7607
  properties: {
5505
- knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
5506
- source_path: { type: "string", description: "Stable private source identifier; not exposed to other users." },
5507
- title: { type: "string", description: 'Descriptive, retrieval-friendly title (e.g. "AlgoTax OCR redaction architecture", not "notes") \u2014 retrieval matches on it.' },
5508
- content: { type: "string", description: "Private knowledge text to store server-side. Keep each entry tight and focused (~1-3 pages, under ~12k chars); split larger corpora into separate entries." }
7608
+ refresh: {
7609
+ type: "boolean",
7610
+ description: "Re-scan the skills directory instead of using the cached corpus."
7611
+ }
5509
7612
  },
5510
- required: ["knowledge_class", "source_path", "title", "content"],
5511
- additionalProperties: false
7613
+ required: []
5512
7614
  };
5513
- var contextInputSchema = {
7615
+ var getInputSchema = {
5514
7616
  type: "object",
5515
7617
  properties: {
5516
- query: { type: "string" },
5517
- limit: { type: "number", minimum: 1, maximum: 50 },
5518
- knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES }
7618
+ name: {
7619
+ type: "string",
7620
+ description: "Skill name exactly as returned by vo_skill_list."
7621
+ }
5519
7622
  },
5520
- required: ["query"],
5521
- additionalProperties: false
7623
+ required: ["name"]
5522
7624
  };
5523
- var upsertDescription = "Uploads or refreshes the authenticated operator\u2019s private cloud knowledge. Works for Claude, Codex, Cursor, and cowork clients via the same vo-mcp login credential. Returns metadata only, not raw stored content. PRECISION DISCIPLINE: keep each entry tight and focused (~1-3 pages) with a descriptive retrieval-friendly title \u2014 retrieval surfaces whole entries, so small dense entries beat bulk dumps. Split large corpora into focused entries, then run a retrieval self-test via vo_private_knowledge_context before relying on the knowledge.";
5524
- var contextDescription = "Retrieves prompt-ready private knowledge context for the authenticated operator. Returns snippets/context only; no raw corpus download. Also the retrieval self-test surface: after upserting critical knowledge, query for it here and confirm the entry surfaces before trusting it in downstream work.";
5525
- function isKnowledgeClass(value) {
5526
- return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
5527
- }
5528
- function isUpsertInput(value) {
5529
- if (typeof value !== "object" || value === null) return false;
5530
- const input = value;
5531
- return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
7625
+ var MAX_WALK_UP_LEVELS = 8;
7626
+ var cachedCorpus = null;
7627
+ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
7628
+ const override = env.VO_SKILLS_DIR;
7629
+ if (typeof override === "string" && override.length > 0) {
7630
+ const abs = isAbsolute(override) ? override : resolve2(startDir, override);
7631
+ return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
7632
+ }
7633
+ let dir = resolve2(startDir);
7634
+ for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
7635
+ const candidate = join13(dir, ".claude", "skills");
7636
+ if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
7637
+ const parent = dirname5(dir);
7638
+ if (parent === dir) break;
7639
+ dir = parent;
7640
+ }
7641
+ return null;
5532
7642
  }
5533
- function isContextInput(value) {
5534
- if (typeof value !== "object" || value === null) return false;
5535
- const input = value;
5536
- if (typeof input["query"] !== "string") return false;
5537
- if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
5538
- if (input["knowledge_class"] !== void 0 && !isKnowledgeClass(input["knowledge_class"])) return false;
5539
- return true;
7643
+ function loadCorpus() {
7644
+ const skillsDir = resolveSkillsDir();
7645
+ if (skillsDir === null) {
7646
+ return {
7647
+ skills: [],
7648
+ skillsDir: null,
7649
+ unavailableReason: "No skills directory found. Set VO_SKILLS_DIR or run inside a repo with .claude/skills."
7650
+ };
7651
+ }
7652
+ try {
7653
+ return { skills: loadSkillsFromDir(skillsDir), skillsDir, unavailableReason: null };
7654
+ } catch (err) {
7655
+ const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
7656
+ return { skills: [], skillsDir, unavailableReason: message };
7657
+ }
5540
7658
  }
5541
- async function getCloudAuth(fetchFn) {
5542
- const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.replace(/\/+$/, "");
5543
- if (!controlPlaneUrl) {
5544
- return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
7659
+ function getCorpus(refresh) {
7660
+ if (refresh || cachedCorpus === null) {
7661
+ cachedCorpus = loadCorpus();
5545
7662
  }
5546
- const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
5547
- const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
5548
- const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
5549
- if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
5550
- const token = await tokenSource.getToken();
5551
- if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
5552
- return { ok: true, controlPlaneUrl, token };
7663
+ return cachedCorpus;
5553
7664
  }
5554
- async function callPrivateKnowledge(path3, body, fetchFn) {
5555
- const auth = await getCloudAuth(fetchFn);
5556
- if (!auth.ok) return { ok: false, reason: auth.reason };
5557
- const response = await fetchFn(`${auth.controlPlaneUrl}${path3}`, {
5558
- method: "POST",
5559
- headers: {
5560
- authorization: `Bearer ${auth.token}`,
5561
- "content-type": "application/json"
5562
- },
5563
- body: JSON.stringify(body)
7665
+ async function handleSkillList(_deps, rawInput) {
7666
+ const input = rawInput ?? {};
7667
+ const refresh = input.refresh === true;
7668
+ const corpus = getCorpus(refresh);
7669
+ return jsonContent({
7670
+ corpus_available: corpus.unavailableReason === null,
7671
+ skills_dir: corpus.skillsDir,
7672
+ unavailable_reason: corpus.unavailableReason,
7673
+ skill_count: corpus.skills.length,
7674
+ skills: corpus.skills.map((s) => ({ name: s.name, description: s.description }))
5564
7675
  });
5565
- const text = await response.text();
5566
- const parsed = text ? JSON.parse(text) : null;
5567
- if (response.status < 200 || response.status >= 300) {
5568
- return { ok: false, status: response.status, response: parsed ?? text };
5569
- }
5570
- return parsed;
5571
7676
  }
5572
- async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
5573
- if (!isUpsertInput(rawInput)) {
5574
- throw invalidParams(UPSERT_TOOL_NAME, "expected { knowledge_class, source_path, title, content }.");
7677
+ async function handleSkillGet(_deps, rawInput) {
7678
+ const input = rawInput ?? {};
7679
+ if (typeof input.name !== "string" || input.name.trim().length === 0) {
7680
+ throw invalidParams(GET_TOOL_NAME, 'input field "name" (non-empty string) is required');
5575
7681
  }
5576
- const payload = await callPrivateKnowledge("/api/v1/knowledge/private", rawInput, fetchFn);
5577
- const envelope = {
5578
- tool: UPSERT_TOOL_NAME,
5579
- schema_version: 1,
5580
- payload
5581
- };
5582
- if (rawInput.content.length > PRECISION_CHAR_BUDGET) {
5583
- envelope.precision_note = `content is ${rawInput.content.length} chars (> ${PRECISION_CHAR_BUDGET}). Tight 1-3 page entries retrieve better \u2014 consider splitting into focused entries, then re-test retrieval via ${CONTEXT_TOOL_NAME}.`;
7682
+ const requested = input.name.trim();
7683
+ const corpus = getCorpus(false);
7684
+ if (corpus.unavailableReason !== null) {
7685
+ return jsonContent({
7686
+ corpus_available: false,
7687
+ unavailable_reason: corpus.unavailableReason,
7688
+ skill: null
7689
+ });
5584
7690
  }
5585
- return jsonContent(envelope);
5586
- }
5587
- async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
5588
- if (!isContextInput(rawInput)) {
5589
- throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
7691
+ const skill = corpus.skills.find((s) => s.name === requested);
7692
+ if (skill === void 0) {
7693
+ throw invalidParams(
7694
+ GET_TOOL_NAME,
7695
+ `unknown skill "${requested}". Known skills: ${corpus.skills.map((s) => s.name).join(", ")}`
7696
+ );
5590
7697
  }
5591
- const payload = await callPrivateKnowledge("/api/v1/knowledge/private/context", rawInput, fetchFn);
5592
- return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
7698
+ return jsonContent({
7699
+ corpus_available: true,
7700
+ skill: {
7701
+ name: skill.name,
7702
+ description: skill.description,
7703
+ instructions: skill.body,
7704
+ source_path: skill.sourcePath
7705
+ }
7706
+ });
5593
7707
  }
5594
7708
 
5595
7709
  // src/server.ts
@@ -5745,7 +7859,7 @@ function buildToolRegistry() {
5745
7859
  description: description19,
5746
7860
  inputSchema: inputSchema19
5747
7861
  },
5748
- handler: handleReportSessionState
7862
+ handler: handlePreparedJobMode
5749
7863
  },
5750
7864
  [TOOL_NAME20]: {
5751
7865
  definition: {
@@ -5753,7 +7867,7 @@ function buildToolRegistry() {
5753
7867
  description: description20,
5754
7868
  inputSchema: inputSchema20
5755
7869
  },
5756
- handler: handleSpawnSuccessor
7870
+ handler: handleReportSessionState
5757
7871
  },
5758
7872
  [TOOL_NAME21]: {
5759
7873
  definition: {
@@ -5761,7 +7875,7 @@ function buildToolRegistry() {
5761
7875
  description: description21,
5762
7876
  inputSchema: inputSchema21
5763
7877
  },
5764
- handler: handleConciergeDispatch
7878
+ handler: handleSpawnSuccessor
5765
7879
  },
5766
7880
  [TOOL_NAME22]: {
5767
7881
  definition: {
@@ -5769,6 +7883,14 @@ function buildToolRegistry() {
5769
7883
  description: description22,
5770
7884
  inputSchema: inputSchema22
5771
7885
  },
7886
+ handler: handleConciergeDispatch
7887
+ },
7888
+ [TOOL_NAME23]: {
7889
+ definition: {
7890
+ name: TOOL_NAME23,
7891
+ description: description23,
7892
+ inputSchema: inputSchema23
7893
+ },
5772
7894
  handler: handleSyncConfig
5773
7895
  },
5774
7896
  [UPSERT_TOOL_NAME]: {
@@ -5786,11 +7908,59 @@ function buildToolRegistry() {
5786
7908
  inputSchema: contextInputSchema
5787
7909
  },
5788
7910
  handler: handlePrivateKnowledgeContext
7911
+ },
7912
+ [INVALIDATE_TOOL_NAME]: {
7913
+ definition: {
7914
+ name: INVALIDATE_TOOL_NAME,
7915
+ description: invalidateDescription,
7916
+ inputSchema: invalidateInputSchema
7917
+ },
7918
+ handler: handlePrivateKnowledgeInvalidate
7919
+ },
7920
+ [STALE_TOOL_NAME]: {
7921
+ definition: {
7922
+ name: STALE_TOOL_NAME,
7923
+ description: staleDescription,
7924
+ inputSchema: staleInputSchema
7925
+ },
7926
+ handler: handlePrivateKnowledgeStale
7927
+ },
7928
+ [POST_TOOL_NAME]: {
7929
+ definition: {
7930
+ name: POST_TOOL_NAME,
7931
+ description: postDescription,
7932
+ inputSchema: postInputSchema
7933
+ },
7934
+ handler: handleHqWhiteboardPost
7935
+ },
7936
+ [READ_TOOL_NAME]: {
7937
+ definition: {
7938
+ name: READ_TOOL_NAME,
7939
+ description: readDescription,
7940
+ inputSchema: readInputSchema
7941
+ },
7942
+ handler: handleHqWhiteboardRead
7943
+ },
7944
+ [LIST_TOOL_NAME]: {
7945
+ definition: {
7946
+ name: LIST_TOOL_NAME,
7947
+ description: listDescription,
7948
+ inputSchema: listInputSchema
7949
+ },
7950
+ handler: handleSkillList
7951
+ },
7952
+ [GET_TOOL_NAME]: {
7953
+ definition: {
7954
+ name: GET_TOOL_NAME,
7955
+ description: getDescription,
7956
+ inputSchema: getInputSchema
7957
+ },
7958
+ handler: handleSkillGet
5789
7959
  }
5790
7960
  };
5791
7961
  }
5792
7962
  function createServer(options) {
5793
- const sessionId = options.sessionId ?? randomUUID2();
7963
+ const sessionId = options.sessionId ?? randomUUID3();
5794
7964
  const mode = createLocalMode();
5795
7965
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
5796
7966
  const server = new Server(
@@ -5839,9 +8009,9 @@ function createServer(options) {
5839
8009
  }
5840
8010
 
5841
8011
  // src/cache/sqlite-cache.ts
5842
- import { createHash as createHash3 } from "node:crypto";
5843
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync5 } from "node:fs";
5844
- import { dirname as dirname5 } from "node:path";
8012
+ import { createHash as createHash4 } from "node:crypto";
8013
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
8014
+ import { dirname as dirname6 } from "node:path";
5845
8015
  import { DatabaseSync } from "node:sqlite";
5846
8016
 
5847
8017
  // src/cache/canonicalize.ts
@@ -5886,7 +8056,7 @@ function normalizeString(s) {
5886
8056
  function createSqliteCache(options) {
5887
8057
  const fileBacked = options.dbPath !== ":memory:";
5888
8058
  if (fileBacked) {
5889
- mkdirSync5(dirname5(options.dbPath), { recursive: true, mode: 448 });
8059
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
5890
8060
  }
5891
8061
  const versionNamespace = options.cacheVersionNamespace ?? "";
5892
8062
  const db = new DatabaseSync(options.dbPath);
@@ -5917,7 +8087,7 @@ function createSqliteCache(options) {
5917
8087
  return {
5918
8088
  keyFor(toolName, input, opts) {
5919
8089
  const canonical = canonicalize(input, opts);
5920
- const hash = createHash3("sha256");
8090
+ const hash = createHash4("sha256");
5921
8091
  if (versionNamespace.length > 0) {
5922
8092
  hash.update(versionNamespace);
5923
8093
  hash.update("|");
@@ -6012,7 +8182,7 @@ function createStubRatchetClient() {
6012
8182
  let m;
6013
8183
  while ((m = pat.regex.exec(req.source)) !== null) {
6014
8184
  findings.push({
6015
- line_excerpt: clip(m[0], 80),
8185
+ line_excerpt: clip2(m[0], 80),
6016
8186
  severity: pat.severity,
6017
8187
  code: pat.code,
6018
8188
  message: pat.message
@@ -6044,7 +8214,7 @@ function createStubRatchetClient() {
6044
8214
  }
6045
8215
  };
6046
8216
  }
6047
- function clip(s, n) {
8217
+ function clip2(s, n) {
6048
8218
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
6049
8219
  }
6050
8220
  function buildSummary2(args) {
@@ -6055,7 +8225,7 @@ function buildSummary2(args) {
6055
8225
  // src/consensus/engine-client.ts
6056
8226
  init_events_writer();
6057
8227
  init_common();
6058
- import { randomUUID as randomUUID3 } from "node:crypto";
8228
+ import { randomUUID as randomUUID4 } from "node:crypto";
6059
8229
 
6060
8230
  // src/consensus/null-client.ts
6061
8231
  var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
@@ -6078,63 +8248,59 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
6078
8248
  }
6079
8249
 
6080
8250
  // src/consensus/meta-model-caller.ts
6081
- var META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
6082
8251
  var META_CONSENSUS_MODEL = "muse-spark-1.1";
6083
- var META_MODEL_API_KEY_ENV = "MODEL_API_KEY";
6084
- var META_MODEL_API_KEY_ALIAS = "META_API";
6085
- function resolveMetaKey(env) {
6086
- return String(env[META_MODEL_API_KEY_ENV] || env[META_MODEL_API_KEY_ALIAS] || "").trim();
6087
- }
6088
- function positiveMaxTokens(value) {
6089
- const parsed = Math.floor(Number(value));
6090
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 2048;
6091
- }
6092
8252
  function createMetaModelCaller(options = {}) {
6093
- const fetchImpl = options.fetchImpl ?? fetch;
6094
- const envSource = options.envSource ?? process.env;
6095
- const reasoningEffort = options.reasoningEffort ?? "high";
6096
- return async function callMetaWithMetrics2(prompt, systemPrompt, model, maxTokens, _privacyOptions, signal) {
6097
- const key = resolveMetaKey(envSource);
6098
- if (!key) throw new Error(`Missing ${META_MODEL_API_KEY_ENV} for Meta Model API`);
6099
- const messages = [
6100
- ...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
6101
- { role: "user", content: prompt }
6102
- ];
6103
- const response = await fetchImpl(`${META_MODEL_API_BASE_URL}/chat/completions`, {
6104
- method: "POST",
6105
- headers: {
6106
- Authorization: `Bearer ${key}`,
6107
- "Content-Type": "application/json"
6108
- },
6109
- body: JSON.stringify({
6110
- model: model || META_CONSENSUS_MODEL,
6111
- messages,
6112
- max_tokens: positiveMaxTokens(maxTokens),
6113
- reasoning_effort: reasoningEffort
6114
- }),
6115
- signal
6116
- });
6117
- const payload = await response.json();
6118
- if (!response.ok) {
6119
- const message = String(payload.error?.message || response.statusText || "request failed").slice(0, 500);
6120
- throw Object.assign(new Error(`Meta Model API ${response.status}: ${message}`), { status: response.status });
6121
- }
6122
- const content = payload.choices?.[0]?.message?.content;
6123
- if (typeof content !== "string" || !content.trim()) {
6124
- throw new Error(`Meta Model API returned no text (finish=${payload.choices?.[0]?.finish_reason || "unknown"})`);
6125
- }
6126
- const inputTokens = Number(payload.usage?.prompt_tokens || 0);
6127
- const outputTokens = Number(payload.usage?.completion_tokens || 0);
6128
- return {
6129
- content,
6130
- inputTokens,
6131
- outputTokens,
6132
- totalTokens: Number(payload.usage?.total_tokens || inputTokens + outputTokens)
6133
- };
8253
+ void options;
8254
+ return async function callMetaWithMetrics2() {
8255
+ throw new Error(
8256
+ "Muse Spark direct consensus is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
8257
+ );
6134
8258
  };
6135
8259
  }
6136
8260
  var callMetaWithMetrics = createMetaModelCaller();
6137
8261
 
8262
+ // src/consensus/consensus-panel.ts
8263
+ var VO_MCP_CONSENSUS_PANEL = {
8264
+ // claude-opus-5 (2026-07-24). Opus 4.7 was STRICTLY DOMINATED, not merely old:
8265
+ // Opus 5 is $5/$25 per MTok vs Opus 4.7's $15/$75 — a 3x cost cut on this slot,
8266
+ // corroborated by our own catalog (constants/pricing/sciencePricing.ts prices
8267
+ // claude-opus-5 at 0.010 vs claude-opus-4-7 at 0.030) — AND the same 2026-07-24
8268
+ // release note REMOVED fast mode from Opus 4.7 outright: `speed: "fast"` now
8269
+ // returns an error there rather than degrading, unlike the Opus 4.6 removal.
8270
+ // Verified served: GET /v1/models/claude-opus-5 -> HTTP 200 (2026-07-30).
8271
+ //
8272
+ // Claude-5 API safety checked before this swap: Opus 5 rejects `temperature` /
8273
+ // `top_p` / `top_k` and manual `thinking.budget_tokens` with HTTP 400. Neither
8274
+ // the consensus-engine Anthropic adapter nor functions-shared `callAnthropic`
8275
+ // sends any of them, and buildAdaptiveThinking emits `thinking: {type:'adaptive'}`
8276
+ // (the supported form) — so this swap cannot 400.
8277
+ anthropic: "claude-opus-5",
8278
+ // gpt-5.6-terra (GA 2026-07-09; −20% price cut 2026-07-30). NOTE the real IDs
8279
+ // are tiered — `gpt-5.6-sol` / `-terra` / `-luna`; there is NO bare `gpt-5.6`
8280
+ // alias (verified against the served model list, 2026-07-30). Terra is the
8281
+ // cost/capability balance point and the right default for a judgment panel;
8282
+ // Sol is available if verdict quality ever needs it.
8283
+ openai: "gpt-5.6-terra",
8284
+ // gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
8285
+ // callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
8286
+ // Flash is also ~10x cheaper. 2026-06-02.
8287
+ google: "gemini-2.5-flash",
8288
+ deepseek: "deepseek-chat",
8289
+ // Muse Spark identity is owned by meta-model-caller.ts (single source of
8290
+ // truth for the meta slot); re-exported here so the panel stays complete.
8291
+ meta: META_CONSENSUS_MODEL
8292
+ };
8293
+ function getVoMcpConsensusPanel(panel = VO_MCP_CONSENSUS_PANEL) {
8294
+ for (const [provider, modelId] of Object.entries(panel)) {
8295
+ if (typeof modelId !== "string" || modelId.trim().length === 0) {
8296
+ throw new Error(
8297
+ `getVoMcpConsensusPanel: panel slot "${provider}" has a missing or blank model ID`
8298
+ );
8299
+ }
8300
+ }
8301
+ return panel;
8302
+ }
8303
+
6138
8304
  // src/consensus/engine-options.ts
6139
8305
  var AGREEMENT_GATE_ENV_VAR = "VO_CONSENSUS_AGREEMENT_GATE";
6140
8306
  function isTruthyFlag(raw) {
@@ -6188,6 +8354,14 @@ function shadowEnabled(env) {
6188
8354
  const norm = raw.trim().toLowerCase();
6189
8355
  return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
6190
8356
  }
8357
+ var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
8358
+ function resolveMinResponders(env) {
8359
+ const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
8360
+ if (raw === void 0 || raw.trim() === "") return 2;
8361
+ const parsed = Number.parseInt(raw.trim(), 10);
8362
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
8363
+ return parsed;
8364
+ }
6191
8365
  function mapShadowSynthesis(s) {
6192
8366
  if (s === void 0) return void 0;
6193
8367
  return {
@@ -6337,9 +8511,11 @@ function createEngineConsensusClient(options) {
6337
8511
  ...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
6338
8512
  ...options.env !== void 0 ? { env: options.env } : {}
6339
8513
  });
8514
+ const minResponders = resolveMinResponders(options.env);
6340
8515
  const engineOptions = {
6341
8516
  panel,
6342
8517
  ...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
8518
+ ...minResponders !== void 0 ? { min_responders: minResponders } : {},
6343
8519
  ...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
6344
8520
  // Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
6345
8521
  // Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
@@ -6348,7 +8524,7 @@ function createEngineConsensusClient(options) {
6348
8524
  shadow_synthesis: { enabled: shadowEnabled(options.env) }
6349
8525
  };
6350
8526
  const sources = request.source_urls;
6351
- const useSourceGrounded = sources !== void 0 && sources.length > 0 && typeof engine.runSourceGroundedConsensus === "function";
8527
+ const useSourceGrounded = sources !== void 0 && sources.length > 0;
6352
8528
  let response;
6353
8529
  let sourceExtras;
6354
8530
  if (useSourceGrounded) {
@@ -6392,8 +8568,13 @@ function createEngineConsensusClient(options) {
6392
8568
  synthesized_verdict: response.synthesized_verdict,
6393
8569
  per_model_verdicts: response.per_model_verdicts,
6394
8570
  degraded: response.degraded,
8571
+ ...response.quorum_failed === true ? { quorum_failed: true } : {},
6395
8572
  duration_ms: response.duration_ms,
6396
8573
  engine_version: response.engine_version,
8574
+ // Cumulative cross-round inference usage (B44-3). Absent when no panel
8575
+ // member reported usage; forwarded verbatim — the aggregator prefers it
8576
+ // over summing final-round verdicts (which under-reports deliberation).
8577
+ ...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
6397
8578
  // Phase 2 Lane D-1 — forward escalation signal when present. The
6398
8579
  // source-grounded layer's own escalation (from the citation grade)
6399
8580
  // takes precedence when set, else the synthesizer's.
@@ -6403,6 +8584,10 @@ function createEngineConsensusClient(options) {
6403
8584
  ...mapFanOutDiagnostics(response.fan_out_diagnostics) !== void 0 ? { fan_out_diagnostics: mapFanOutDiagnostics(response.fan_out_diagnostics) } : {},
6404
8585
  // Stage A7-shadow — adaptive-vs-incumbent comparison (PII-free; present iff shadow on).
6405
8586
  ...mapShadowSynthesis(response.shadow_synthesis) !== void 0 ? { shadow_synthesis: mapShadowSynthesis(response.shadow_synthesis) } : {},
8587
+ // Critique-uptake (2026-07-20 red-team fix) — verifier-critique
8588
+ // visibility report; previously computed by the engine on every
8589
+ // call but dropped at this boundary.
8590
+ ...response.critique_uptake !== void 0 ? { critique_uptake: response.critique_uptake } : {},
6406
8591
  // Source-grounded additive outputs (Tier-4 features).
6407
8592
  ...useSourceGrounded ? { source_grounded: true } : {},
6408
8593
  ...sourceExtras?.citation_grade !== void 0 ? { citation_grade: sourceExtras.citation_grade } : {},
@@ -6418,27 +8603,12 @@ function createEngineConsensusClient(options) {
6418
8603
  }
6419
8604
  };
6420
8605
  }
6421
- var DEFAULT_MODELS = {
6422
- // These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
6423
- // intent — current production model ids. Per handoff §C-3 these MUST come
6424
- // from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
6425
- // for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
6426
- anthropic: "claude-opus-4-7",
6427
- openai: "gpt-5",
6428
- // gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
6429
- // callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
6430
- // Flash is also ~10x cheaper. 2026-06-02.
6431
- google: "gemini-2.5-flash",
6432
- deepseek: "deepseek-chat",
6433
- meta: META_CONSENSUS_MODEL
6434
- };
8606
+ var DEFAULT_MODELS = getVoMcpConsensusPanel();
6435
8607
  function probeProviders(env = process.env) {
6436
8608
  const out = [];
6437
8609
  if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
6438
8610
  if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
6439
8611
  if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
6440
- if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
6441
- if ((env[META_MODEL_API_KEY_ENV] ?? "").trim().length > 0 || (env[META_MODEL_API_KEY_ALIAS] ?? "").trim().length > 0) out.push("meta");
6442
8612
  return out;
6443
8613
  }
6444
8614
  async function loadFactoryAndCallers(injectedEngine, injectedShared) {
@@ -6483,25 +8653,20 @@ async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
6483
8653
  const callerByProvider = {
6484
8654
  anthropic: loaded.shared.callAnthropicWithMetrics,
6485
8655
  openai: loaded.shared.callOpenAIWithMetrics,
6486
- google: loaded.shared.callGeminiWithMetrics,
6487
- deepseek: loaded.shared.callDeepSeekWithMetrics,
6488
- meta: options.metaCaller ?? callMetaWithMetrics
8656
+ google: loaded.shared.callGeminiWithMetrics
6489
8657
  };
6490
8658
  const modelByProvider = {
6491
8659
  anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
6492
8660
  openai: options.models?.openai ?? DEFAULT_MODELS.openai,
6493
- google: options.models?.google ?? DEFAULT_MODELS.google,
6494
- deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek,
6495
- meta: options.models?.meta ?? DEFAULT_MODELS.meta
8661
+ google: options.models?.google ?? DEFAULT_MODELS.google
6496
8662
  };
6497
- const adapterEnv = !(env[META_MODEL_API_KEY_ENV] ?? "").trim() && (env[META_MODEL_API_KEY_ALIAS] ?? "").trim() ? { ...env, [META_MODEL_API_KEY_ENV]: env[META_MODEL_API_KEY_ALIAS] } : env;
6498
8663
  const panel = [];
6499
8664
  for (const p of providers) {
6500
8665
  try {
6501
8666
  const adapter = loaded.engine.createAdapter(p, {
6502
8667
  model: modelByProvider[p],
6503
8668
  caller: callerByProvider[p],
6504
- envSource: adapterEnv
8669
+ envSource: env
6505
8670
  });
6506
8671
  panel.push(adapter);
6507
8672
  } catch {
@@ -6535,10 +8700,13 @@ function tryCreateEngineConsensusClientFromEnv(options = {}) {
6535
8700
  }
6536
8701
 
6537
8702
  // src/consensus/moat-client.ts
6538
- import { randomUUID as randomUUID4 } from "node:crypto";
8703
+ import { randomUUID as randomUUID5 } from "node:crypto";
6539
8704
 
6540
8705
  // src/consensus/client.ts
6541
8706
  var CANCELLED_REASON = "cancelled";
8707
+ function hasLocalConsensusQuorum(result) {
8708
+ return result.execution_source !== "cloud" && result.quorum_failed !== true && result.per_model_verdicts.filter((verdict) => verdict.verdict !== "error").length >= 2;
8709
+ }
6542
8710
 
6543
8711
  // src/consensus/moat-client.ts
6544
8712
  var DEFAULT_VO_MOAT_ENDPOINT = "https://vo-moat-plane-620873201932.us-central1.run.app";
@@ -6554,7 +8722,9 @@ var MOAT_CLIENT_REASONS = {
6554
8722
  /** Network failure / local timeout (not the caller's cancellation). */
6555
8723
  NETWORK: "consensus-cloud-network-error",
6556
8724
  /** Excerpt exceeds the server's 50KB cap — refused locally with a clear reason. */
6557
- EXCERPT_TOO_LARGE: "consensus-cloud-excerpt-too-large"
8725
+ EXCERPT_TOO_LARGE: "consensus-cloud-excerpt-too-large",
8726
+ /** Cloud v1 cannot retrieve or grade caller-supplied authoritative sources. */
8727
+ SOURCE_GROUNDING_UNSUPPORTED: "consensus-cloud-source-grounding-unsupported"
6558
8728
  };
6559
8729
  function isVerdict(v) {
6560
8730
  if (!v || typeof v !== "object") return false;
@@ -6578,6 +8748,9 @@ function createMoatConsensusClient(opts) {
6578
8748
  return {
6579
8749
  async run(request) {
6580
8750
  if (request.signal?.aborted) return { ok: false, reason: CANCELLED_REASON };
8751
+ if (request.source_urls !== void 0 && request.source_urls.length > 0) {
8752
+ return { ok: false, reason: MOAT_CLIENT_REASONS.SOURCE_GROUNDING_UNSUPPORTED };
8753
+ }
6581
8754
  if (Buffer.byteLength(request.prompt, "utf8") > MOAT_EXCERPT_MAX_BYTES) {
6582
8755
  return { ok: false, reason: MOAT_CLIENT_REASONS.EXCERPT_TOO_LARGE };
6583
8756
  }
@@ -6587,7 +8760,7 @@ function createMoatConsensusClient(opts) {
6587
8760
  request.signal?.addEventListener("abort", onAbort, { once: true });
6588
8761
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
6589
8762
  const body = JSON.stringify({
6590
- task_id: randomUUID4(),
8763
+ task_id: randomUUID5(),
6591
8764
  gate_type: request.gate_type,
6592
8765
  excerpt: request.prompt,
6593
8766
  ...request.system_prompt ? { question: request.system_prompt } : {},
@@ -6616,8 +8789,11 @@ function createMoatConsensusClient(opts) {
6616
8789
  }
6617
8790
  const agreeing = normalizeAgreeing(parsed.models_agreeing);
6618
8791
  const reasoning = agreeing !== void 0 ? `${parsed.reason} (${agreeing} models agreeing)` : parsed.reason;
8792
+ const receiptId = typeof parsed.decision_id === "string" && parsed.decision_id.trim().length > 0 ? parsed.decision_id.trim().slice(0, 120) : null;
6619
8793
  return {
6620
8794
  ok: true,
8795
+ execution_source: "cloud",
8796
+ ...receiptId ? { receipt_id: receiptId } : {},
6621
8797
  synthesized_verdict: {
6622
8798
  verdict: parsed.approved ? "pass" : "fail",
6623
8799
  confidence: normalizeConfidence(parsed.confidence),
@@ -6658,6 +8834,178 @@ function tryCreateMoatConsensusClientFromEnv(env = process.env, fetchFn) {
6658
8834
  });
6659
8835
  }
6660
8836
 
8837
+ // src/consensus/fallback-client.ts
8838
+ var INSUFFICIENT_LOCAL_VERDICTS_REASON = "local-panel-insufficient-valid-verdicts";
8839
+ function createConsensusFallbackClient(primary, fallback, options = {}) {
8840
+ return {
8841
+ async run(request) {
8842
+ const primaryResult = await primary.run(request);
8843
+ if (request.signal?.aborted || !primaryResult.ok && primaryResult.reason === CANCELLED_REASON) {
8844
+ return primaryResult;
8845
+ }
8846
+ if (primaryResult.ok) {
8847
+ if (primaryResult.execution_source === "cloud" || hasLocalConsensusQuorum(primaryResult)) {
8848
+ return primaryResult;
8849
+ }
8850
+ options.onFallback?.(INSUFFICIENT_LOCAL_VERDICTS_REASON);
8851
+ return fallback.run(request);
8852
+ }
8853
+ options.onFallback?.(primaryResult.reason);
8854
+ return fallback.run(request);
8855
+ }
8856
+ };
8857
+ }
8858
+
8859
+ // src/consensus/shadow-client.ts
8860
+ import { appendFileSync as appendFileSync2, chmodSync as chmodSync4, mkdirSync as mkdirSync8, renameSync, statSync as statSync8 } from "node:fs";
8861
+ import { homedir as homedir8 } from "node:os";
8862
+ import { dirname as dirname7, join as join14 } from "node:path";
8863
+ var DEFAULT_SHADOW_TIMEOUT_MS = 2e4;
8864
+ var SHADOW_RECEIPT_MAX_BYTES = 20 * 1024 * 1024;
8865
+ function defaultShadowReceiptPath(env = process.env) {
8866
+ const p = (env["VO_MCP_MOAT_SHADOW_PATH"] ?? "").trim();
8867
+ return p || join14(homedir8(), ".claude", "vo-mcp-moat-shadow.jsonl");
8868
+ }
8869
+ function appendShadowReceipt(receipt, path4 = defaultShadowReceiptPath()) {
8870
+ try {
8871
+ mkdirSync8(dirname7(path4), { recursive: true, mode: 448 });
8872
+ try {
8873
+ if (statSync8(path4).size > SHADOW_RECEIPT_MAX_BYTES) renameSync(path4, `${path4}.1`);
8874
+ } catch {
8875
+ }
8876
+ appendFileSync2(path4, `${JSON.stringify(receipt)}
8877
+ `, "utf8");
8878
+ try {
8879
+ chmodSync4(path4, 384);
8880
+ } catch {
8881
+ }
8882
+ } catch {
8883
+ }
8884
+ }
8885
+ function summarize(result) {
8886
+ if (result.ok) {
8887
+ return {
8888
+ ok: true,
8889
+ verdict: result.synthesized_verdict.verdict,
8890
+ confidence: result.synthesized_verdict.confidence,
8891
+ ...result.receipt_id ? { receipt_id: result.receipt_id } : {}
8892
+ };
8893
+ }
8894
+ return { ok: false, reason: result.reason };
8895
+ }
8896
+ async function runMoat(shadow, request, timeoutMs, linkCallerSignal) {
8897
+ const callerSignal = linkCallerSignal ? request.signal : void 0;
8898
+ if (callerSignal?.aborted) return { ok: false, reason: CANCELLED_REASON };
8899
+ const controller = new AbortController();
8900
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8901
+ const onCallerAbort = () => controller.abort();
8902
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
8903
+ try {
8904
+ const { signal: _ignored, ...rest } = request;
8905
+ void _ignored;
8906
+ return await shadow.run({ ...rest, signal: controller.signal });
8907
+ } catch (err) {
8908
+ return { ok: false, reason: `moat-shadow-threw: ${err instanceof Error ? err.message : String(err)}`.slice(0, 200) };
8909
+ } finally {
8910
+ clearTimeout(timer);
8911
+ callerSignal?.removeEventListener("abort", onCallerAbort);
8912
+ }
8913
+ }
8914
+ function createConsensusShadowClient(primary, shadow, options = {}) {
8915
+ const authoritative = options.authoritative === true;
8916
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SHADOW_TIMEOUT_MS;
8917
+ const now = options.now ?? (() => Date.now());
8918
+ const random = options.random ?? Math.random;
8919
+ const samplePct = Math.min(100, Math.max(0, options.samplePct ?? 100));
8920
+ const onReceipt = options.onReceipt ?? ((r) => appendShadowReceipt(r));
8921
+ function receiptFor(request, local, moat, startedAt) {
8922
+ const l = summarize(local);
8923
+ const m = summarize(moat);
8924
+ return {
8925
+ ts: new Date(now()).toISOString(),
8926
+ gate_type: request.gate_type,
8927
+ mode: authoritative ? "authoritative" : "shadow",
8928
+ local: l,
8929
+ moat: m,
8930
+ agree: l.ok && m.ok ? l.verdict === m.verdict : null,
8931
+ duration_ms: Math.max(0, now() - startedAt)
8932
+ };
8933
+ }
8934
+ return {
8935
+ async run(request) {
8936
+ const startedAt = now();
8937
+ const primaryResult = await primary.run(request);
8938
+ if (request.signal?.aborted || !primaryResult.ok && primaryResult.reason === CANCELLED_REASON) {
8939
+ return primaryResult;
8940
+ }
8941
+ if (!primaryResult.ok || primaryResult.receipt_id || !hasLocalConsensusQuorum(primaryResult)) {
8942
+ return primaryResult;
8943
+ }
8944
+ if (authoritative) {
8945
+ const moatResult = await runMoat(shadow, request, timeoutMs, true);
8946
+ try {
8947
+ onReceipt(receiptFor(request, primaryResult, moatResult, startedAt));
8948
+ } catch {
8949
+ }
8950
+ return moatResult.ok ? moatResult : primaryResult;
8951
+ }
8952
+ if (samplePct < 100 && random() * 100 >= samplePct) return primaryResult;
8953
+ void runMoat(shadow, request, timeoutMs, false).then((moatResult) => {
8954
+ try {
8955
+ onReceipt(receiptFor(request, primaryResult, moatResult, startedAt));
8956
+ } catch {
8957
+ }
8958
+ });
8959
+ return primaryResult;
8960
+ }
8961
+ };
8962
+ }
8963
+
8964
+ // src/consensus/local-credential-env.ts
8965
+ import { createRequire as createRequire2 } from "node:module";
8966
+ var require2 = createRequire2(import.meta.url);
8967
+ var KEY_SERVICE = "algosuite-vo";
8968
+ var MOAT_ENTITLEMENT_KEYCHAIN_ACCOUNT = "moat-api-key";
8969
+ var KEYCHAIN_TARGETS = [
8970
+ { account: "anthropic-api-key", envVar: "ANTHROPIC_API_KEY" },
8971
+ { account: "openai-api-key", envVar: "OPENAI_API_KEY" },
8972
+ { account: "meta-api-key", envVar: "MODEL_API_KEY" },
8973
+ // The cloud-consensus entitlement (ADR-002 moat). Read here so an npm-installed
8974
+ // runner — where the local engine package never exists — still has a working
8975
+ // consensus path without an operator plumbing a secret into the daemon's env.
8976
+ { account: MOAT_ENTITLEMENT_KEYCHAIN_ACCOUNT, envVar: "VO_ENTITLEMENT_TOKEN" }
8977
+ ];
8978
+ function loadEntryCtor() {
8979
+ try {
8980
+ return require2("@napi-rs/keyring").Entry ?? null;
8981
+ } catch {
8982
+ return null;
8983
+ }
8984
+ }
8985
+ function readKey(EntryCtor, account) {
8986
+ try {
8987
+ return new EntryCtor(KEY_SERVICE, account).getPassword()?.trim() || null;
8988
+ } catch {
8989
+ return null;
8990
+ }
8991
+ }
8992
+ var SKIP_KEYCHAIN_ENV = "VO_MCP_SKIP_KEYCHAIN";
8993
+ function withLocalConsensusCredentials(baseEnv = process.env, options = {}) {
8994
+ const env = { ...baseEnv };
8995
+ if (!env.OPENAI_API_KEY?.trim() && env.CODEX_API_KEY?.trim()) {
8996
+ env.OPENAI_API_KEY = env.CODEX_API_KEY;
8997
+ }
8998
+ if (env[SKIP_KEYCHAIN_ENV]?.trim() === "1") return env;
8999
+ const EntryCtor = options.EntryCtor === void 0 ? loadEntryCtor() : options.EntryCtor;
9000
+ if (!EntryCtor) return env;
9001
+ for (const target of KEYCHAIN_TARGETS) {
9002
+ if (env[target.envVar]?.trim()) continue;
9003
+ const key = readKey(EntryCtor, target.account);
9004
+ if (key) env[target.envVar] = key;
9005
+ }
9006
+ return env;
9007
+ }
9008
+
6661
9009
  // src/cloud/login.ts
6662
9010
  init_credential_store();
6663
9011
  import { createServer as createServer2 } from "node:http";
@@ -6683,16 +9031,16 @@ function processCapture(rawBody, expectedState, store) {
6683
9031
  return { ok: false, httpStatus: 400, error: "login response missing refresh_token / api_key" };
6684
9032
  }
6685
9033
  const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : void 0;
6686
- const path3 = store({ refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} });
9034
+ const path4 = store({ refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} });
6687
9035
  return {
6688
9036
  ok: true,
6689
9037
  httpStatus: 200,
6690
- result: { ...email ? { email } : {}, credentialPath: path3 },
9038
+ result: { ...email ? { email } : {}, credentialPath: path4 },
6691
9039
  captured: { refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} }
6692
9040
  };
6693
9041
  }
6694
9042
  function captureHtml() {
6695
- return `<!doctype html><html><head><meta charset="utf-8"><title>VO login</title></head>
9043
+ return `<!doctype html><html><head><meta charset="utf-8"><title>AlgoHQ login</title></head>
6696
9044
  <body style="font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center">
6697
9045
  <h2 id="m">Completing sign-in\u2026</h2>
6698
9046
  <script>
@@ -6708,7 +9056,7 @@ function captureHtml() {
6708
9056
  function defaultOpenBrowser(url) {
6709
9057
  const platform = process.platform;
6710
9058
  if (platform === "win32") {
6711
- spawn2("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
9059
+ spawn2("rundll32", ["url.dll,FileProtocolHandler", url], { detached: true, stdio: "ignore" }).unref();
6712
9060
  } else if (platform === "darwin") {
6713
9061
  spawn2("open", [url], { detached: true, stdio: "ignore" }).unref();
6714
9062
  } else {
@@ -6723,7 +9071,7 @@ async function runLogin(opts = {}) {
6723
9071
  const nowIso = opts.nowIso ?? (() => (/* @__PURE__ */ new Date()).toISOString());
6724
9072
  const openBrowser = opts.openBrowser ?? defaultOpenBrowser;
6725
9073
  const state = randomBytes(32).toString("base64url");
6726
- return new Promise((resolve2, reject) => {
9074
+ return new Promise((resolve3, reject) => {
6727
9075
  let settled = false;
6728
9076
  const finish = (err, result) => {
6729
9077
  if (settled) return;
@@ -6731,7 +9079,7 @@ async function runLogin(opts = {}) {
6731
9079
  clearTimeout(timer);
6732
9080
  server.close();
6733
9081
  if (err) reject(err);
6734
- else resolve2(result);
9082
+ else resolve3(result);
6735
9083
  };
6736
9084
  const server = createServer2((req, res) => {
6737
9085
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -6775,11 +9123,11 @@ async function runLogin(opts = {}) {
6775
9123
  }
6776
9124
  } catch {
6777
9125
  }
6778
- const path3 = writeNow(cred);
6779
- result = { ...capt.email ? { email: capt.email } : {}, credentialPath: path3 };
9126
+ const path4 = writeNow(cred);
9127
+ result = { ...capt.email ? { email: capt.email } : {}, credentialPath: path4 };
6780
9128
  }
6781
9129
  res.writeHead(outcome.httpStatus, { "content-type": "text/html; charset=utf-8" });
6782
- res.end(outcome.ok ? "<h2>VO login complete \u2014 you can close this tab.</h2>" : `<h2>Login failed: ${outcome.error}</h2>`);
9130
+ res.end(outcome.ok ? "<h2>AlgoHQ login complete \u2014 you can close this tab.</h2>" : `<h2>Login failed: ${outcome.error}</h2>`);
6783
9131
  finish(outcome.ok ? null : new Error(outcome.error ?? "login failed"), result);
6784
9132
  })();
6785
9133
  });
@@ -6846,7 +9194,7 @@ init_common();
6846
9194
  function defaultCacheDbPath() {
6847
9195
  const env = process.env["VO_MCP_DB_PATH"];
6848
9196
  if (env && env.length > 0) return env;
6849
- return join8(homedir6(), ".claude", "vo-mcp-cache.db");
9197
+ return join15(homedir9(), ".claude", "vo-mcp-cache.db");
6850
9198
  }
6851
9199
  async function probeEngineVersion() {
6852
9200
  try {
@@ -6921,11 +9269,35 @@ async function main() {
6921
9269
  const ratchets = createStubRatchetClient();
6922
9270
  const testModule = process.env["VO_MCP_TEST_ENGINE_MODULE"];
6923
9271
  const testClient = testModule !== void 0 && testModule.length > 0 ? await loadTestEngineClient(testModule) : null;
6924
- const cloudConsensus = testClient ? null : tryCreateMoatConsensusClientFromEnv();
6925
- if (cloudConsensus) {
6926
- console.error("[vo-mcp] cloud consensus active \u2014 verdicts via vo-moat-plane (no local model keys)");
9272
+ const localEnv = withLocalConsensusCredentials();
9273
+ const localProviders = probeProviders(localEnv);
9274
+ for (const key of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY", "DEEPSEEK_API_KEY", "MODEL_API_KEY"]) {
9275
+ if (!process.env[key] && localEnv[key]) process.env[key] = localEnv[key];
9276
+ }
9277
+ const localConsensus = tryCreateEngineConsensusClientFromEnv({ envSource: localEnv });
9278
+ const cloudConsensus = testClient ? null : tryCreateMoatConsensusClientFromEnv(localEnv);
9279
+ let consensus = testClient ?? localConsensus;
9280
+ if (!testClient && cloudConsensus && localProviders.length >= 2) {
9281
+ console.error(`[vo-mcp] local-first consensus active (${localProviders.join(", ")}); cloud moat is fallback-only`);
9282
+ consensus = createConsensusFallbackClient(localConsensus, cloudConsensus, {
9283
+ onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
9284
+ });
9285
+ const moatAuthoritative = process.env["VO_CONSENSUS_MOAT_AUTHORITATIVE"] === "1";
9286
+ if (moatAuthoritative || process.env["VO_CONSENSUS_MOAT_SHADOW"] === "1") {
9287
+ const pctRaw = Number(process.env["VO_CONSENSUS_MOAT_SHADOW_PCT"] ?? "100");
9288
+ const samplePct = Number.isFinite(pctRaw) ? Math.min(100, Math.max(0, pctRaw)) : 100;
9289
+ const shadowedLocal = createConsensusShadowClient(localConsensus, cloudConsensus, { authoritative: moatAuthoritative, samplePct });
9290
+ consensus = createConsensusFallbackClient(shadowedLocal, cloudConsensus, {
9291
+ onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
9292
+ });
9293
+ console.error(`[vo-mcp] moat ${moatAuthoritative ? "AUTHORITATIVE" : `SHADOW (${samplePct}%)`} active \u2014 local verdicts also reach the moat verify path (receipts: ${defaultShadowReceiptPath()})`);
9294
+ }
9295
+ } else if (!testClient && cloudConsensus) {
9296
+ console.error("[vo-mcp] fewer than 2 linked local providers; cloud moat consensus active");
9297
+ consensus = cloudConsensus;
9298
+ } else if (!testClient) {
9299
+ console.error("[vo-mcp] no VO_ENTITLEMENT_TOKEN (env or keychain via `vo-mcp set-key --provider moat`); consensus tools depend on a locally installed engine and will report engine-unavailable on an npm-installed runner");
6927
9300
  }
6928
- const consensus = testClient ?? cloudConsensus ?? tryCreateEngineConsensusClientFromEnv();
6929
9301
  let adminCallables = null;
6930
9302
  try {
6931
9303
  adminCallables = buildAdminCallableClientFromEnv();
@@ -6961,7 +9333,7 @@ async function main() {
6961
9333
  }
6962
9334
  if (process.argv[2] === "login") {
6963
9335
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.trim();
6964
- const credentialLabel = `vo-mcp-cli@${hostname()}`.slice(0, 200);
9336
+ const credentialLabel = `vo-mcp-cli@${hostname2()}`.slice(0, 200);
6965
9337
  runLogin(
6966
9338
  controlPlaneUrl ? {
6967
9339
  exchange: (refreshToken, apiKey) => exchangeForVoCredential({ refreshToken, apiKey, controlPlaneUrl, label: credentialLabel })
@@ -6970,35 +9342,53 @@ if (process.argv[2] === "login") {
6970
9342
  console.error(`[vo-mcp] login successful${r.email ? ` as ${r.email}` : ""}. Credential stored at ${r.credentialPath}.`);
6971
9343
  console.error("[vo-mcp] You can now remove VO_CONTROL_PLANE_ADMIN_TOKEN (the god-token) from your MCP config.");
6972
9344
  console.error("[vo-mcp] NOTE: per-user auth requires VO_OPERATOR_ALLOWED_EMAILS (with your email) on the deployed control-plane.");
6973
- process.exit(0);
6974
9345
  }).catch((err) => {
6975
9346
  console.error("[vo-mcp] login failed:", err instanceof Error ? err.message : String(err));
6976
- process.exit(1);
9347
+ process.exitCode = 1;
6977
9348
  });
6978
9349
  } else if (process.argv[2] === "sync") {
6979
9350
  const action = process.argv[3];
6980
9351
  if (action !== "push" && action !== "pull") {
6981
- console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>]");
9352
+ console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>] [--lock-wait-ms <n>]");
6982
9353
  process.exit(2);
6983
9354
  }
6984
9355
  const cwdFlag = process.argv.indexOf("--cwd");
6985
9356
  const cwd = cwdFlag >= 0 && typeof process.argv[cwdFlag + 1] === "string" ? process.argv[cwdFlag + 1] : process.cwd();
6986
- const sessionId = randomUUID5();
9357
+ const waitFlag = process.argv.indexOf("--lock-wait-ms");
9358
+ const parsedWait = waitFlag >= 0 ? Number(process.argv[waitFlag + 1]) : Number.NaN;
9359
+ const lockOptions = Number.isFinite(parsedWait) && parsedWait >= 0 ? { waitMs: parsedWait } : {};
9360
+ const sessionId = randomUUID6();
9361
+ const appendSyncLog = async (line) => {
9362
+ try {
9363
+ const { appendFileSync: appendFileSync3, mkdirSync: mkdirSync9 } = await import("node:fs");
9364
+ const { join: join16 } = await import("node:path");
9365
+ const { homedir: homedir10 } = await import("node:os");
9366
+ const dir = join16(homedir10(), ".claude");
9367
+ mkdirSync9(dir, { recursive: true });
9368
+ appendFileSync3(join16(dir, "vo-mcp-sync.log"), `${line}
9369
+ `, "utf8");
9370
+ } catch {
9371
+ }
9372
+ };
6987
9373
  Promise.resolve().then(() => (init_sync_config(), sync_config_exports)).then(async ({ runMemorySync: runMemorySync2, isNoopSyncReason: isNoopSyncReason2 }) => {
6988
- const r = await runMemorySync2(action, cwd, sessionId);
9374
+ const r = await runMemorySync2(action, cwd, sessionId, void 0, lockOptions);
9375
+ const stamp = `${sessionId} ${action}`;
6989
9376
  if (r.synced) {
6990
9377
  console.error(`[vo-mcp] sync ${action} ok: ${JSON.stringify(r)}`);
6991
- process.exit(0);
6992
- }
6993
- if (isNoopSyncReason2(r.reason)) {
9378
+ await appendSyncLog(`ok ${stamp} ${JSON.stringify(r)}`);
9379
+ } else if (isNoopSyncReason2(r.reason)) {
6994
9380
  console.error(`[vo-mcp] sync ${action} skipped: ${r.reason}`);
6995
- process.exit(0);
9381
+ await appendSyncLog(`skip ${stamp} ${r.reason ?? ""}`);
9382
+ } else {
9383
+ console.error(`[vo-mcp] sync ${action} failed: ${r.reason}`);
9384
+ await appendSyncLog(`FAIL ${stamp} ${r.reason ?? ""}`);
9385
+ process.exitCode = 1;
6996
9386
  }
6997
- console.error(`[vo-mcp] sync ${action} failed: ${r.reason}`);
6998
- process.exit(1);
6999
- }).catch((err) => {
7000
- console.error("[vo-mcp] sync fatal:", err instanceof Error ? err.message : String(err));
7001
- process.exit(1);
9387
+ }).catch(async (err) => {
9388
+ const message = err instanceof Error ? err.message : String(err);
9389
+ console.error("[vo-mcp] sync fatal:", message);
9390
+ await appendSyncLog(`FATAL ${sessionId} ${action} ${message}`);
9391
+ process.exitCode = 1;
7002
9392
  });
7003
9393
  } else {
7004
9394
  main().catch((err) => {