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

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/index.js CHANGED
@@ -180,6 +180,7 @@ __export(credential_store_exports, {
180
180
  KEYCHAIN_LOCATION: () => KEYCHAIN_LOCATION,
181
181
  credentialPath: () => credentialPath,
182
182
  readStoredCredential: () => readStoredCredential,
183
+ readStoredCredentialKeychainOnly: () => readStoredCredentialKeychainOnly,
183
184
  writeStoredCredential: () => writeStoredCredential
184
185
  });
185
186
  import { homedir as homedir3 } from "node:os";
@@ -238,6 +239,11 @@ function readStoredCredential(env = process.env, keychain = realKeychain) {
238
239
  }
239
240
  return readFromFile(env);
240
241
  }
242
+ function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
243
+ if (!keychainEnabled(env, keychain)) return null;
244
+ const raw = keychain.get();
245
+ return raw ? deserialize(raw) : null;
246
+ }
241
247
  function deleteFile(env) {
242
248
  try {
243
249
  rmSync(credentialPath(env), { force: true });
@@ -287,8 +293,332 @@ var init_credential_store = __esm({
287
293
  }
288
294
  });
289
295
 
296
+ // src/tools/memory/safe-memory-file.ts
297
+ import { resolve, sep } from "node:path";
298
+ function isSafeMemoryFileName(fileName) {
299
+ return fileName.length <= 200 && fileName.trim() === fileName && !fileName.includes("/") && !fileName.includes("\\") && !fileName.includes(":") && SAFE_MEMORY_FILE_RE.test(fileName);
300
+ }
301
+ function resolveMemoryFilePath(memoryDir, fileName) {
302
+ if (!isSafeMemoryFileName(fileName)) {
303
+ throw new Error(`unsafe memory file_name: ${fileName.slice(0, 80)}`);
304
+ }
305
+ const root = resolve(memoryDir);
306
+ const filePath = resolve(root, fileName);
307
+ const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
308
+ if (filePath !== root && !filePath.startsWith(rootPrefix)) {
309
+ throw new Error(`memory file path escapes memory directory: ${fileName.slice(0, 80)}`);
310
+ }
311
+ return filePath;
312
+ }
313
+ var SAFE_MEMORY_FILE_RE;
314
+ var init_safe_memory_file = __esm({
315
+ "src/tools/memory/safe-memory-file.ts"() {
316
+ "use strict";
317
+ SAFE_MEMORY_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._ -]*\.md$/i;
318
+ }
319
+ });
320
+
321
+ // src/tools/memory/bounded-sync.ts
322
+ function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
323
+ const startedAt = now();
324
+ return {
325
+ check() {
326
+ const elapsed = now() - startedAt;
327
+ if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
328
+ },
329
+ remainingMs() {
330
+ return Math.max(0, budgetMs - (now() - startedAt));
331
+ }
332
+ };
333
+ }
334
+ async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
335
+ let timer;
336
+ try {
337
+ return await Promise.race([
338
+ run(),
339
+ new Promise((_resolve, reject) => {
340
+ timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
341
+ timer.unref?.();
342
+ })
343
+ ]);
344
+ } finally {
345
+ if (timer) clearTimeout(timer);
346
+ }
347
+ }
348
+ async function mapWithConcurrency(items, limit, fn) {
349
+ const results = new Array(items.length);
350
+ const width = Math.max(1, Math.min(limit, items.length));
351
+ let next = 0;
352
+ async function worker() {
353
+ for (; ; ) {
354
+ const index = next++;
355
+ if (index >= items.length) return;
356
+ try {
357
+ results[index] = { ok: true, value: await fn(items[index], index) };
358
+ } catch (error) {
359
+ results[index] = { ok: false, error };
360
+ }
361
+ }
362
+ }
363
+ await Promise.all(Array.from({ length: width }, () => worker()));
364
+ return results;
365
+ }
366
+ var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
367
+ var init_bounded_sync = __esm({
368
+ "src/tools/memory/bounded-sync.ts"() {
369
+ "use strict";
370
+ REQUEST_TIMEOUT_MS = 15e3;
371
+ SYNC_DEADLINE_MS = 12e4;
372
+ PUSH_CONCURRENCY = 6;
373
+ SyncDeadlineExceededError = class extends Error {
374
+ constructor(elapsedMs, budgetMs) {
375
+ super(
376
+ `memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
377
+ );
378
+ this.name = "SyncDeadlineExceededError";
379
+ }
380
+ };
381
+ RequestTimeoutError = class extends Error {
382
+ constructor(url, budgetMs) {
383
+ super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
384
+ this.name = "RequestTimeoutError";
385
+ }
386
+ };
387
+ }
388
+ });
389
+
390
+ // src/tools/memory/memory-push-cache.ts
391
+ import { createHash as createHash3 } from "node:crypto";
392
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
393
+ import { join as join9 } from "node:path";
394
+ function sha256(content) {
395
+ return createHash3("sha256").update(content, "utf8").digest("hex");
396
+ }
397
+ function statePath(memoryDir) {
398
+ return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
399
+ }
400
+ function readPushCache(memoryDir, controlPlaneUrl) {
401
+ const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
402
+ let raw;
403
+ try {
404
+ raw = readFileSync10(statePath(memoryDir), "utf8");
405
+ } catch {
406
+ return empty;
407
+ }
408
+ let parsed;
409
+ try {
410
+ parsed = JSON.parse(raw);
411
+ } catch {
412
+ return empty;
413
+ }
414
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
415
+ const obj = parsed;
416
+ if (obj["version"] !== STATE_VERSION) return empty;
417
+ if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
418
+ const files = obj["entries"];
419
+ if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
420
+ const entries = /* @__PURE__ */ new Map();
421
+ for (const [name, value] of Object.entries(files)) {
422
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
423
+ const row = value;
424
+ const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
425
+ const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
426
+ if (memoryHash === void 0 && knowledgeHash === void 0) continue;
427
+ entries.set(name, {
428
+ ...memoryHash !== void 0 ? { memoryHash } : {},
429
+ ...knowledgeHash !== void 0 ? { knowledgeHash } : {}
430
+ });
431
+ }
432
+ const sweptAt = obj["knowledgeSweptAtMs"];
433
+ return {
434
+ controlPlaneUrl,
435
+ entries,
436
+ // An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
437
+ // sweep — the fail-closed direction (more upserts, never fewer).
438
+ knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
439
+ };
440
+ }
441
+ function writePushCache(memoryDir, cache) {
442
+ const entries = {};
443
+ for (const [name, value] of cache.entries) entries[name] = value;
444
+ try {
445
+ writeFileSync5(
446
+ statePath(memoryDir),
447
+ `${JSON.stringify(
448
+ {
449
+ version: STATE_VERSION,
450
+ controlPlaneUrl: cache.controlPlaneUrl,
451
+ knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
452
+ entries
453
+ },
454
+ null,
455
+ 2
456
+ )}
457
+ `,
458
+ "utf8"
459
+ );
460
+ } catch {
461
+ }
462
+ }
463
+ function recordMemoryPush(cache, fileName, payloadHash) {
464
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
465
+ }
466
+ function recordKnowledgePush(cache, fileName, contentHash) {
467
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
468
+ }
469
+ function pruneMissing(cache, presentFileNames) {
470
+ const present = new Set(presentFileNames);
471
+ for (const name of [...cache.entries.keys()]) {
472
+ if (!present.has(name)) cache.entries.delete(name);
473
+ }
474
+ }
475
+ function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
476
+ if (!serverHasEntry) return true;
477
+ return cache.entries.get(fileName)?.memoryHash !== payloadHash;
478
+ }
479
+ function knowledgeSweepDue(cache, nowMs = Date.now()) {
480
+ const swept = cache.knowledgeSweptAtMs;
481
+ if (swept === null || !Number.isFinite(swept)) return true;
482
+ const age = nowMs - swept;
483
+ return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
484
+ }
485
+ function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
486
+ if (sweepDue) return true;
487
+ return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
488
+ }
489
+ var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
490
+ var init_memory_push_cache = __esm({
491
+ "src/tools/memory/memory-push-cache.ts"() {
492
+ "use strict";
493
+ MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
494
+ STATE_VERSION = 1;
495
+ KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
496
+ }
497
+ });
498
+
499
+ // src/tools/memory/memory-knowledge-bridge.ts
500
+ var memory_knowledge_bridge_exports = {};
501
+ __export(memory_knowledge_bridge_exports, {
502
+ extractMemoryTitle: () => extractMemoryTitle,
503
+ upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
504
+ });
505
+ import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
506
+ function extractMemoryTitle(fileName, content) {
507
+ const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
508
+ if (frontmatter) {
509
+ const description24 = frontmatter[1].match(/^description:\s*(.+)$/m);
510
+ if (description24 && description24[1].trim()) return description24[1].trim().slice(0, 200);
511
+ }
512
+ const heading = content.match(/^#\s+(.+)$/m);
513
+ if (heading && heading[1].trim()) return heading[1].trim().slice(0, 200);
514
+ return fileName;
515
+ }
516
+ async function upsertMemoryFilesAsKnowledge(options) {
517
+ const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
518
+ let files;
519
+ try {
520
+ if (!existsSync8(memoryDir)) {
521
+ return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
522
+ }
523
+ files = readdirSync5(memoryDir).filter(
524
+ (f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
525
+ );
526
+ } catch (err) {
527
+ return {
528
+ attempted: 0,
529
+ upserted: 0,
530
+ failed: 1,
531
+ skipped: 0,
532
+ failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
533
+ };
534
+ }
535
+ const sweepDue = cache ? knowledgeSweepDue(cache) : true;
536
+ const candidates = [];
537
+ const failures = [];
538
+ let skipped = 0;
539
+ for (const fileName of files) {
540
+ try {
541
+ const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
542
+ if (content.length > CONTENT_HARD_LIMIT) {
543
+ failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
544
+ continue;
545
+ }
546
+ const hash = sha256(content);
547
+ if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
548
+ skipped += 1;
549
+ continue;
550
+ }
551
+ candidates.push({ fileName, content, hash });
552
+ } catch (err) {
553
+ failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
554
+ }
555
+ }
556
+ const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
557
+ const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
558
+ deadline?.check();
559
+ const base = {
560
+ knowledge_class: "memory",
561
+ source_path: `memory/${candidate.fileName}`,
562
+ title: extractMemoryTitle(candidate.fileName, candidate.content),
563
+ content: candidate.content
564
+ };
565
+ const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
566
+ method: "POST",
567
+ headers: {
568
+ authorization: `Bearer ${token}`,
569
+ "content-type": "application/json"
570
+ },
571
+ body: JSON.stringify(body)
572
+ }));
573
+ let response = await post({
574
+ ...base,
575
+ provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
576
+ });
577
+ if (response.status === 400) {
578
+ response = await post(base);
579
+ }
580
+ if (response.status >= 200 && response.status < 300) return true;
581
+ const text = await response.text();
582
+ throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
583
+ });
584
+ let upserted = 0;
585
+ for (let i = 0; i < outcomes.length; i++) {
586
+ const outcome = outcomes[i];
587
+ const candidate = candidates[i];
588
+ if (outcome.ok) {
589
+ upserted += 1;
590
+ if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
591
+ } else {
592
+ const error = outcome.error;
593
+ failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
594
+ }
595
+ }
596
+ if (cache && sweepDue && failures.length === 0) {
597
+ cache.knowledgeSweptAtMs = Date.now();
598
+ }
599
+ return {
600
+ // Every memory file this run considered. `attempted === upserted + skipped
601
+ // + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
602
+ attempted: files.length,
603
+ upserted,
604
+ failed: failures.length,
605
+ skipped,
606
+ failures: failures.slice(0, 5)
607
+ };
608
+ }
609
+ var CONTENT_HARD_LIMIT;
610
+ var init_memory_knowledge_bridge = __esm({
611
+ "src/tools/memory/memory-knowledge-bridge.ts"() {
612
+ "use strict";
613
+ init_safe_memory_file();
614
+ init_bounded_sync();
615
+ init_memory_push_cache();
616
+ CONTENT_HARD_LIMIT = 5e5;
617
+ }
618
+ });
619
+
290
620
  // src/server.ts
291
- import { randomUUID as randomUUID2 } from "node:crypto";
621
+ import { randomUUID as randomUUID3 } from "node:crypto";
292
622
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
293
623
  import {
294
624
  CallToolRequestSchema,
@@ -477,24 +807,24 @@ function defaultOverridePath() {
477
807
  return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
478
808
  }
479
809
  function loadTenantOverride(opts = {}) {
480
- const path3 = opts.path ?? defaultOverridePath();
481
- if (!existsSync2(path3)) {
810
+ const path4 = opts.path ?? defaultOverridePath();
811
+ if (!existsSync2(path4)) {
482
812
  return { override: null, source_path: null };
483
813
  }
484
- const raw = readFileSync2(path3, "utf8");
814
+ const raw = readFileSync2(path4, "utf8");
485
815
  let parsed;
486
816
  try {
487
817
  parsed = JSON.parse(raw);
488
818
  } catch (err) {
489
819
  const m = err instanceof Error ? err.message : String(err);
490
- throw new Error(`vo-arch-defaults: invalid JSON in override ${path3}: ${m}`, { cause: err });
820
+ throw new Error(`vo-arch-defaults: invalid JSON in override ${path4}: ${m}`, { cause: err });
491
821
  }
492
822
  try {
493
823
  const override = parseOverride(parsed);
494
- return { override, source_path: path3 };
824
+ return { override, source_path: path4 };
495
825
  } catch (err) {
496
826
  const m = err instanceof Error ? err.message : String(err);
497
- throw new Error(`vo-arch-defaults: override schema validation failed for ${path3}: ${m}`, { cause: err });
827
+ throw new Error(`vo-arch-defaults: override schema validation failed for ${path4}: ${m}`, { cause: err });
498
828
  }
499
829
  }
500
830
 
@@ -624,9 +954,9 @@ function globToRegExp(glob) {
624
954
  }
625
955
  return new RegExp("^" + out + "$");
626
956
  }
627
- function matchesAnyGlob(path3, globs) {
957
+ function matchesAnyGlob(path4, globs) {
628
958
  for (const g of globs) {
629
- if (globToRegExp(g).test(path3)) return true;
959
+ if (globToRegExp(g).test(path4)) return true;
630
960
  }
631
961
  return false;
632
962
  }
@@ -1160,7 +1490,17 @@ function assertWithinByteCap(toolName, fieldName, value, maxBytes) {
1160
1490
  );
1161
1491
  }
1162
1492
  }
1493
+ var REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
1494
+ function subjectFromEnv(env = process.env) {
1495
+ const code_task_id = (env["VO_CODE_TASK_ID"] ?? "").trim().slice(0, 120);
1496
+ const repo = (env["VO_CODE_TASK_REPO"] ?? "").trim();
1497
+ const subject = {};
1498
+ if (code_task_id) subject.code_task_id = code_task_id;
1499
+ if (repo && REPO_RE.test(repo) && repo.length <= 200) subject.repo = repo;
1500
+ return subject.code_task_id || subject.repo ? subject : null;
1501
+ }
1163
1502
  function buildBaseEvent(args) {
1503
+ const subject = args.subject === void 0 ? subjectFromEnv() : args.subject;
1164
1504
  return {
1165
1505
  schema_version: 1,
1166
1506
  event_id: args.eventId ?? randomUUID(),
@@ -1186,7 +1526,12 @@ function buildBaseEvent(args) {
1186
1526
  downstream_outcome: null,
1187
1527
  vo_mcp_version: VO_MCP_VERSION,
1188
1528
  consensus_engine_version: null,
1189
- cache_hit: false
1529
+ cache_hit: false,
1530
+ // OMIT the key when there is no subject (rather than `subject: null`): the
1531
+ // ingest schema is `.strict()`, so an event with no subject stays valid on a
1532
+ // sink that has not learned the field yet — only subject-carrying events
1533
+ // depend on the sink being current (deploy ordering: sink before producer).
1534
+ ...subject ? { subject } : {}
1190
1535
  };
1191
1536
  }
1192
1537
  function jsonContent(value) {
@@ -1216,6 +1561,52 @@ function toEventPerModelVerdicts(src) {
1216
1561
  };
1217
1562
  });
1218
1563
  }
1564
+ function aggregateEventTokenUsage(src, engineUsage) {
1565
+ if (engineUsage !== void 0) {
1566
+ const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
1567
+ const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
1568
+ return {
1569
+ per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
1570
+ per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
1571
+ total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
1572
+ };
1573
+ }
1574
+ const tokensIn = {};
1575
+ const tokensOut = {};
1576
+ let anyTokensIn = false;
1577
+ let anyTokensOut = false;
1578
+ let costMicroUsd = 0;
1579
+ let anyCost = false;
1580
+ for (const v of src) {
1581
+ if (typeof v.input_tokens === "number") {
1582
+ tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
1583
+ anyTokensIn = true;
1584
+ }
1585
+ if (typeof v.output_tokens === "number") {
1586
+ tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
1587
+ anyTokensOut = true;
1588
+ }
1589
+ if (typeof v.cost_micro_usd === "number") {
1590
+ costMicroUsd += v.cost_micro_usd;
1591
+ anyCost = true;
1592
+ }
1593
+ }
1594
+ return {
1595
+ per_model_tokens_in: anyTokensIn ? tokensIn : null,
1596
+ per_model_tokens_out: anyTokensOut ? tokensOut : null,
1597
+ total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
1598
+ };
1599
+ }
1600
+ function trajectoryFromEngine(result, known = {}) {
1601
+ 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;
1602
+ const called = result.fan_out_diagnostics?.models_called;
1603
+ const turns = typeof called === "number" && Number.isInteger(called) && called >= 0 ? called : rounds !== null && rounds > 0 ? result.per_model_verdicts.length * rounds : null;
1604
+ const sourceGrounded = result.citation_grade !== void 0 || result.low_confidence_sources !== void 0;
1605
+ const tool_calls = typeof known.tool_calls === "number" && Number.isInteger(known.tool_calls) && known.tool_calls >= 0 ? known.tool_calls : sourceGrounded ? null : 0;
1606
+ if (rounds === null && turns === null && tool_calls === null) return {};
1607
+ const trajectory = { rounds, turns, tool_calls };
1608
+ return { trajectory };
1609
+ }
1219
1610
  function toEventSynthesizedVerdict(src) {
1220
1611
  return {
1221
1612
  verdict: src.verdict,
@@ -1587,7 +1978,9 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
1587
1978
  synthesized_verdict: synthForEvent,
1588
1979
  consensus_confidence: engineResult.synthesized_verdict.confidence,
1589
1980
  duration_ms: engineResult.duration_ms,
1590
- consensus_engine_version: engineResult.engine_version
1981
+ consensus_engine_version: engineResult.engine_version,
1982
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
1983
+ ...trajectoryFromEngine(engineResult)
1591
1984
  };
1592
1985
  const payload = {
1593
1986
  verdict: engineResult.synthesized_verdict.verdict,
@@ -1769,7 +2162,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
1769
2162
  synthesized_verdict: synthForEvent,
1770
2163
  consensus_confidence: engineResult.synthesized_verdict.confidence,
1771
2164
  duration_ms: engineResult.duration_ms,
1772
- consensus_engine_version: engineResult.engine_version
2165
+ consensus_engine_version: engineResult.engine_version,
2166
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
2167
+ ...trajectoryFromEngine(engineResult)
1773
2168
  };
1774
2169
  const payload = {
1775
2170
  verdict: engineResult.synthesized_verdict.verdict,
@@ -1778,6 +2173,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
1778
2173
  synthesized_verdict: synthForEvent,
1779
2174
  engine_version: engineResult.engine_version,
1780
2175
  degraded: engineResult.degraded,
2176
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
2177
+ // The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
2178
+ ...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
1781
2179
  gate_type: gateType,
1782
2180
  ...kbResult.error !== null ? { kb_unavailable: true } : {},
1783
2181
  ...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
@@ -2031,7 +2429,9 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2031
2429
  duration_ms: engineResult.duration_ms,
2032
2430
  consensus_engine_version: engineResult.engine_version,
2033
2431
  per_model_verdicts: perModelForEvent,
2034
- synthesized_verdict: synthForEvent
2432
+ synthesized_verdict: synthForEvent,
2433
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
2434
+ ...trajectoryFromEngine(engineResult)
2035
2435
  };
2036
2436
  const payload = {
2037
2437
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2040,6 +2440,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2040
2440
  synthesized_verdict: synthForEvent,
2041
2441
  engine_version: engineResult.engine_version,
2042
2442
  degraded: engineResult.degraded,
2443
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
2444
+ // The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
2445
+ // NOTE: a content-hash cache hit replays the ORIGINAL call's receipt_id (same claim, same verdict, no new spend) —
2446
+ // a receipt asserts the stage ran for this claim, not one-receipt-per-call.
2447
+ ...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
2043
2448
  gate_type: gateType,
2044
2449
  // ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
2045
2450
  // Feature 2 (calibrated-confidence) — ON by default; the engine attaches
@@ -2058,7 +2463,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2058
2463
  ...engineResult.low_confidence_sources !== void 0 ? { low_confidence_sources: engineResult.low_confidence_sources } : {},
2059
2464
  // Escalation (from citation grade or human-tiebreak synthesizer).
2060
2465
  ...engineResult.escalation_required !== void 0 ? { escalation_required: engineResult.escalation_required } : {},
2061
- ...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {}
2466
+ ...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {},
2467
+ // Critique-uptake (2026-07-20 red-team fix) — the engine computes this
2468
+ // on every call; this spread closes the gap where the visibility report
2469
+ // was itself silently dropped at the payload boundary.
2470
+ ...engineResult.critique_uptake !== void 0 ? { critique_uptake: engineResult.critique_uptake } : {}
2062
2471
  };
2063
2472
  const envelope = {
2064
2473
  tool: TOOL_NAME4,
@@ -2234,7 +2643,9 @@ async function handleArchitectureReview(deps, rawInput, signal) {
2234
2643
  synthesized_verdict: synthForEvent,
2235
2644
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2236
2645
  duration_ms: engineResult.duration_ms,
2237
- consensus_engine_version: engineResult.engine_version
2646
+ consensus_engine_version: engineResult.engine_version,
2647
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
2648
+ ...trajectoryFromEngine(engineResult)
2238
2649
  };
2239
2650
  const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
2240
2651
  const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
@@ -3719,7 +4130,9 @@ Produce the JSON dispatch plan now.`;
3719
4130
  duration_ms: engineResult.duration_ms,
3720
4131
  consensus_engine_version: engineResult.engine_version,
3721
4132
  per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
3722
- synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
4133
+ synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
4134
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
4135
+ ...trajectoryFromEngine(engineResult)
3723
4136
  };
3724
4137
  deps.events.append(enrichedEvent);
3725
4138
  return jsonContent(envelope);
@@ -3729,14 +4142,16 @@ Produce the JSON dispatch plan now.`;
3729
4142
  init_auth_token_source();
3730
4143
  init_credential_store();
3731
4144
  var AdminCallableError = class extends Error {
3732
- constructor(status, path3, message) {
4145
+ constructor(status, path4, message, body) {
3733
4146
  super(message);
3734
4147
  this.status = status;
3735
- this.path = path3;
4148
+ this.path = path4;
4149
+ this.body = body;
3736
4150
  this.name = "AdminCallableError";
3737
4151
  }
3738
4152
  status;
3739
4153
  path;
4154
+ body;
3740
4155
  };
3741
4156
 
3742
4157
  // src/tools/cloud-call.ts
@@ -3771,11 +4186,12 @@ async function buildCloudOrStubResponse(args) {
3771
4186
  args.deps.events.append(event);
3772
4187
  return jsonContent(envelope);
3773
4188
  }
4189
+ const invokeOptions = args.rawEnvelope || args.invokeOptions ? { ...args.rawEnvelope ? { rawEnvelope: true } : {}, ...args.invokeOptions } : void 0;
3774
4190
  try {
3775
4191
  const result = await args.deps.adminCallables.invoke(
3776
4192
  args.adminPath,
3777
4193
  args.cloudBody ?? args.normalizedInput,
3778
- args.rawEnvelope ? { rawEnvelope: true } : void 0
4194
+ invokeOptions
3779
4195
  );
3780
4196
  const payload = {
3781
4197
  verdict: "pass",
@@ -3795,11 +4211,13 @@ async function buildCloudOrStubResponse(args) {
3795
4211
  } catch (err) {
3796
4212
  const status = err instanceof AdminCallableError ? err.status : void 0;
3797
4213
  const message = err instanceof Error ? err.message : String(err);
4214
+ const errorBody = err instanceof AdminCallableError && err.body !== void 0 ? err.body : void 0;
3798
4215
  const payload = {
3799
4216
  verdict: "fail",
3800
4217
  reason: status !== void 0 ? `vo-control-plane returned HTTP ${status}: ${message.slice(0, 300)}` : `cloud invocation failed: ${message.slice(0, 300)}`,
3801
4218
  callable: args.callableName,
3802
- normalized_input: args.normalizedInput
4219
+ normalized_input: args.normalizedInput,
4220
+ ...errorBody ? { response_data: errorBody } : {}
3803
4221
  };
3804
4222
  const envelope = {
3805
4223
  tool: args.toolName,
@@ -3813,7 +4231,7 @@ async function buildCloudOrStubResponse(args) {
3813
4231
  }
3814
4232
 
3815
4233
  // src/tools/heal/common-heal.ts
3816
- 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")';
4234
+ 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.";
3817
4235
  var HEAL_GATE_TYPE = "admin-action";
3818
4236
 
3819
4237
  // src/tools/heal/trigger-heal.ts
@@ -3834,7 +4252,7 @@ var inputSchema8 = {
3834
4252
  },
3835
4253
  additionalProperties: false
3836
4254
  };
3837
- 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.";
4255
+ 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.";
3838
4256
  function isToolInput8(v) {
3839
4257
  if (typeof v !== "object" || v === null) return false;
3840
4258
  const o = v;
@@ -3891,7 +4309,7 @@ var inputSchema9 = {
3891
4309
  },
3892
4310
  additionalProperties: false
3893
4311
  };
3894
- 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.";
4312
+ 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.";
3895
4313
  function isToolInput9(v) {
3896
4314
  if (typeof v !== "object" || v === null) return false;
3897
4315
  const o = v;
@@ -3978,7 +4396,7 @@ var inputSchema10 = {
3978
4396
  required: ["attempt_id"],
3979
4397
  additionalProperties: false
3980
4398
  };
3981
- 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.";
4399
+ 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.";
3982
4400
  function isToolInput10(v) {
3983
4401
  if (typeof v !== "object" || v === null) return false;
3984
4402
  const o = v;
@@ -4025,7 +4443,7 @@ var inputSchema11 = {
4025
4443
  required: ["run_id"],
4026
4444
  additionalProperties: false
4027
4445
  };
4028
- 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.";
4446
+ 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.";
4029
4447
  function isToolInput11(v) {
4030
4448
  if (typeof v !== "object" || v === null) return false;
4031
4449
  const o = v;
@@ -4070,7 +4488,7 @@ var inputSchema12 = {
4070
4488
  properties: {},
4071
4489
  additionalProperties: false
4072
4490
  };
4073
- 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.";
4491
+ 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.";
4074
4492
  function isToolInput12(v) {
4075
4493
  if (typeof v !== "object" || v === null) return false;
4076
4494
  return true;
@@ -4093,7 +4511,7 @@ async function handleGetWorkflowRuns(deps, rawInput, _signal) {
4093
4511
  }
4094
4512
 
4095
4513
  // src/tools/pr/common-pr.ts
4096
- 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")';
4514
+ 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.";
4097
4515
  var PR_GATE_TYPE = "admin-action";
4098
4516
 
4099
4517
  // src/tools/pr/list-pending-prs.ts
@@ -4105,7 +4523,7 @@ var inputSchema13 = {
4105
4523
  properties: {},
4106
4524
  additionalProperties: false
4107
4525
  };
4108
- 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.";
4526
+ 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.";
4109
4527
  function isToolInput13(v) {
4110
4528
  return typeof v === "object" && v !== null;
4111
4529
  }
@@ -4141,7 +4559,7 @@ var inputSchema14 = {
4141
4559
  required: ["pr_number"],
4142
4560
  additionalProperties: false
4143
4561
  };
4144
- 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.";
4562
+ 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.";
4145
4563
  function isToolInput14(v) {
4146
4564
  if (typeof v !== "object" || v === null) return false;
4147
4565
  const o = v;
@@ -4185,7 +4603,7 @@ var inputSchema15 = {
4185
4603
  required: ["pr_number"],
4186
4604
  additionalProperties: false
4187
4605
  };
4188
- 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.";
4606
+ 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.";
4189
4607
  function isToolInput15(v) {
4190
4608
  if (typeof v !== "object" || v === null) return false;
4191
4609
  const o = v;
@@ -4223,7 +4641,7 @@ var inputSchema16 = {
4223
4641
  properties: {},
4224
4642
  additionalProperties: false
4225
4643
  };
4226
- 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.";
4644
+ 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.";
4227
4645
  function isToolInput16(v) {
4228
4646
  return typeof v === "object" && v !== null;
4229
4647
  }
@@ -4257,7 +4675,7 @@ var inputSchema17 = {
4257
4675
  required: ["pr_number"],
4258
4676
  additionalProperties: false
4259
4677
  };
4260
- 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.";
4678
+ 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.";
4261
4679
  function isToolInput17(v) {
4262
4680
  if (typeof v !== "object" || v === null) return false;
4263
4681
  const o = v;
@@ -4289,6 +4707,7 @@ async function handleRejectAndRetry(deps, rawInput, _signal) {
4289
4707
  // src/tools/pr/review-merge.ts
4290
4708
  var TOOL_NAME18 = "vo_review_merge";
4291
4709
  var LIST_PATH = "/api/v1/admin/pr/list";
4710
+ var DEFAULT_REVIEW_REPO = "Algosuite-ai/Nexus";
4292
4711
  var ENGINE_GATE = "final-deep-verify";
4293
4712
  var EVENT_GATE = "merge-review";
4294
4713
  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.)";
@@ -4333,7 +4752,7 @@ function buildPrompt4(pr, notes) {
4333
4752
  const lines = [
4334
4753
  "You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
4335
4754
  "Recommend exactly one of: merge / hold / reject. Be conservative \u2014 this is a high-stakes irreversible action.",
4336
- "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.",
4755
+ "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.",
4337
4756
  "",
4338
4757
  `PR #${pr.number}: ${pr.title}`,
4339
4758
  `Source: ${pr.source ?? "unknown"}`,
@@ -4353,6 +4772,11 @@ async function handleReviewMerge(deps, rawInput, signal) {
4353
4772
  if (rawInput.notes !== void 0) normalizedInput.notes = rawInput.notes;
4354
4773
  const inputJson = JSON.stringify(normalizedInput);
4355
4774
  const key = deps.cache.keyFor(TOOL_NAME18, normalizedInput);
4775
+ const subject = {
4776
+ ...subjectFromEnv() ?? {},
4777
+ repo: subjectFromEnv()?.repo ?? DEFAULT_REVIEW_REPO,
4778
+ pr_number: prNumber
4779
+ };
4356
4780
  const baseEvent = buildBaseEvent({
4357
4781
  tool: TOOL_NAME18,
4358
4782
  gateType: EVENT_GATE,
@@ -4360,7 +4784,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
4360
4784
  inputExcerpt: inputJson.slice(0, 300),
4361
4785
  inputSizeBytes: bytesOf(inputJson),
4362
4786
  session: deps.session,
4363
- now: deps.now()
4787
+ now: deps.now(),
4788
+ subject
4364
4789
  });
4365
4790
  const emit = (payload2, eventExtra) => {
4366
4791
  deps.events.append(eventExtra ? { ...baseEvent, ...eventExtra } : baseEvent);
@@ -4401,7 +4826,7 @@ async function handleReviewMerge(deps, rawInput, signal) {
4401
4826
  }
4402
4827
  if (pr === null) {
4403
4828
  return emit(
4404
- emptyPayload("hold", `PR #${prNumber} is not among open VO PRs (already merged/closed, or not a VO-source PR).`, null)
4829
+ emptyPayload("hold", `PR #${prNumber} is not among open AlgoHQ PRs (already merged/closed, or not an AlgoHQ-source PR).`, null)
4405
4830
  );
4406
4831
  }
4407
4832
  const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
@@ -4461,7 +4886,138 @@ async function handleReviewMerge(deps, rawInput, signal) {
4461
4886
  duration_ms: result.duration_ms,
4462
4887
  consensus_engine_version: result.engine_version,
4463
4888
  per_model_verdicts: perModel,
4464
- synthesized_verdict: synth
4889
+ synthesized_verdict: synth,
4890
+ ...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage),
4891
+ ...trajectoryFromEngine(result)
4892
+ });
4893
+ }
4894
+
4895
+ // src/tools/runner/prepared-job-mode.ts
4896
+ var TOOL_NAME19 = "vo_prepared_job_mode";
4897
+ var CALLABLE_NAME10 = "GET|POST /api/v1/runner/prepared-job-mode";
4898
+ var PLANE_PATH = "/api/v1/runner/prepared-job-mode";
4899
+ var PREPARED_JOB_MODE_GATE_TYPE = "admin-action";
4900
+ 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.";
4901
+ var PREPARED_JOB_MODES = ["off", "shadow", "prepared"];
4902
+ var MIN_REASON_CHARS = 10;
4903
+ var MAX_REASON_CHARS = 500;
4904
+ var MAX_RUNNER_ID_CHARS = 100;
4905
+ var inputSchema19 = {
4906
+ type: "object",
4907
+ properties: {
4908
+ action: {
4909
+ type: "string",
4910
+ enum: ["get", "set"],
4911
+ 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)."
4912
+ },
4913
+ runner_id: {
4914
+ type: "string",
4915
+ minLength: 1,
4916
+ maxLength: MAX_RUNNER_ID_CHARS,
4917
+ 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."
4918
+ },
4919
+ prepared_job_mode: {
4920
+ type: "string",
4921
+ enum: [...PREPARED_JOB_MODES],
4922
+ 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)."
4923
+ },
4924
+ expected_revision: {
4925
+ type: "integer",
4926
+ minimum: 0,
4927
+ 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."
4928
+ },
4929
+ reason: {
4930
+ type: "string",
4931
+ minLength: MIN_REASON_CHARS,
4932
+ maxLength: MAX_REASON_CHARS,
4933
+ description: `Required for action='set'. Audit reason stored on the config-change record; at least ${MIN_REASON_CHARS} characters after trimming.`
4934
+ }
4935
+ },
4936
+ required: ["action", "runner_id"],
4937
+ additionalProperties: false
4938
+ };
4939
+ 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.";
4940
+ 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'.`;
4941
+ function parseInput(v) {
4942
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
4943
+ const o = v;
4944
+ const action = o["action"];
4945
+ if (action !== "get" && action !== "set") return null;
4946
+ const rawRunnerId = o["runner_id"];
4947
+ if (typeof rawRunnerId !== "string") return null;
4948
+ const runner_id = rawRunnerId.trim();
4949
+ if (runner_id.length === 0 || runner_id.length > MAX_RUNNER_ID_CHARS) return null;
4950
+ const setOnly = ["prepared_job_mode", "expected_revision", "reason"];
4951
+ if (action === "get") {
4952
+ if (setOnly.some((key) => o[key] !== void 0)) return null;
4953
+ return { action, runner_id };
4954
+ }
4955
+ const mode = o["prepared_job_mode"];
4956
+ if (typeof mode !== "string") return null;
4957
+ if (!PREPARED_JOB_MODES.includes(mode)) return null;
4958
+ const revision = o["expected_revision"];
4959
+ if (typeof revision !== "number") return null;
4960
+ if (!Number.isInteger(revision) || revision < 0) return null;
4961
+ const rawReason = o["reason"];
4962
+ if (typeof rawReason !== "string") return null;
4963
+ const reason = rawReason.trim();
4964
+ if (reason.length < MIN_REASON_CHARS || reason.length > MAX_REASON_CHARS) return null;
4965
+ return {
4966
+ action,
4967
+ runner_id,
4968
+ prepared_job_mode: mode,
4969
+ expected_revision: revision,
4970
+ reason
4971
+ };
4972
+ }
4973
+ async function handlePreparedJobMode(deps, rawInput, _signal) {
4974
+ const input = parseInput(rawInput);
4975
+ if (!input) {
4976
+ throw invalidParams(TOOL_NAME19, SHAPE_HINT);
4977
+ }
4978
+ if (input.action === "get") {
4979
+ return buildCloudOrStubResponse({
4980
+ toolName: TOOL_NAME19,
4981
+ callableName: CALLABLE_NAME10,
4982
+ adminPath: PLANE_PATH,
4983
+ normalizedInput: { action: "get", runner_id: input.runner_id },
4984
+ // A GET carries no body; the runner id rides the query string.
4985
+ cloudBody: {},
4986
+ invokeOptions: { method: "GET", query: { runner_id: input.runner_id } },
4987
+ // The plane answers `{ok, config}` — not the `{ok, callable, result}`
4988
+ // admin-proxy envelope — so take the whole object with `ok` stripped.
4989
+ rawEnvelope: true,
4990
+ gateType: PREPARED_JOB_MODE_GATE_TYPE,
4991
+ stubReason: PREPARED_JOB_MODE_STUB_REASON,
4992
+ readOnly: true,
4993
+ deps
4994
+ });
4995
+ }
4996
+ return buildCloudOrStubResponse({
4997
+ toolName: TOOL_NAME19,
4998
+ callableName: CALLABLE_NAME10,
4999
+ adminPath: PLANE_PATH,
5000
+ normalizedInput: {
5001
+ action: "set",
5002
+ runner_id: input.runner_id,
5003
+ prepared_job_mode: input.prepared_job_mode,
5004
+ expected_revision: input.expected_revision,
5005
+ reason: input.reason
5006
+ },
5007
+ // Exactly the four fields of `updateRunnerPreparedJobConfigInputSchema`
5008
+ // (.strict()) — no `operator_id`, which the route would ignore anyway.
5009
+ cloudBody: {
5010
+ runner_id: input.runner_id,
5011
+ prepared_job_mode: input.prepared_job_mode,
5012
+ expected_revision: input.expected_revision,
5013
+ reason: input.reason
5014
+ },
5015
+ rawEnvelope: true,
5016
+ gateType: PREPARED_JOB_MODE_GATE_TYPE,
5017
+ stubReason: PREPARED_JOB_MODE_STUB_REASON,
5018
+ // A write: stays gated behind VO_ADMIN_CALLABLES_READONLY.
5019
+ readOnly: false,
5020
+ deps
4465
5021
  });
4466
5022
  }
4467
5023
 
@@ -4498,12 +5054,12 @@ function suggestedHandoffPath(session_id, isoTimestamp) {
4498
5054
  // src/tools/session/report-session-state.ts
4499
5055
  init_auth_token_source();
4500
5056
  init_credential_store();
4501
- var TOOL_NAME19 = "vo_report_session_state";
5057
+ var TOOL_NAME20 = "vo_report_session_state";
4502
5058
  var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
4503
5059
  var MAX_GOAL_CHARS = 500;
4504
5060
  var MAX_RECENT_FILES = 20;
4505
5061
  var MAX_RECENT_TOOLS = 50;
4506
- var inputSchema19 = {
5062
+ var inputSchema20 = {
4507
5063
  type: "object",
4508
5064
  properties: {
4509
5065
  operator_id: {
@@ -4548,7 +5104,7 @@ var inputSchema19 = {
4548
5104
  required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
4549
5105
  additionalProperties: false
4550
5106
  };
4551
- 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).";
5107
+ 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).";
4552
5108
  function isStringArray2(v, maxItems) {
4553
5109
  if (!Array.isArray(v)) return false;
4554
5110
  if (v.length > maxItems) return false;
@@ -4686,7 +5242,7 @@ async function tryCloudReportState(cloud, input, fetchFn = fetch) {
4686
5242
  async function handleReportSessionState(deps, rawInput, _signal) {
4687
5243
  if (!isToolInput19(rawInput)) {
4688
5244
  throw invalidParams(
4689
- TOOL_NAME19,
5245
+ TOOL_NAME20,
4690
5246
  `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}).`
4691
5247
  );
4692
5248
  }
@@ -4716,99 +5272,702 @@ async function handleReportSessionState(deps, rawInput, _signal) {
4716
5272
 
4717
5273
  // src/tools/session/spawn-successor.ts
4718
5274
  import { spawn } from "node:child_process";
4719
- import { homedir as homedir4 } from "node:os";
4720
- import { join as join6 } from "node:path";
4721
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
4722
- var TOOL_NAME20 = "vo_spawn_successor";
4723
- var MAX_HANDOFF_BYTES = 64e3;
4724
- var inputSchema20 = {
4725
- type: "object",
4726
- properties: {
4727
- handoff_path: {
4728
- type: "string",
4729
- description: "Path to the handoff doc to pre-inject. Default: the newest .md in ~/.vo/handoffs/."
4730
- },
4731
- goal: {
4732
- type: "string",
4733
- description: "Optional one-line goal override appended after the handoff."
4734
- },
4735
- cwd: {
4736
- type: "string",
4737
- description: "Working directory for the successor (default: the repo the handoff names, else process cwd)."
4738
- },
4739
- max_turns: {
4740
- type: "number",
4741
- description: "Optional --max-turns bound for the successor."
4742
- }
4743
- },
4744
- required: [],
4745
- additionalProperties: false
4746
- };
4747
- 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.";
4748
- function isToolInput20(v) {
4749
- if (typeof v !== "object" || v === null) return false;
4750
- const o = v;
4751
- if (o["handoff_path"] !== void 0 && typeof o["handoff_path"] !== "string") return false;
4752
- if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
4753
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
4754
- if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
4755
- return true;
5275
+ import { homedir as homedir5 } from "node:os";
5276
+ import { join as join7 } from "node:path";
5277
+ 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";
5278
+
5279
+ // src/swarm/tier-binding.ts
5280
+ var SWARM_TIERS = Object.freeze([
5281
+ "tier1_subscription",
5282
+ "tier1_local",
5283
+ "tier2_user_key",
5284
+ "tier3_platform_key",
5285
+ "refused",
5286
+ "unresolved"
5287
+ ]);
5288
+ var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
5289
+ "tier1_subscription",
5290
+ "tier1_local",
5291
+ "tier2_user_key",
5292
+ "tier3_platform_key"
5293
+ ]);
5294
+ var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
5295
+ var MAX_BOUND_SUBAGENTS = 20;
5296
+ function isPositiveCap(cap) {
5297
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
5298
+ }
5299
+ function unresolvedBinding(swarmId, nowIso, reason) {
5300
+ return {
5301
+ schema_version: 1,
5302
+ swarm_id: swarmId,
5303
+ tier: "unresolved",
5304
+ agent: null,
5305
+ reason,
5306
+ exhausted_agents: [],
5307
+ subagent_budget: 0,
5308
+ spend_cap_usd: null,
5309
+ resolved_at: nowIso
5310
+ };
4756
5311
  }
4757
- function newestHandoff(dir = join6(homedir4(), ".vo", "handoffs")) {
5312
+ function serializeSwarmTierBinding(binding) {
5313
+ return JSON.stringify(binding);
5314
+ }
5315
+ function parseSwarmTierBinding(raw, nowIso) {
5316
+ if (typeof raw !== "string" || raw.trim().length === 0) {
5317
+ return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
5318
+ }
5319
+ let parsed;
4758
5320
  try {
4759
- 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);
4760
- return entries.length > 0 && entries[0] ? join6(dir, entries[0].f) : null;
5321
+ parsed = JSON.parse(raw);
4761
5322
  } catch {
4762
- return null;
5323
+ return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
5324
+ }
5325
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5326
+ return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
5327
+ }
5328
+ const o = parsed;
5329
+ const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
5330
+ if (o["schema_version"] !== 1) {
5331
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
5332
+ }
5333
+ const tier = o["tier"];
5334
+ if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
5335
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
5336
+ }
5337
+ const budget = o["subagent_budget"];
5338
+ const cap = o["spend_cap_usd"];
5339
+ const capNum = isPositiveCap(cap) ? cap : null;
5340
+ if (tier === "tier3_platform_key" && capNum === null) {
5341
+ return unresolvedBinding(
5342
+ swarmId,
5343
+ nowIso,
5344
+ "inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
5345
+ );
4763
5346
  }
5347
+ return {
5348
+ schema_version: 1,
5349
+ swarm_id: swarmId,
5350
+ tier,
5351
+ agent: typeof o["agent"] === "string" ? o["agent"] : null,
5352
+ reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
5353
+ exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
5354
+ subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
5355
+ spend_cap_usd: capNum,
5356
+ resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
5357
+ };
4764
5358
  }
4765
- var MANDATORY_READS = [
4766
- "CLAUDE.md + AGENTS.md + README.md (repo root)",
4767
- "docs/current/virtual-office-agent-charter.md, -operating-model.md, -test-architect.md",
4768
- "docs/current/evidence-grounded-consensus-testing.md",
4769
- "docs/vo/ADR-001-* (verify + sign, human approves merge; no autonomous bot-merge / headless triggers) + docs/vo/vo-adr-002-two-plane-moat.md",
4770
- "docs/vo/vo-roadmap-2026-05-26.md (read the Change log tail for current state)",
4771
- "the operator memory index ~/.claude/projects/C--Users-greyl/memory/MEMORY.md"
4772
- ];
4773
- function buildSuccessorPrompt(handoffMarkdown, goal) {
4774
- const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
4775
- const lines = [
4776
- "You are the SUCCESSOR agent for a Virtual Office lane. The previous session",
4777
- "exhausted its context and wrote the handoff below. Read it fully, verify its",
4778
- '"verification needed" items against live state (a handoff is a claim, not',
4779
- "evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
4780
- "",
4781
- "MANDATORY READS before writing any code (NOT all auto-loaded \u2014 open them):",
4782
- reads,
4783
- "",
4784
- "NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
4785
- "(verified-answer-only, no fake green); verify-before-act + human merge approval;",
4786
- "never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
4787
- "branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and VO changes update",
4788
- "the roadmap in the same PR.",
4789
- "",
4790
- "--- HANDOFF ---",
4791
- handoffMarkdown,
4792
- "--- END HANDOFF ---"
4793
- ];
4794
- if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
4795
- return lines.join("\n");
5359
+ function inheritSwarmTierBinding(env, nowIso) {
5360
+ return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
4796
5361
  }
4797
- function buildSuccessorArgs(maxTurns) {
4798
- const args = ["-p", "--permission-mode", "acceptEdits"];
4799
- if (Number.isInteger(maxTurns) && maxTurns > 0) {
4800
- args.push("--max-turns", String(maxTurns));
4801
- }
4802
- return args;
5362
+ function bindingEnvFragment(binding) {
5363
+ return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
4803
5364
  }
4804
- async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
4805
- if (!isToolInput20(rawInput)) {
4806
- throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
5365
+ function childBindingEnvFragment(binding, allocatedCapUsd = null) {
5366
+ return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
5367
+ }
5368
+ function admitSubagentSpawn(binding, spawnsSoFar = 0) {
5369
+ if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
5370
+ return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
4807
5371
  }
4808
- const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
4809
- if (!handoffPath || !existsSync4(handoffPath)) {
5372
+ if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
5373
+ return {
5374
+ allowed: false,
5375
+ reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
5376
+ };
5377
+ }
5378
+ if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
5379
+ return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
5380
+ }
5381
+ if (spawnsSoFar >= binding.subagent_budget) {
5382
+ return {
5383
+ allowed: false,
5384
+ reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
5385
+ };
5386
+ }
5387
+ return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
5388
+ }
5389
+ function childBinding(binding, allocatedCapUsd = null) {
5390
+ const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
5391
+ const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
5392
+ return {
5393
+ ...binding,
5394
+ subagent_budget: Math.max(0, binding.subagent_budget - 1),
5395
+ // A child never carries more than its parent, whatever the ledger says: a
5396
+ // forged or hand-edited pool cannot inflate a descendant above the binding
5397
+ // it descends from.
5398
+ spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
5399
+ };
5400
+ }
5401
+ function agentBindingRefusal(binding, requestedAgent) {
5402
+ const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
5403
+ if (requested.length === 0) return null;
5404
+ if (binding.agent !== null && requested === binding.agent) return null;
5405
+ 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)`;
5406
+ }
5407
+
5408
+ // src/swarm/successor-launch.ts
5409
+ var AGENT_LAUNCH_SHAPES = Object.freeze({
5410
+ claude: {
5411
+ bin: "claude",
5412
+ baseArgs: ["-p", "--permission-mode", "acceptEdits"],
5413
+ enforcesMaxTurns: true,
5414
+ maxTurnsFlag: "--max-turns",
5415
+ windowsShellSafe: true
5416
+ },
5417
+ codex: {
5418
+ bin: "codex",
5419
+ baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
5420
+ enforcesMaxTurns: false,
5421
+ // `-` makes codex read the prompt from stdin (injection-safe), matching how
5422
+ // codex-runner.mjs already spawns it.
5423
+ trailingArgs: ["-"],
5424
+ // `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
5425
+ // unverified, so win32 refuses rather than risking a mangled sandbox flag.
5426
+ windowsShellSafe: false
5427
+ }
5428
+ });
5429
+ function resolveSuccessorLaunch(input) {
5430
+ const agent = typeof input.agent === "string" ? input.agent.trim() : "";
5431
+ if (!agent) {
5432
+ return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
5433
+ }
5434
+ const shape = AGENT_LAUNCH_SHAPES[agent];
5435
+ if (!shape) {
5436
+ const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
5437
+ return {
5438
+ ok: false,
5439
+ reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
5440
+ };
5441
+ }
5442
+ const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
5443
+ if (wantsMaxTurns && !shape.enforcesMaxTurns) {
5444
+ return {
5445
+ ok: false,
5446
+ reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
5447
+ };
5448
+ }
5449
+ const platform = input.platform ?? process.platform;
5450
+ if (platform === "win32" && !shape.windowsShellSafe) {
5451
+ return {
5452
+ ok: false,
5453
+ 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`
5454
+ };
5455
+ }
5456
+ const args = [...shape.baseArgs];
5457
+ if (wantsMaxTurns && shape.maxTurnsFlag) {
5458
+ args.push(shape.maxTurnsFlag, String(input.maxTurns));
5459
+ }
5460
+ if (shape.trailingArgs) args.push(...shape.trailingArgs);
5461
+ return { ok: true, agent, bin: shape.bin, args };
5462
+ }
5463
+
5464
+ // src/swarm/spawn-ledger.ts
5465
+ import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5466
+ import { homedir as homedir4 } from "node:os";
5467
+ import { join as join6 } from "node:path";
5468
+ var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
5469
+ function resolveLedgerDir(env) {
5470
+ const override = env[SWARM_LEDGER_DIR_ENV];
5471
+ if (typeof override === "string" && override.trim().length > 0) return override.trim();
5472
+ return join6(homedir4(), ".vo", "swarm-ledger");
5473
+ }
5474
+ function sanitizeSwarmId(raw) {
5475
+ if (typeof raw !== "string") return null;
5476
+ const id = raw.trim();
5477
+ if (id.length === 0 || id.length > 128) return null;
5478
+ if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
5479
+ if (id === "." || id === "..") return null;
5480
+ return id;
5481
+ }
5482
+ var CEILING_FILE = "ceiling.json";
5483
+ function createExclusive(path4, contents) {
5484
+ let fd;
5485
+ try {
5486
+ fd = openSync(path4, "wx");
5487
+ } catch {
5488
+ return false;
5489
+ }
5490
+ try {
5491
+ writeFileSync3(fd, contents, "utf8");
5492
+ } finally {
5493
+ closeSync(fd);
5494
+ }
5495
+ return true;
5496
+ }
5497
+ function capToCents(cap) {
5498
+ return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
5499
+ }
5500
+ function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
5501
+ const path4 = join6(swarmDir, CEILING_FILE);
5502
+ const head = JSON.stringify({
5503
+ ceiling: proposedCeiling,
5504
+ cap_cents: proposedCapCents,
5505
+ recorded_at: nowIso
5506
+ });
5507
+ if (createExclusive(path4, head)) {
5508
+ return { ceiling: proposedCeiling, capCents: proposedCapCents };
5509
+ }
5510
+ let parsed;
5511
+ try {
5512
+ parsed = JSON.parse(readFileSync6(path4, "utf8"));
5513
+ } catch {
5514
+ return null;
5515
+ }
5516
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
5517
+ const record = parsed;
5518
+ const recorded = record["ceiling"];
5519
+ if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
5520
+ const recordedCap = record["cap_cents"];
5521
+ const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
5522
+ return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
5523
+ }
5524
+ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
5525
+ const id = sanitizeSwarmId(swarmId);
5526
+ if (id === null) {
5527
+ return {
5528
+ ok: false,
5529
+ 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`
5530
+ };
5531
+ }
5532
+ const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
5533
+ if (proposed < 1) {
5534
+ return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
5535
+ }
5536
+ const swarmDir = join6(dir, id);
5537
+ try {
5538
+ mkdirSync3(swarmDir, { recursive: true });
5539
+ } catch (err) {
5540
+ return {
5541
+ ok: false,
5542
+ reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
5543
+ };
5544
+ }
5545
+ const wantedCents = capToCents(proposedCapUsd);
5546
+ const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
5547
+ if (head === null) {
5548
+ return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
5549
+ }
5550
+ const { ceiling, capCents } = head;
5551
+ const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
5552
+ if (wantedCents > 0 && shareCents < 1) {
5553
+ return {
5554
+ ok: false,
5555
+ 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`
5556
+ };
5557
+ }
5558
+ for (let slot = 0; slot < ceiling; slot++) {
5559
+ const debitedCents = shareCents;
5560
+ const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
5561
+ const claimed = createExclusive(
5562
+ join6(swarmDir, `slot-${slot}.json`),
5563
+ JSON.stringify({
5564
+ slot,
5565
+ ceiling,
5566
+ pid: process.pid,
5567
+ claimed_at: nowIso,
5568
+ // The debit record. Durable and atomic with the claim: this file is
5569
+ // created with O_EXCL, so exactly one claimant ever writes this line.
5570
+ cap_cents_pool: capCents,
5571
+ cap_cents_debited: debitedCents,
5572
+ cap_cents_remaining: remainingCents
5573
+ })
5574
+ );
5575
+ if (claimed) {
5576
+ return {
5577
+ ok: true,
5578
+ slot,
5579
+ ceiling,
5580
+ remaining: ceiling - slot - 1,
5581
+ capUsd: debitedCents > 0 ? debitedCents / 100 : null,
5582
+ capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
5583
+ };
5584
+ }
5585
+ }
5586
+ return {
5587
+ ok: false,
5588
+ reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
5589
+ };
5590
+ };
5591
+
5592
+ // src/swarm/spawn-plan.ts
5593
+ function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
5594
+ const rawBinding = env[SWARM_TIER_BINDING_ENV];
5595
+ const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
5596
+ if (!hasBinding) {
5597
+ const explicit = input.agent?.trim();
5598
+ const resolved2 = resolveSuccessorLaunch({ agent: explicit || "claude", maxTurns: input.max_turns, platform });
5599
+ if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
5600
+ return {
5601
+ ok: true,
5602
+ bin: resolved2.bin,
5603
+ args: resolved2.args,
5604
+ agent: resolved2.agent,
5605
+ tier: "unbound",
5606
+ bound: false,
5607
+ env: {},
5608
+ slot: null,
5609
+ capUsd: null,
5610
+ capRemainingUsd: null
5611
+ };
5612
+ }
5613
+ const binding = inheritSwarmTierBinding(env, nowIso);
5614
+ const admission = admitSubagentSpawn(binding);
5615
+ if (!admission.allowed) {
5616
+ return { ok: false, reason: admission.reason, tier: binding.tier };
5617
+ }
5618
+ const agentRefusal = agentBindingRefusal(binding, input.agent);
5619
+ if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
5620
+ const resolved = resolveSuccessorLaunch({
5621
+ agent: binding.agent,
5622
+ maxTurns: input.max_turns,
5623
+ platform
5624
+ });
5625
+ if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
5626
+ const slot = claim({
5627
+ swarmId: binding.swarm_id,
5628
+ proposedCeiling: binding.subagent_budget,
5629
+ // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
5630
+ // child's cap is DEBITED from it below, not recomputed from this binding.
5631
+ proposedCapUsd: binding.spend_cap_usd,
5632
+ dir: resolveLedgerDir(env),
5633
+ nowIso
5634
+ });
5635
+ if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
5636
+ return {
5637
+ ok: true,
5638
+ bin: resolved.bin,
5639
+ args: resolved.args,
5640
+ agent: resolved.agent,
5641
+ tier: binding.tier,
5642
+ bound: true,
5643
+ // Re-export the same TIER with a DECREMENTED budget and the spend cap the
5644
+ // ledger just DEBITED. Exporting the binding verbatim (what this did before
5645
+ // #9312) meant the child re-read the full budget and every generation
5646
+ // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
5647
+ // bounded a chain but not a tree: three siblings each re-halved the parent's
5648
+ // untouched $50 and walked away with $75 between them.
5649
+ env: childBindingEnvFragment(binding, slot.capUsd),
5650
+ slot: slot.slot,
5651
+ capUsd: slot.capUsd,
5652
+ capRemainingUsd: slot.capRemainingUsd
5653
+ };
5654
+ }
5655
+
5656
+ // ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
5657
+ import { existsSync as existsSync4, realpathSync } from "node:fs";
5658
+ import { win32 as path3 } from "node:path";
5659
+ import { spawnSync as spawnSync2 } from "node:child_process";
5660
+ var NATIVE_CLAUDE_PARTS = [
5661
+ "node_modules",
5662
+ "@anthropic-ai",
5663
+ "claude-code",
5664
+ "bin",
5665
+ "claude.exe"
5666
+ ];
5667
+ function pathValue(env) {
5668
+ for (const key of ["Path", "PATH", "path"]) {
5669
+ if (typeof env?.[key] === "string") return env[key];
5670
+ }
5671
+ return "";
5672
+ }
5673
+ function cleanPathSegment(value) {
5674
+ const trimmed = String(value || "").trim();
5675
+ return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
5676
+ }
5677
+ function envValue(env, name) {
5678
+ const exact = env?.[name];
5679
+ if (typeof exact === "string") return exact.trim();
5680
+ const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
5681
+ return typeof env?.[key] === "string" ? env[key].trim() : "";
5682
+ }
5683
+ function userClaudeCandidates(bin, env) {
5684
+ if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
5685
+ const userProfile = envValue(env, "USERPROFILE");
5686
+ const appData = envValue(env, "APPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Roaming") : "");
5687
+ const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Local") : "");
5688
+ const candidates = [];
5689
+ if (appData) {
5690
+ const npmBin = path3.join(appData, "npm");
5691
+ candidates.push(
5692
+ path3.join(npmBin, "claude.exe"),
5693
+ path3.join(npmBin, "claude.cmd"),
5694
+ path3.join(npmBin, "claude.ps1"),
5695
+ path3.join(npmBin, "claude"),
5696
+ path3.join(npmBin, ...NATIVE_CLAUDE_PARTS)
5697
+ );
5698
+ }
5699
+ if (userProfile) candidates.push(path3.join(userProfile, ".local", "bin", "claude.exe"));
5700
+ if (localAppData) {
5701
+ candidates.push(
5702
+ path3.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
5703
+ path3.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
5704
+ );
5705
+ }
5706
+ return candidates;
5707
+ }
5708
+ function pathCandidates(bin, env) {
5709
+ if (path3.isAbsolute(bin) || /[\\/]/u.test(bin)) {
5710
+ return [path3.resolve(bin)];
5711
+ }
5712
+ const extension = path3.extname(bin);
5713
+ const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path3.join(directory, bin)] : [
5714
+ path3.join(directory, `${bin}.exe`),
5715
+ path3.join(directory, `${bin}.cmd`),
5716
+ path3.join(directory, `${bin}.ps1`),
5717
+ path3.join(directory, bin)
5718
+ ]);
5719
+ const seen = /* @__PURE__ */ new Set();
5720
+ return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
5721
+ const key = candidate.toLowerCase();
5722
+ if (seen.has(key)) return false;
5723
+ seen.add(key);
5724
+ return true;
5725
+ });
5726
+ }
5727
+ function canonicalExistingPath(candidate, exists, canonicalize2) {
5728
+ if (!exists(candidate)) return null;
5729
+ try {
5730
+ return canonicalize2(candidate);
5731
+ } catch {
5732
+ return null;
5733
+ }
5734
+ }
5735
+ function resolveWindowsClaudeExecutable({
5736
+ bin = "claude",
5737
+ env = process.env,
5738
+ exists = existsSync4,
5739
+ canonicalize: canonicalize2 = realpathSync
5740
+ } = {}) {
5741
+ const requested = String(bin || "").trim();
5742
+ if (!requested || requested.includes("\0")) {
5743
+ throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
5744
+ }
5745
+ for (const candidate of pathCandidates(requested, env)) {
5746
+ const found = canonicalExistingPath(candidate, exists, canonicalize2);
5747
+ if (!found) continue;
5748
+ if (path3.extname(found).toLowerCase() === ".exe") return found;
5749
+ const native = path3.join(path3.dirname(found), ...NATIVE_CLAUDE_PARTS);
5750
+ const resolvedNative = canonicalExistingPath(native, exists, canonicalize2);
5751
+ if (resolvedNative) return resolvedNative;
5752
+ }
5753
+ const error = new Error(
5754
+ `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.`
5755
+ );
5756
+ error.code = "ENOENT";
5757
+ throw error;
5758
+ }
5759
+
5760
+ // src/swarm/successor-windows-exe.ts
5761
+ var resolveWindowsClaudeExe = (bin, env) => resolveWindowsClaudeExecutable({ bin, env });
5762
+ function resolveNativeWindowsExecutable(bin, env = process.env, resolve3 = resolveWindowsClaudeExe) {
5763
+ try {
5764
+ return { ok: true, bin: resolve3(bin, env) };
5765
+ } catch (error) {
5766
+ const message = error instanceof Error ? error.message : String(error);
5767
+ return {
5768
+ ok: false,
5769
+ reason: `could not resolve a native Windows executable for '${bin}': ${message}`
5770
+ };
5771
+ }
5772
+ }
5773
+
5774
+ // src/swarm/successor-liveness.ts
5775
+ import { statSync as statSync3 } from "node:fs";
5776
+ var DEFAULT_EARLY_EXIT_SEC = 10;
5777
+ var DEFAULT_NO_OUTPUT_SEC = 0;
5778
+ var DEFAULT_POLL_MS = 200;
5779
+ var KILL_ESCALATION_MS = 2e3;
5780
+ function positiveSeconds(raw, fallback) {
5781
+ if (raw === void 0) return fallback;
5782
+ const n = Number(raw.trim());
5783
+ return Number.isFinite(n) && n > 0 ? n : fallback;
5784
+ }
5785
+ function resolveLivenessConfigFromEnv(env = process.env, explicitOverride = {}) {
5786
+ const earlyExitMs = explicitOverride.earlyExitMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_EXIT_CHECK_SEC"], DEFAULT_EARLY_EXIT_SEC) * 1e3;
5787
+ const noOutputMs = explicitOverride.noOutputMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_OUTPUT_CHECK_SEC"], DEFAULT_NO_OUTPUT_SEC) * 1e3;
5788
+ return { earlyExitMs, noOutputMs };
5789
+ }
5790
+ function defaultStatLogBytes(path4) {
5791
+ try {
5792
+ return statSync3(path4).size;
5793
+ } catch {
5794
+ return 0;
5795
+ }
5796
+ }
5797
+ function defaultKillChild(child) {
5798
+ try {
5799
+ child.kill("SIGTERM");
5800
+ } catch {
5801
+ }
5802
+ const escalation = setTimeout(() => {
5803
+ try {
5804
+ child.kill("SIGKILL");
5805
+ } catch {
5806
+ }
5807
+ }, KILL_ESCALATION_MS);
5808
+ escalation.unref();
5809
+ }
5810
+ function checkSuccessorLiveness(child, logPath, config, deps = {}) {
5811
+ const statLogBytes = deps.statLogBytes ?? defaultStatLogBytes;
5812
+ const now = deps.now ?? Date.now;
5813
+ const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS;
5814
+ const killChild = deps.killChild ?? defaultKillChild;
5815
+ const outputGateEnabled = Number.isFinite(config.noOutputMs) && config.noOutputMs > 0;
5816
+ const startedAt = now();
5817
+ return new Promise((resolve3) => {
5818
+ let settled = false;
5819
+ let pollTimer = null;
5820
+ const onExit = (code, signal) => {
5821
+ const elapsedMs = now() - startedAt;
5822
+ finish({
5823
+ ok: false,
5824
+ reason: `child_exited_early: exit code ${code ?? "null"} signal ${signal ?? "none"} after ${elapsedMs}ms`,
5825
+ detail: { exitCode: code, signal, elapsedMs }
5826
+ });
5827
+ };
5828
+ const cleanup = () => {
5829
+ if (pollTimer !== null) clearInterval(pollTimer);
5830
+ child.off?.("exit", onExit);
5831
+ };
5832
+ const finish = (result) => {
5833
+ if (settled) return;
5834
+ settled = true;
5835
+ cleanup();
5836
+ if (!result.ok) {
5837
+ try {
5838
+ killChild(child);
5839
+ } catch {
5840
+ }
5841
+ }
5842
+ resolve3(result);
5843
+ };
5844
+ if (typeof child.exitCode === "number" || typeof child.signalCode === "string" && child.signalCode.length > 0) {
5845
+ finish({
5846
+ ok: false,
5847
+ reason: `child_exited_early: exit code ${child.exitCode ?? "null"} signal ${child.signalCode ?? "none"} before the liveness watch attached`,
5848
+ detail: { exitCode: child.exitCode ?? null, signal: child.signalCode ?? null, elapsedMs: 0 }
5849
+ });
5850
+ return;
5851
+ }
5852
+ child.on("exit", onExit);
5853
+ const tick = () => {
5854
+ if (settled) return;
5855
+ const elapsedMs = now() - startedAt;
5856
+ const outputSeen = outputGateEnabled ? statLogBytes(logPath) > 0 : true;
5857
+ if (outputSeen && elapsedMs >= config.earlyExitMs) {
5858
+ finish({ ok: true });
5859
+ return;
5860
+ }
5861
+ if (outputGateEnabled && !outputSeen && elapsedMs >= config.noOutputMs) {
5862
+ finish({
5863
+ ok: false,
5864
+ reason: `no_output: log stayed empty for ${elapsedMs}ms (limit ${config.noOutputMs}ms)`,
5865
+ detail: { elapsedMs, logPath }
5866
+ });
5867
+ }
5868
+ };
5869
+ pollTimer = setInterval(tick, pollIntervalMs);
5870
+ tick();
5871
+ });
5872
+ }
5873
+
5874
+ // src/tools/session/spawn-successor.ts
5875
+ var TOOL_NAME21 = "vo_spawn_successor";
5876
+ var MAX_HANDOFF_BYTES = 64e3;
5877
+ var inputSchema21 = {
5878
+ type: "object",
5879
+ properties: {
5880
+ handoff_path: {
5881
+ type: "string",
5882
+ description: "Path to the handoff doc to pre-inject. Default: the newest .md in ~/.vo/handoffs/."
5883
+ },
5884
+ goal: {
5885
+ type: "string",
5886
+ description: "Optional one-line goal override appended after the handoff."
5887
+ },
5888
+ cwd: {
5889
+ type: "string",
5890
+ description: "Working directory for the successor (default: the repo the handoff names, else process cwd)."
5891
+ },
5892
+ max_turns: {
5893
+ type: "number",
5894
+ description: "Optional --max-turns bound for the successor."
5895
+ },
5896
+ agent: {
5897
+ type: "string",
5898
+ 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.`
5899
+ }
5900
+ },
5901
+ required: [],
5902
+ additionalProperties: false
5903
+ };
5904
+ var RETIRED_COUNTER_INPUT = "spawns_so_far";
5905
+ 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.";
5906
+ function isToolInput20(v) {
5907
+ if (typeof v !== "object" || v === null) return false;
5908
+ const o = v;
5909
+ if (o["handoff_path"] !== void 0 && typeof o["handoff_path"] !== "string") return false;
5910
+ if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
5911
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
5912
+ if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
5913
+ if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
5914
+ return true;
5915
+ }
5916
+ function retiredCounterRefusal(v) {
5917
+ if (typeof v !== "object" || v === null) return null;
5918
+ if (!(RETIRED_COUNTER_INPUT in v)) return null;
5919
+ 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.`;
5920
+ }
5921
+ function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
5922
+ try {
5923
+ 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);
5924
+ return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
5925
+ } catch {
5926
+ return null;
5927
+ }
5928
+ }
5929
+ var MANDATORY_READS = [
5930
+ "CLAUDE.md + AGENTS.md + README.md (repo root)",
5931
+ "docs/current/virtual-office-agent-charter.md, -operating-model.md, -test-architect.md",
5932
+ "docs/current/evidence-grounded-consensus-testing.md",
5933
+ "docs/vo/ADR-001-* (verify + sign, human approves merge; no autonomous bot-merge / headless triggers) + docs/vo/vo-adr-002-two-plane-moat.md",
5934
+ "docs/vo/vo-roadmap-2026-05-26.md (read the Change log tail for current state)",
5935
+ "the operator memory index ~/.claude/projects/C--Users-greyl/memory/MEMORY.md"
5936
+ ];
5937
+ function buildSuccessorPrompt(handoffMarkdown, goal) {
5938
+ const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
5939
+ const lines = [
5940
+ "You are the SUCCESSOR agent for an AlgoHQ lane. The previous session",
5941
+ "exhausted its context and wrote the handoff below. Read it fully, verify its",
5942
+ '"verification needed" items against live state (a handoff is a claim, not',
5943
+ "evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
5944
+ "",
5945
+ "MANDATORY READS before writing any code (NOT all auto-loaded \u2014 open them):",
5946
+ reads,
5947
+ "",
5948
+ "NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
5949
+ "(verified-answer-only, no fake green); verify-before-act + human merge approval;",
5950
+ "never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
5951
+ "branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and AlgoHQ changes update",
5952
+ "the roadmap in the same PR.",
5953
+ "",
5954
+ "--- HANDOFF ---",
5955
+ handoffMarkdown,
5956
+ "--- END HANDOFF ---"
5957
+ ];
5958
+ if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
5959
+ return lines.join("\n");
5960
+ }
5961
+ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
5962
+ const retired = retiredCounterRefusal(rawInput);
5963
+ if (retired !== null) throw invalidParams(TOOL_NAME21, retired);
5964
+ if (!isToolInput20(rawInput)) {
5965
+ throw invalidParams(TOOL_NAME21, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
5966
+ }
5967
+ const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
5968
+ if (!handoffPath || !existsSync5(handoffPath)) {
4810
5969
  return jsonContent({
4811
- tool: TOOL_NAME20,
5970
+ tool: TOOL_NAME21,
4812
5971
  schema_version: 1,
4813
5972
  payload: {
4814
5973
  spawned: false,
@@ -4816,25 +5975,67 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
4816
5975
  }
4817
5976
  });
4818
5977
  }
4819
- const handoff = readFileSync6(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
5978
+ const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
4820
5979
  const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
4821
- const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join6(homedir4(), ".vo", "successors");
4822
- mkdirSync3(logDir, { recursive: true });
4823
- const logPath = join6(logDir, `successor-${Date.now()}.log`);
4824
- const logFd = openSync(logPath, "a");
4825
- const child = spawnImpl("claude", buildSuccessorArgs(rawInput.max_turns), {
5980
+ const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
5981
+ if (!plan.ok) {
5982
+ return jsonContent({
5983
+ tool: TOOL_NAME21,
5984
+ schema_version: 1,
5985
+ payload: {
5986
+ spawned: false,
5987
+ reason: `swarm tier binding refused this spawn: ${plan.reason}`,
5988
+ tier: plan.tier,
5989
+ handoff_path: handoffPath
5990
+ }
5991
+ });
5992
+ }
5993
+ const platform = overrides.platform ?? process.platform;
5994
+ let resolvedBin = plan.bin;
5995
+ if (platform === "win32") {
5996
+ const resolution = resolveNativeWindowsExecutable(plan.bin, process.env, overrides.resolveWindowsExecutable);
5997
+ if (!resolution.ok) {
5998
+ return jsonContent({
5999
+ tool: TOOL_NAME21,
6000
+ schema_version: 1,
6001
+ payload: {
6002
+ spawned: false,
6003
+ reason: resolution.reason,
6004
+ agent: plan.agent,
6005
+ tier: plan.tier,
6006
+ handoff_path: handoffPath
6007
+ }
6008
+ });
6009
+ }
6010
+ resolvedBin = resolution.bin;
6011
+ }
6012
+ const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
6013
+ mkdirSync4(logDir, { recursive: true });
6014
+ const logPath = join7(logDir, `successor-${Date.now()}.log`);
6015
+ const logFd = openSync2(logPath, "a");
6016
+ const child = spawnImpl(resolvedBin, [...plan.args], {
4826
6017
  cwd: rawInput.cwd?.trim() || process.cwd(),
4827
6018
  detached: true,
4828
6019
  stdio: ["pipe", logFd, logFd],
4829
- // Windows: `claude` is a .cmd shim needs a shell to resolve. The prompt
4830
- // goes via STDIN below, never argv, so the shell never sees it.
4831
- shell: process.platform === "win32",
4832
- windowsHide: true
6020
+ // Never a shell: `resolvedBin` is either the bare platform-neutral name
6021
+ // (POSIX, resolved by the OS via PATH + shebang) or the native win32 exe
6022
+ // resolved above — routing either through cmd.exe/sh is the extra layer
6023
+ // a detached, unref'd child can lose silently (2026-08-16 incident).
6024
+ shell: false,
6025
+ windowsHide: true,
6026
+ windowsVerbatimArguments: false,
6027
+ // Carry the SAME binding to the child. Without this the successor inherits
6028
+ // no tier and re-resolves its own — which is the split-payer defect one
6029
+ // generation down.
6030
+ ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
4833
6031
  });
6032
+ closeSync2(logFd);
4834
6033
  let spawnError = null;
4835
6034
  child.on("error", (e) => {
4836
6035
  spawnError = e.message;
4837
6036
  });
6037
+ child.stdin.on?.("error", () => {
6038
+ });
4838
6039
  try {
4839
6040
  child.stdin.write(prompt);
4840
6041
  child.stdin.end();
@@ -4842,10 +6043,56 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
4842
6043
  }
4843
6044
  child.unref();
4844
6045
  await new Promise((r) => setTimeout(r, 150));
6046
+ if (spawnError) {
6047
+ return jsonContent({
6048
+ tool: TOOL_NAME21,
6049
+ schema_version: 1,
6050
+ payload: {
6051
+ spawned: false,
6052
+ reason: `spawn failed: ${spawnError}`,
6053
+ agent: plan.agent,
6054
+ tier: plan.tier,
6055
+ handoff_path: handoffPath
6056
+ }
6057
+ });
6058
+ }
6059
+ const checkLiveness = overrides.checkLiveness ?? ((c, p) => checkSuccessorLiveness(c, p, resolveLivenessConfigFromEnv(process.env, overrides.livenessConfig), overrides.livenessDeps));
6060
+ const liveness = await checkLiveness(child, logPath);
6061
+ if (!liveness.ok) {
6062
+ return jsonContent({
6063
+ tool: TOOL_NAME21,
6064
+ schema_version: 1,
6065
+ payload: {
6066
+ spawned: false,
6067
+ reason: liveness.reason,
6068
+ pid: child.pid ?? null,
6069
+ log_path: logPath,
6070
+ agent: plan.agent,
6071
+ tier: plan.tier,
6072
+ handoff_path: handoffPath
6073
+ }
6074
+ });
6075
+ }
4845
6076
  return jsonContent({
4846
- tool: TOOL_NAME20,
6077
+ tool: TOOL_NAME21,
4847
6078
  schema_version: 1,
4848
- payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, handoff_path: handoffPath } : { spawned: true, pid: child.pid ?? null, log_path: logPath, handoff_path: handoffPath }
6079
+ payload: {
6080
+ spawned: true,
6081
+ pid: child.pid ?? null,
6082
+ log_path: logPath,
6083
+ handoff_path: handoffPath,
6084
+ agent: plan.agent,
6085
+ tier: plan.tier,
6086
+ tier_bound: plan.bound,
6087
+ ledger_slot: plan.slot,
6088
+ // The debit, surfaced so an operator can reconcile a fan-out's spend
6089
+ // against the pool without reading the ledger directory by hand.
6090
+ ledger_cap_usd: plan.capUsd,
6091
+ ledger_cap_remaining_usd: plan.capRemainingUsd,
6092
+ // Additive (2026-08-17): true only once the child survived the
6093
+ // early-exit window (and the output window, when that gate is enabled).
6094
+ verified_alive: true
6095
+ }
4849
6096
  });
4850
6097
  }
4851
6098
 
@@ -4867,10 +6114,10 @@ function isKnownConciergePack(value) {
4867
6114
  }
4868
6115
 
4869
6116
  // src/tools/concierge/dispatch.ts
4870
- var TOOL_NAME21 = "vo_concierge_dispatch";
4871
- var CALLABLE_NAME10 = "voConciergeDispatch";
6117
+ var TOOL_NAME22 = "vo_concierge_dispatch";
6118
+ var CALLABLE_NAME11 = "voConciergeDispatch";
4872
6119
  var ADMIN_PATH10 = "/api/v1/admin/concierge/dispatch";
4873
- var inputSchema21 = {
6120
+ var inputSchema22 = {
4874
6121
  type: "object",
4875
6122
  properties: {
4876
6123
  pack: {
@@ -4885,7 +6132,7 @@ var inputSchema21 = {
4885
6132
  },
4886
6133
  additionalProperties: false
4887
6134
  };
4888
- 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'`.";
6135
+ 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'`.";
4889
6136
  function isToolInput21(v) {
4890
6137
  if (typeof v !== "object" || v === null) return false;
4891
6138
  const obj = v;
@@ -4896,13 +6143,13 @@ function isToolInput21(v) {
4896
6143
  async function handleConciergeDispatch(deps, rawInput, _signal) {
4897
6144
  if (!isToolInput21(rawInput)) {
4898
6145
  throw invalidParams(
4899
- TOOL_NAME21,
6146
+ TOOL_NAME22,
4900
6147
  "invalid input. Expected { pack?: string, tenant_id?: string }."
4901
6148
  );
4902
6149
  }
4903
6150
  if (rawInput.pack !== void 0 && rawInput.pack !== "" && !isKnownConciergePack(rawInput.pack)) {
4904
6151
  throw invalidParams(
4905
- TOOL_NAME21,
6152
+ TOOL_NAME22,
4906
6153
  `unknown pack: ${JSON.stringify(rawInput.pack)}. Known packs: ${KNOWN_CONCIERGE_PACKS.join(", ")}.`
4907
6154
  );
4908
6155
  }
@@ -4913,8 +6160,8 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
4913
6160
  if (rawInput.pack) cloudBody.pack = rawInput.pack;
4914
6161
  if (rawInput.tenant_id) cloudBody.tenantId = rawInput.tenant_id;
4915
6162
  return buildCloudOrStubResponse({
4916
- toolName: TOOL_NAME21,
4917
- callableName: CALLABLE_NAME10,
6163
+ toolName: TOOL_NAME22,
6164
+ callableName: CALLABLE_NAME11,
4918
6165
  adminPath: ADMIN_PATH10,
4919
6166
  normalizedInput,
4920
6167
  cloudBody,
@@ -4931,70 +6178,294 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
4931
6178
  }
4932
6179
 
4933
6180
  // src/tools/memory/sync-config.ts
4934
- import { homedir as homedir5 } from "node:os";
4935
- import { join as join7 } from "node:path";
4936
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "node:fs";
6181
+ import { existsSync as existsSync9 } from "node:fs";
6182
+ import { homedir as homedir7 } from "node:os";
6183
+ import { join as join11 } from "node:path";
4937
6184
 
4938
- // src/tools/memory/safe-memory-file.ts
4939
- import { resolve, sep } from "node:path";
4940
- var SAFE_MEMORY_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._ -]*\.md$/i;
4941
- function isSafeMemoryFileName(fileName) {
4942
- return fileName.length <= 200 && fileName.trim() === fileName && !fileName.includes("/") && !fileName.includes("\\") && !fileName.includes(":") && SAFE_MEMORY_FILE_RE.test(fileName);
6185
+ // src/tools/memory/memory-sync-http.ts
6186
+ init_safe_memory_file();
6187
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
6188
+
6189
+ // src/tools/memory/sync-lock.ts
6190
+ import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
6191
+ import { hostname } from "node:os";
6192
+ import { join as join8 } from "node:path";
6193
+ import { randomUUID as randomUUID2 } from "node:crypto";
6194
+
6195
+ // src/tools/memory/sync-lock-liveness.ts
6196
+ import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
6197
+ function defaultIsProcessAlive(pid) {
6198
+ try {
6199
+ process.kill(pid, 0);
6200
+ return true;
6201
+ } catch (err) {
6202
+ return err.code === "EPERM";
6203
+ }
4943
6204
  }
4944
- function resolveMemoryFilePath(memoryDir, fileName) {
4945
- if (!isSafeMemoryFileName(fileName)) {
4946
- throw new Error(`unsafe memory file_name: ${fileName.slice(0, 80)}`);
6205
+ function toPayload(parsed) {
6206
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
6207
+ const record = parsed;
6208
+ const token = record["token"];
6209
+ const host = record["hostname"];
6210
+ if (typeof token !== "string" || token.length === 0) return null;
6211
+ const pid = record["pid"];
6212
+ const acquiredAtMs = record["acquiredAtMs"];
6213
+ return {
6214
+ pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
6215
+ hostname: typeof host === "string" ? host : "",
6216
+ sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
6217
+ token,
6218
+ acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
6219
+ acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
6220
+ };
6221
+ }
6222
+ function readLockRecord(path4) {
6223
+ let raw;
6224
+ try {
6225
+ raw = readFileSync8(path4, "utf8");
6226
+ } catch {
6227
+ return null;
4947
6228
  }
4948
- const root = resolve(memoryDir);
4949
- const filePath = resolve(root, fileName);
4950
- const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
4951
- if (filePath !== root && !filePath.startsWith(rootPrefix)) {
4952
- throw new Error(`memory file path escapes memory directory: ${fileName.slice(0, 80)}`);
6229
+ try {
6230
+ return { raw, payload: toPayload(JSON.parse(raw)) };
6231
+ } catch {
6232
+ return { raw, payload: null };
4953
6233
  }
4954
- return filePath;
6234
+ }
6235
+ function lockAgeMs(record, path4, nowMs) {
6236
+ let startedMs = Number.NaN;
6237
+ if (record.payload) {
6238
+ if (Number.isFinite(record.payload.acquiredAtMs)) {
6239
+ startedMs = record.payload.acquiredAtMs;
6240
+ } else if (record.payload.acquiredAt) {
6241
+ startedMs = Date.parse(record.payload.acquiredAt);
6242
+ }
6243
+ }
6244
+ if (!Number.isFinite(startedMs)) {
6245
+ try {
6246
+ startedMs = statSync5(path4).mtimeMs;
6247
+ } catch {
6248
+ return null;
6249
+ }
6250
+ }
6251
+ const age = nowMs - startedMs;
6252
+ return Number.isFinite(age) && age >= 0 ? age : null;
6253
+ }
6254
+ function classifyHolderLiveness(record, isProcessAlive, thisHost) {
6255
+ const payload = record.payload;
6256
+ if (payload === null) return "unknown";
6257
+ if (payload.pid <= 0) return "unknown";
6258
+ if (thisHost.length === 0) return "unknown";
6259
+ if (payload.hostname !== thisHost) return "unknown";
6260
+ return isProcessAlive(payload.pid) ? "alive" : "dead";
6261
+ }
6262
+ function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
6263
+ const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
6264
+ if (liveness === "alive") return false;
6265
+ if (liveness === "dead") return true;
6266
+ return ageMs !== null && ageMs > ttlMs;
4955
6267
  }
4956
6268
 
4957
- // src/tools/memory/sync-config.ts
4958
- var TOOL_NAME22 = "vo_sync_config";
4959
- var inputSchema22 = {
4960
- type: "object",
4961
- properties: {
4962
- action: {
4963
- type: "string",
4964
- enum: ["pull", "push"],
4965
- description: "pull: download cloud memory to local files. push: upload local files to cloud."
4966
- },
4967
- cwd: {
4968
- type: "string",
4969
- description: "Working directory to derive project slug from (default: process.cwd())."
6269
+ // src/tools/memory/sync-lock.ts
6270
+ var MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
6271
+ var DEFAULT_LOCK_TTL_MS = 15 * 6e4;
6272
+ var DEFAULT_LOCK_WAIT_MS = 1e4;
6273
+ var INITIAL_BACKOFF_MS = 25;
6274
+ var MAX_BACKOFF_MS = 500;
6275
+ var BACKOFF_FACTOR = 1.6;
6276
+ function positiveOr(value, fallback) {
6277
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
6278
+ }
6279
+ function createExclusive2(path4, contents) {
6280
+ let fd;
6281
+ try {
6282
+ fd = openSync3(path4, "wx");
6283
+ } catch (err) {
6284
+ const code = err.code;
6285
+ return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
6286
+ }
6287
+ try {
6288
+ writeFileSync4(fd, contents, "utf8");
6289
+ } catch (err) {
6290
+ closeSync3(fd);
6291
+ try {
6292
+ unlinkSync2(path4);
6293
+ } catch {
4970
6294
  }
4971
- },
4972
- required: ["action"],
4973
- additionalProperties: false
4974
- };
4975
- var 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.";
4976
- function isToolInput22(v) {
4977
- if (typeof v !== "object" || v === null) return false;
4978
- const o = v;
4979
- if (o["action"] !== "pull" && o["action"] !== "push") return false;
4980
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
4981
- return true;
6295
+ return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
6296
+ }
6297
+ closeSync3(fd);
6298
+ return { ok: true };
4982
6299
  }
4983
- function deriveProjectSlug(cwd) {
4984
- return cwd.replace(/\\/g, "/").replace(/\/+$/g, "").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
6300
+ function removeAbandoned(path4, expectedRaw) {
6301
+ let current;
6302
+ try {
6303
+ current = readFileSync9(path4, "utf8");
6304
+ } catch {
6305
+ return;
6306
+ }
6307
+ if (current !== expectedRaw) return;
6308
+ try {
6309
+ unlinkSync2(path4);
6310
+ } catch {
6311
+ }
4985
6312
  }
4986
- function getMemoryDir(cwd) {
4987
- const slug = deriveProjectSlug(cwd);
4988
- return join7(homedir5(), ".claude", "projects", slug, "memory");
6313
+ function makeRelease(path4, token) {
6314
+ let released = false;
6315
+ return () => {
6316
+ if (released) return;
6317
+ released = true;
6318
+ let raw;
6319
+ try {
6320
+ raw = readFileSync9(path4, "utf8");
6321
+ } catch {
6322
+ return;
6323
+ }
6324
+ let stillOurs;
6325
+ try {
6326
+ stillOurs = toPayload(JSON.parse(raw))?.token === token;
6327
+ } catch {
6328
+ stillOurs = false;
6329
+ }
6330
+ if (!stillOurs) return;
6331
+ try {
6332
+ unlinkSync2(path4);
6333
+ } catch {
6334
+ }
6335
+ };
6336
+ }
6337
+ function describeHolder(record) {
6338
+ const payload = record?.payload;
6339
+ if (!payload) return "an unreadable lock file";
6340
+ return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
6341
+ }
6342
+ async function acquireMemorySyncLock(options) {
6343
+ const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
6344
+ const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
6345
+ const now = options.now ?? Date.now;
6346
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
6347
+ setTimeout(resolve3, ms);
6348
+ }));
6349
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
6350
+ const thisHost = hostname();
6351
+ const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
6352
+ if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
6353
+ const deadline = now() + waitMs;
6354
+ let backoffMs = INITIAL_BACKOFF_MS;
6355
+ let tookOverFrom = null;
6356
+ let holderDescription = "another session";
6357
+ for (; ; ) {
6358
+ const acquiredAtMs = now();
6359
+ const payload = {
6360
+ pid: process.pid,
6361
+ hostname: thisHost,
6362
+ sessionId: options.sessionId ?? null,
6363
+ token: randomUUID2(),
6364
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
6365
+ acquiredAtMs
6366
+ };
6367
+ const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
6368
+ `);
6369
+ if (created.ok) {
6370
+ return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
6371
+ }
6372
+ if (!created.exists) {
6373
+ throw new Error(
6374
+ `memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
6375
+ );
6376
+ }
6377
+ const record = readLockRecord(path4);
6378
+ let reclaimed = false;
6379
+ if (record) {
6380
+ holderDescription = describeHolder(record);
6381
+ const age = lockAgeMs(record, path4, now());
6382
+ if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
6383
+ tookOverFrom = record.payload;
6384
+ removeAbandoned(path4, record.raw);
6385
+ reclaimed = true;
6386
+ }
6387
+ }
6388
+ if (now() >= deadline) {
6389
+ throw new Error(
6390
+ `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.`
6391
+ );
6392
+ }
6393
+ if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
6394
+ await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
6395
+ if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
6396
+ }
6397
+ }
6398
+ async function withMemorySyncLock(options, fn) {
6399
+ const handle = await acquireMemorySyncLock(options);
6400
+ try {
6401
+ return await fn(handle);
6402
+ } finally {
6403
+ handle.release();
6404
+ }
4989
6405
  }
6406
+
6407
+ // src/tools/memory/memory-index-merge.ts
6408
+ var MEMORY_INDEX_FILE = "MEMORY.md";
6409
+ function isMemoryIndexFile(fileName) {
6410
+ return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
6411
+ }
6412
+ var INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
6413
+ function indexRowKey(line) {
6414
+ const match = INDEX_ROW_RE.exec(line);
6415
+ if (!match) return null;
6416
+ let target = match[1].trim();
6417
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
6418
+ target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
6419
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
6420
+ target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
6421
+ target = target.replace(/^(?:\.\/)+/, "");
6422
+ }
6423
+ return target.length > 0 ? target.toLowerCase() : null;
6424
+ }
6425
+ function mergeMemoryIndex(localContent, cloudContent) {
6426
+ if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
6427
+ return { content: localContent, addedFromCloud: [] };
6428
+ }
6429
+ const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
6430
+ const localLines = localContent.split(/\r?\n/);
6431
+ const localKeys = /* @__PURE__ */ new Set();
6432
+ let lastLocalRowIndex = -1;
6433
+ for (let i = 0; i < localLines.length; i++) {
6434
+ const key = indexRowKey(localLines[i]);
6435
+ if (key === null) continue;
6436
+ localKeys.add(key);
6437
+ lastLocalRowIndex = i;
6438
+ }
6439
+ const addedFromCloud = [];
6440
+ const seenCloudKeys = /* @__PURE__ */ new Set();
6441
+ for (const rawLine of cloudContent.split(/\r?\n/)) {
6442
+ const key = indexRowKey(rawLine);
6443
+ if (key === null) continue;
6444
+ if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
6445
+ seenCloudKeys.add(key);
6446
+ addedFromCloud.push(rawLine.replace(/\r$/, ""));
6447
+ }
6448
+ if (addedFromCloud.length === 0) {
6449
+ return { content: localContent, addedFromCloud: [] };
6450
+ }
6451
+ const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
6452
+ return { content: merged.join(eol), addedFromCloud };
6453
+ }
6454
+
6455
+ // src/tools/memory/memory-sync-http.ts
6456
+ init_bounded_sync();
6457
+ init_memory_push_cache();
4990
6458
  async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
4991
6459
  const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
4992
- const response = await fetchFn(url, {
4993
- method: "GET",
4994
- headers: {
4995
- authorization: `Bearer ${token}`
4996
- }
4997
- });
6460
+ const response = await withRequestTimeout(
6461
+ url,
6462
+ () => fetchFn(url, {
6463
+ method: "GET",
6464
+ headers: {
6465
+ authorization: `Bearer ${token}`
6466
+ }
6467
+ })
6468
+ );
4998
6469
  if (response.status !== 200) {
4999
6470
  const text = await response.text();
5000
6471
  throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
@@ -5007,103 +6478,246 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
5007
6478
  entry,
5008
6479
  filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
5009
6480
  }));
5010
- mkdirSync4(memoryDir, { recursive: true });
6481
+ mkdirSync6(memoryDir, { recursive: true });
5011
6482
  const files = [];
5012
6483
  for (const { entry, filePath } of writes) {
5013
- writeFileSync3(filePath, entry.content, "utf8");
6484
+ writeFileSync6(filePath, entry.content, "utf8");
5014
6485
  files.push(entry.file_name);
5015
6486
  }
5016
6487
  return { pulled: data.entries.length, files };
5017
6488
  }
5018
- async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
5019
- if (!existsSync5(memoryDir)) {
5020
- return { pushed: 0, created: 0, updated: 0 };
6489
+ function listPushableFiles(memoryDir) {
6490
+ return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
6491
+ }
6492
+ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
6493
+ deadline.check();
6494
+ if (item.memoryId !== null) {
6495
+ const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
6496
+ const updateBody = { content: item.content, session_id: sessionId };
6497
+ const updateResponse = await withRequestTimeout(
6498
+ updateUrl,
6499
+ () => fetchFn(updateUrl, {
6500
+ method: "PUT",
6501
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6502
+ body: JSON.stringify(updateBody)
6503
+ })
6504
+ );
6505
+ if (updateResponse.status !== 200) {
6506
+ const text = await updateResponse.text();
6507
+ throw new Error(
6508
+ `PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
6509
+ );
6510
+ }
6511
+ const updateData = JSON.parse(await updateResponse.text());
6512
+ if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
6513
+ return "updated";
6514
+ }
6515
+ const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
6516
+ const createBody = {
6517
+ entry_type: item.entryType,
6518
+ file_name: item.fileName,
6519
+ content: item.content,
6520
+ session_id: sessionId
6521
+ };
6522
+ const createResponse = await withRequestTimeout(
6523
+ createUrl,
6524
+ () => fetchFn(createUrl, {
6525
+ method: "POST",
6526
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6527
+ body: JSON.stringify(createBody)
6528
+ })
6529
+ );
6530
+ if (createResponse.status !== 200 && createResponse.status !== 201) {
6531
+ const text = await createResponse.text();
6532
+ throw new Error(
6533
+ `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
6534
+ );
6535
+ }
6536
+ const createData = JSON.parse(await createResponse.text());
6537
+ if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
6538
+ return "created";
6539
+ }
6540
+ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
6541
+ const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
6542
+ if (!existsSync6(memoryDir)) {
6543
+ return empty;
5021
6544
  }
5022
- const localFiles = readdirSync4(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
6545
+ const localFiles = listPushableFiles(memoryDir).map((f) => ({
5023
6546
  file_name: f,
5024
- content: readFileSync7(resolveMemoryFilePath(memoryDir, f), "utf8"),
5025
- entry_type: f === "MEMORY.md" ? "index" : "topic"
6547
+ content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
6548
+ entry_type: isMemoryIndexFile(f) ? "index" : "topic"
5026
6549
  }));
5027
6550
  if (localFiles.length === 0) {
5028
- return { pushed: 0, created: 0, updated: 0 };
6551
+ return empty;
5029
6552
  }
6553
+ const deadline = options.deadline ?? createSyncDeadline();
6554
+ const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
6555
+ deadline.check();
5030
6556
  const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5031
- const getResponse = await fetchFn(getUrl, {
5032
- method: "GET",
5033
- headers: {
5034
- authorization: `Bearer ${token}`
5035
- }
5036
- });
6557
+ const getResponse = await withRequestTimeout(
6558
+ getUrl,
6559
+ () => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
6560
+ );
5037
6561
  const existingMap = /* @__PURE__ */ new Map();
5038
6562
  if (getResponse.status === 200) {
5039
6563
  const getData = JSON.parse(await getResponse.text());
5040
6564
  if (getData.ok && Array.isArray(getData.entries)) {
5041
6565
  for (const entry of getData.entries) {
5042
- existingMap.set(entry.file_name, entry.memory_id);
6566
+ existingMap.set(entry.file_name, {
6567
+ memoryId: entry.memory_id,
6568
+ content: typeof entry.content === "string" ? entry.content : ""
6569
+ });
5043
6570
  }
5044
6571
  }
5045
6572
  }
6573
+ const toUpload = [];
6574
+ let skipped = 0;
6575
+ for (const localFile of localFiles) {
6576
+ const existing = existingMap.get(localFile.file_name);
6577
+ let content = localFile.content;
6578
+ let rowsPreserved = 0;
6579
+ if (localFile.entry_type === "index") {
6580
+ const merged = mergeMemoryIndex(localFile.content, existing?.content);
6581
+ content = merged.content;
6582
+ rowsPreserved = merged.addedFromCloud.length;
6583
+ }
6584
+ const payloadHash = sha256(content);
6585
+ if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
6586
+ skipped++;
6587
+ continue;
6588
+ }
6589
+ toUpload.push({
6590
+ fileName: localFile.file_name,
6591
+ entryType: localFile.entry_type,
6592
+ content,
6593
+ payloadHash,
6594
+ memoryId: existing?.memoryId ?? null,
6595
+ rowsPreserved
6596
+ });
6597
+ }
6598
+ const outcomes = await mapWithConcurrency(
6599
+ toUpload,
6600
+ options.concurrency ?? PUSH_CONCURRENCY,
6601
+ (item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
6602
+ );
5046
6603
  let created = 0;
5047
6604
  let updated = 0;
5048
- for (const localFile of localFiles) {
5049
- const memoryId = existingMap.get(localFile.file_name);
5050
- if (memoryId) {
5051
- const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${memoryId}`;
5052
- const updateBody = {
5053
- content: localFile.content,
5054
- session_id: sessionId
5055
- };
5056
- const updateResponse = await fetchFn(updateUrl, {
5057
- method: "PUT",
5058
- headers: {
5059
- authorization: `Bearer ${token}`,
5060
- "content-type": "application/json"
5061
- },
5062
- body: JSON.stringify(updateBody)
5063
- });
5064
- if (updateResponse.status !== 200) {
5065
- const text = await updateResponse.text();
5066
- throw new Error(
5067
- `PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
5068
- );
5069
- }
5070
- const updateData = JSON.parse(await updateResponse.text());
5071
- if (!updateData.ok) {
5072
- throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
5073
- }
5074
- updated++;
5075
- } else {
5076
- const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5077
- const createBody = {
5078
- entry_type: localFile.entry_type,
5079
- file_name: localFile.file_name,
5080
- content: localFile.content,
5081
- session_id: sessionId
5082
- };
5083
- const createResponse = await fetchFn(createUrl, {
5084
- method: "POST",
5085
- headers: {
5086
- authorization: `Bearer ${token}`,
5087
- "content-type": "application/json"
5088
- },
5089
- body: JSON.stringify(createBody)
5090
- });
5091
- if (createResponse.status !== 200 && createResponse.status !== 201) {
5092
- const text = await createResponse.text();
5093
- throw new Error(
5094
- `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
5095
- );
5096
- }
5097
- const createData = JSON.parse(await createResponse.text());
5098
- if (!createData.ok) {
5099
- throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
5100
- }
5101
- created++;
6605
+ let indexRowsPreserved = 0;
6606
+ let firstError;
6607
+ for (let i = 0; i < outcomes.length; i++) {
6608
+ const outcome = outcomes[i];
6609
+ const item = toUpload[i];
6610
+ if (outcome.ok) {
6611
+ if (outcome.value === "created") created++;
6612
+ else updated++;
6613
+ indexRowsPreserved += item.rowsPreserved;
6614
+ recordMemoryPush(cache, item.fileName, item.payloadHash);
6615
+ } else if (firstError === void 0) {
6616
+ firstError = outcome.error;
6617
+ }
6618
+ }
6619
+ pruneMissing(cache, localFiles.map((f) => f.file_name));
6620
+ if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
6621
+ if (firstError !== void 0) throw firstError;
6622
+ return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
6623
+ }
6624
+
6625
+ // src/tools/memory/sync-config.ts
6626
+ init_bounded_sync();
6627
+ init_memory_push_cache();
6628
+
6629
+ // src/tools/memory/sync-kill-switch.ts
6630
+ import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
6631
+ import { homedir as homedir6 } from "node:os";
6632
+ import { join as join10 } from "node:path";
6633
+ var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
6634
+ var MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
6635
+ var NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
6636
+ var MAX_LOGGED_VALUE = 32;
6637
+ function memorySyncSentinelPath(home) {
6638
+ return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
6639
+ }
6640
+ function isKillSwitchValueOn(raw) {
6641
+ if (raw === void 0 || raw === null) return false;
6642
+ const v = raw.trim().toLowerCase();
6643
+ if (v === "") return false;
6644
+ return !NEGATIONS.has(v);
6645
+ }
6646
+ function clip(raw) {
6647
+ const v = raw.trim();
6648
+ return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
6649
+ }
6650
+ function evaluateMemorySyncKillSwitch(deps = {}) {
6651
+ const env = deps.env ?? process.env;
6652
+ const home = deps.home ?? homedir6();
6653
+ const fileExists = deps.fileExists ?? existsSync7;
6654
+ const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
6655
+ const fired = [];
6656
+ const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
6657
+ if (isKillSwitchValueOn(rawEnv)) {
6658
+ fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
6659
+ }
6660
+ const sentinel = memorySyncSentinelPath(home);
6661
+ let sentinelPresent;
6662
+ try {
6663
+ sentinelPresent = fileExists(sentinel);
6664
+ } catch {
6665
+ sentinelPresent = false;
6666
+ }
6667
+ if (sentinelPresent) {
6668
+ let contents = "";
6669
+ let readable = true;
6670
+ try {
6671
+ contents = readFile3(sentinel);
6672
+ } catch {
6673
+ readable = false;
6674
+ }
6675
+ if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
6676
+ fired.push(`sentinel file ${sentinel}`);
5102
6677
  }
5103
6678
  }
5104
- return { pushed: localFiles.length, created, updated };
6679
+ if (fired.length === 0) return { disabled: false, reason: null };
6680
+ return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
6681
+ }
6682
+
6683
+ // src/tools/memory/sync-config.ts
6684
+ var TOOL_NAME23 = "vo_sync_config";
6685
+ var inputSchema23 = {
6686
+ type: "object",
6687
+ properties: {
6688
+ action: {
6689
+ type: "string",
6690
+ enum: ["pull", "push"],
6691
+ description: "pull: download cloud memory to local files. push: upload local files to cloud."
6692
+ },
6693
+ cwd: {
6694
+ type: "string",
6695
+ description: "Working directory to derive project slug from (default: process.cwd())."
6696
+ }
6697
+ },
6698
+ required: ["action"],
6699
+ additionalProperties: false
6700
+ };
6701
+ var 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.";
6702
+ function isToolInput22(v) {
6703
+ if (typeof v !== "object" || v === null) return false;
6704
+ const o = v;
6705
+ if (o["action"] !== "pull" && o["action"] !== "push") return false;
6706
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
6707
+ return true;
6708
+ }
6709
+ function deriveProjectSlug(cwd) {
6710
+ return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
5105
6711
  }
5106
- async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
6712
+ function getMemoryDir(cwd) {
6713
+ const slug = deriveProjectSlug(cwd);
6714
+ return join11(homedir7(), ".claude", "projects", slug, "memory");
6715
+ }
6716
+ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
6717
+ const killSwitch = evaluateMemorySyncKillSwitch();
6718
+ if (killSwitch.disabled) {
6719
+ return { synced: false, reason: killSwitch.reason };
6720
+ }
5107
6721
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
5108
6722
  if (!controlPlaneUrl) {
5109
6723
  return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
@@ -5120,20 +6734,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
5120
6734
  }
5121
6735
  const memoryDir = getMemoryDir(cwd);
5122
6736
  const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
5123
- try {
5124
- if (action === "pull") {
5125
- const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
5126
- return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
5127
- }
5128
- const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
6737
+ if (action === "push" && !existsSync9(memoryDir)) {
5129
6738
  return {
5130
6739
  synced: true,
5131
6740
  action: "push",
5132
- pushed: result.pushed,
5133
- created: result.created,
5134
- updated: result.updated,
6741
+ pushed: 0,
6742
+ created: 0,
6743
+ updated: 0,
6744
+ skipped: 0,
6745
+ index_rows_preserved: 0,
6746
+ knowledge_upserted: 0,
6747
+ knowledge_failed: 0,
5135
6748
  memory_dir: memoryDir
5136
6749
  };
6750
+ }
6751
+ try {
6752
+ return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
6753
+ const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
6754
+ if (action === "pull") {
6755
+ const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
6756
+ return {
6757
+ synced: true,
6758
+ action: "pull",
6759
+ pulled: result2.pulled,
6760
+ files: result2.files,
6761
+ memory_dir: memoryDir,
6762
+ ...takeover
6763
+ };
6764
+ }
6765
+ const deadline = createSyncDeadline();
6766
+ const cache = readPushCache(memoryDir, baseUrl);
6767
+ let result;
6768
+ try {
6769
+ result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
6770
+ } catch (err) {
6771
+ writePushCache(memoryDir, cache);
6772
+ throw err;
6773
+ }
6774
+ let bridge;
6775
+ try {
6776
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
6777
+ bridge = await upsertMemoryFilesAsKnowledge2({
6778
+ controlPlaneUrl: baseUrl,
6779
+ token,
6780
+ memoryDir,
6781
+ fetchFn,
6782
+ cache,
6783
+ deadline
6784
+ });
6785
+ } catch (err) {
6786
+ bridge = {
6787
+ upserted: 0,
6788
+ failed: 1,
6789
+ skipped: 0,
6790
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
6791
+ };
6792
+ }
6793
+ writePushCache(memoryDir, cache);
6794
+ return {
6795
+ synced: true,
6796
+ action: "push",
6797
+ pushed: result.pushed,
6798
+ created: result.created,
6799
+ updated: result.updated,
6800
+ skipped: result.skipped,
6801
+ index_rows_preserved: result.indexRowsPreserved,
6802
+ memory_dir: memoryDir,
6803
+ knowledge_upserted: bridge.upserted,
6804
+ knowledge_failed: bridge.failed,
6805
+ knowledge_skipped: bridge.skipped,
6806
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
6807
+ ...takeover
6808
+ };
6809
+ });
5137
6810
  } catch (err) {
5138
6811
  const message = err instanceof Error ? err.message : String(err);
5139
6812
  return { synced: false, reason: `Sync failed: ${message}` };
@@ -5142,18 +6815,20 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
5142
6815
  async function handleSyncConfig(deps, rawInput, _signal, fetchFn = globalThis.fetch) {
5143
6816
  if (!isToolInput22(rawInput)) {
5144
6817
  throw invalidParams(
5145
- TOOL_NAME22,
6818
+ TOOL_NAME23,
5146
6819
  'invalid input. Required: { action: "pull" | "push" }. Optional: { cwd: "<path>" }.'
5147
6820
  );
5148
6821
  }
5149
6822
  const cwd = rawInput.cwd?.trim() || process.cwd();
5150
6823
  const result = await runMemorySync(rawInput.action, cwd, deps.session.sessionId, fetchFn);
5151
- return jsonContent({ tool: TOOL_NAME22, schema_version: 1, payload: result });
6824
+ return jsonContent({ tool: TOOL_NAME23, schema_version: 1, payload: result });
5152
6825
  }
5153
6826
 
5154
6827
  // src/tools/memory/private-knowledge.ts
5155
6828
  var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
5156
6829
  var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
6830
+ var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
6831
+ var STALE_TOOL_NAME = "vo_private_knowledge_stale";
5157
6832
  var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
5158
6833
  var PRECISION_CHAR_BUDGET = 12e3;
5159
6834
  var upsertInputSchema = {
@@ -5177,8 +6852,35 @@ var contextInputSchema = {
5177
6852
  required: ["query"],
5178
6853
  additionalProperties: false
5179
6854
  };
6855
+ var invalidateInputSchema = {
6856
+ type: "object",
6857
+ properties: {
6858
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
6859
+ 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." }
6860
+ },
6861
+ required: ["knowledge_class", "source_path"],
6862
+ additionalProperties: false
6863
+ };
6864
+ var staleInputSchema = {
6865
+ type: "object",
6866
+ properties: {
6867
+ 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." },
6868
+ limit: { type: "number", minimum: 1, maximum: 500 }
6869
+ },
6870
+ additionalProperties: false
6871
+ };
6872
+ 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.`;
5180
6873
  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.";
5181
6874
  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.";
6875
+ 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.`;
6876
+ function isStaleInput(value) {
6877
+ if (value === void 0 || value === null) return true;
6878
+ if (typeof value !== "object") return false;
6879
+ const input = value;
6880
+ if (input["days"] !== void 0 && typeof input["days"] !== "number") return false;
6881
+ if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
6882
+ return true;
6883
+ }
5182
6884
  function isKnowledgeClass(value) {
5183
6885
  return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
5184
6886
  }
@@ -5187,6 +6889,11 @@ function isUpsertInput(value) {
5187
6889
  const input = value;
5188
6890
  return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
5189
6891
  }
6892
+ function isInvalidateInput(value) {
6893
+ if (typeof value !== "object" || value === null) return false;
6894
+ const input = value;
6895
+ return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
6896
+ }
5190
6897
  function isContextInput(value) {
5191
6898
  if (typeof value !== "object" || value === null) return false;
5192
6899
  const input = value;
@@ -5208,10 +6915,10 @@ async function getCloudAuth(fetchFn) {
5208
6915
  if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
5209
6916
  return { ok: true, controlPlaneUrl, token };
5210
6917
  }
5211
- async function callPrivateKnowledge(path3, body, fetchFn) {
6918
+ async function callPrivateKnowledge(path4, body, fetchFn) {
5212
6919
  const auth = await getCloudAuth(fetchFn);
5213
6920
  if (!auth.ok) return { ok: false, reason: auth.reason };
5214
- const response = await fetchFn(`${auth.controlPlaneUrl}${path3}`, {
6921
+ const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
5215
6922
  method: "POST",
5216
6923
  headers: {
5217
6924
  authorization: `Bearer ${auth.token}`,
@@ -5220,7 +6927,12 @@ async function callPrivateKnowledge(path3, body, fetchFn) {
5220
6927
  body: JSON.stringify(body)
5221
6928
  });
5222
6929
  const text = await response.text();
5223
- const parsed = text ? JSON.parse(text) : null;
6930
+ let parsed;
6931
+ try {
6932
+ parsed = text ? JSON.parse(text) : null;
6933
+ } catch {
6934
+ parsed = null;
6935
+ }
5224
6936
  if (response.status < 200 || response.status >= 300) {
5225
6937
  return { ok: false, status: response.status, response: parsed ?? text };
5226
6938
  }
@@ -5241,6 +6953,37 @@ async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn =
5241
6953
  }
5242
6954
  return jsonContent(envelope);
5243
6955
  }
6956
+ async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
6957
+ if (!isInvalidateInput(rawInput)) {
6958
+ throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
6959
+ }
6960
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
6961
+ return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
6962
+ }
6963
+ async function handlePrivateKnowledgeStale(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
6964
+ if (!isStaleInput(rawInput)) {
6965
+ throw invalidParams(STALE_TOOL_NAME, "expected { optional days, optional limit }.");
6966
+ }
6967
+ const auth = await getCloudAuth(fetchFn);
6968
+ if (!auth.ok) return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload: { ok: false, reason: auth.reason } });
6969
+ const params = new URLSearchParams();
6970
+ if (rawInput?.days !== void 0) params.set("days", String(Math.trunc(rawInput.days)));
6971
+ if (rawInput?.limit !== void 0) params.set("limit", String(Math.trunc(rawInput.limit)));
6972
+ const qs = params.toString();
6973
+ const response = await fetchFn(`${auth.controlPlaneUrl}/api/v1/knowledge/private/stale${qs ? `?${qs}` : ""}`, {
6974
+ method: "GET",
6975
+ headers: { authorization: `Bearer ${auth.token}` }
6976
+ });
6977
+ const text = await response.text();
6978
+ let parsed;
6979
+ try {
6980
+ parsed = text ? JSON.parse(text) : null;
6981
+ } catch {
6982
+ parsed = null;
6983
+ }
6984
+ const payload = response.status < 200 || response.status >= 300 ? { ok: false, status: response.status, response: parsed ?? text } : parsed;
6985
+ return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload });
6986
+ }
5244
6987
  async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
5245
6988
  if (!isContextInput(rawInput)) {
5246
6989
  throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
@@ -5249,6 +6992,337 @@ async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn =
5249
6992
  return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
5250
6993
  }
5251
6994
 
6995
+ // src/tools/hq/whiteboard.ts
6996
+ init_auth_token_source();
6997
+ init_credential_store();
6998
+ var POST_TOOL_NAME = "hq_whiteboard_post";
6999
+ var READ_TOOL_NAME = "hq_whiteboard_read";
7000
+ 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.";
7001
+ 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.";
7002
+ var postInputSchema = {
7003
+ type: "object",
7004
+ properties: {
7005
+ from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
7006
+ type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
7007
+ content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
7008
+ targetAgent: { type: "string", maxLength: 100 },
7009
+ tester: { type: "string", maxLength: 100 },
7010
+ tier: { type: "string", maxLength: 32 }
7011
+ },
7012
+ required: ["from", "type", "content"],
7013
+ additionalProperties: false
7014
+ };
7015
+ var readInputSchema = {
7016
+ type: "object",
7017
+ properties: {
7018
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
7019
+ since: { type: "string", description: "Optional ISO-8601 lower bound." },
7020
+ type: { type: "string", minLength: 1, maxLength: 64 }
7021
+ },
7022
+ additionalProperties: false
7023
+ };
7024
+ function resolveTimeoutMs() {
7025
+ const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
7026
+ return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
7027
+ }
7028
+ function isRecord(value) {
7029
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7030
+ }
7031
+ function onlyKeys(value, allowed) {
7032
+ return Object.keys(value).every((key) => allowed.includes(key));
7033
+ }
7034
+ function isBoundedString(value, min, max) {
7035
+ return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
7036
+ }
7037
+ function parsePostInput(value) {
7038
+ if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
7039
+ if (!isBoundedString(value["from"], 1, 100)) return null;
7040
+ if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
7041
+ if (!isBoundedString(value["content"], 1, 500)) return null;
7042
+ for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
7043
+ if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
7044
+ }
7045
+ return {
7046
+ from: value["from"].trim(),
7047
+ type: value["type"].trim(),
7048
+ content: value["content"].trim(),
7049
+ ...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
7050
+ ...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
7051
+ ...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
7052
+ };
7053
+ }
7054
+ function parseReadInput(value) {
7055
+ if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
7056
+ if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
7057
+ if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
7058
+ if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
7059
+ return {
7060
+ ...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
7061
+ ...typeof value["since"] === "string" ? { since: value["since"] } : {},
7062
+ ...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
7063
+ };
7064
+ }
7065
+ async function resolveCloud(fetchFn) {
7066
+ const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
7067
+ if (!url) return null;
7068
+ try {
7069
+ const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
7070
+ const token = await source?.getToken();
7071
+ return token ? { url, token } : null;
7072
+ } catch {
7073
+ return null;
7074
+ }
7075
+ }
7076
+ async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
7077
+ const cloud = await resolveCloud(fetchFn);
7078
+ if (!cloud) {
7079
+ return {
7080
+ ok: false,
7081
+ error: "hq_whiteboard_not_configured",
7082
+ message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
7083
+ };
7084
+ }
7085
+ const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
7086
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
7087
+ const query = new URLSearchParams();
7088
+ if (method === "GET") {
7089
+ const input = bodyOrQuery;
7090
+ query.set("limit", String(input.limit ?? 25));
7091
+ if (input.since) query.set("since", input.since);
7092
+ if (input.type) query.set("type", input.type);
7093
+ }
7094
+ try {
7095
+ const response = await fetchFn(
7096
+ `${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
7097
+ {
7098
+ method,
7099
+ headers: {
7100
+ Authorization: `Bearer ${cloud.token}`,
7101
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
7102
+ },
7103
+ ...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
7104
+ signal: requestSignal
7105
+ }
7106
+ );
7107
+ const text = await response.text();
7108
+ let payload;
7109
+ try {
7110
+ payload = JSON.parse(text);
7111
+ } catch {
7112
+ payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
7113
+ }
7114
+ if (!response.ok) {
7115
+ return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
7116
+ }
7117
+ return payload;
7118
+ } catch (error) {
7119
+ return {
7120
+ ok: false,
7121
+ error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
7122
+ message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
7123
+ };
7124
+ }
7125
+ }
7126
+ async function handleHqWhiteboardPost(_deps, rawInput, signal) {
7127
+ const input = parsePostInput(rawInput);
7128
+ if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
7129
+ return jsonContent(await callWhiteboard("POST", input, signal));
7130
+ }
7131
+ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
7132
+ const input = parseReadInput(rawInput);
7133
+ if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
7134
+ return jsonContent(await callWhiteboard("GET", input, signal));
7135
+ }
7136
+
7137
+ // src/tools/skills/skill-corpus.ts
7138
+ import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
7139
+ import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
7140
+
7141
+ // ../skill-registry/src/loader.ts
7142
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
7143
+ import { join as join12 } from "node:path";
7144
+ var InvalidSkillFrontmatterError = class extends Error {
7145
+ constructor(skillFile, reason) {
7146
+ super(`Invalid frontmatter in ${skillFile}: ${reason}`);
7147
+ this.skillFile = skillFile;
7148
+ this.reason = reason;
7149
+ }
7150
+ skillFile;
7151
+ reason;
7152
+ name = "InvalidSkillFrontmatterError";
7153
+ };
7154
+ var FRONTMATTER_DELIMITER = "---";
7155
+ function parseFrontmatter(rawInput, sourcePath) {
7156
+ const raw = rawInput.replace(/\r\n/g, "\n");
7157
+ if (!raw.startsWith(`${FRONTMATTER_DELIMITER}
7158
+ `)) {
7159
+ throw new InvalidSkillFrontmatterError(sourcePath, 'file does not start with frontmatter delimiter "---"');
7160
+ }
7161
+ const afterFirst = raw.slice(FRONTMATTER_DELIMITER.length + 1);
7162
+ const closingIdx = afterFirst.indexOf(`
7163
+ ${FRONTMATTER_DELIMITER}
7164
+ `);
7165
+ if (closingIdx === -1) {
7166
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing closing frontmatter delimiter "---"');
7167
+ }
7168
+ const frontmatterText = afterFirst.slice(0, closingIdx);
7169
+ const body = afterFirst.slice(closingIdx + `
7170
+ ${FRONTMATTER_DELIMITER}
7171
+ `.length);
7172
+ let name = "";
7173
+ let description24 = "";
7174
+ for (const line of frontmatterText.split("\n")) {
7175
+ const trimmed = line.trim();
7176
+ if (trimmed.length === 0) continue;
7177
+ const colonIdx = trimmed.indexOf(":");
7178
+ if (colonIdx === -1) continue;
7179
+ const key = trimmed.slice(0, colonIdx).trim();
7180
+ const value = trimmed.slice(colonIdx + 1).trim();
7181
+ if (key === "name") name = value;
7182
+ else if (key === "description") description24 = value;
7183
+ }
7184
+ if (name.length === 0) {
7185
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "name"');
7186
+ }
7187
+ if (description24.length === 0) {
7188
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "description"');
7189
+ }
7190
+ return { name, description: description24, body };
7191
+ }
7192
+ function loadSkillsFromDir(skillsDir) {
7193
+ const entries = readdirSync6(skillsDir);
7194
+ const skills = [];
7195
+ for (const entry of entries) {
7196
+ const entryPath = join12(skillsDir, entry);
7197
+ let stat;
7198
+ try {
7199
+ stat = statSync6(entryPath);
7200
+ } catch {
7201
+ continue;
7202
+ }
7203
+ if (!stat.isDirectory()) continue;
7204
+ const skillFile = join12(entryPath, "SKILL.md");
7205
+ let raw;
7206
+ try {
7207
+ raw = readFileSync14(skillFile, "utf8");
7208
+ } catch {
7209
+ continue;
7210
+ }
7211
+ const { name, description: description24, body } = parseFrontmatter(raw, skillFile);
7212
+ skills.push({ name, description: description24, body, sourcePath: skillFile });
7213
+ }
7214
+ return [...skills].sort((a, b) => a.name.localeCompare(b.name));
7215
+ }
7216
+
7217
+ // src/tools/skills/skill-corpus.ts
7218
+ var LIST_TOOL_NAME = "vo_skill_list";
7219
+ var GET_TOOL_NAME = "vo_skill_get";
7220
+ 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.";
7221
+ 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.";
7222
+ var listInputSchema = {
7223
+ type: "object",
7224
+ properties: {
7225
+ refresh: {
7226
+ type: "boolean",
7227
+ description: "Re-scan the skills directory instead of using the cached corpus."
7228
+ }
7229
+ },
7230
+ required: []
7231
+ };
7232
+ var getInputSchema = {
7233
+ type: "object",
7234
+ properties: {
7235
+ name: {
7236
+ type: "string",
7237
+ description: "Skill name exactly as returned by vo_skill_list."
7238
+ }
7239
+ },
7240
+ required: ["name"]
7241
+ };
7242
+ var MAX_WALK_UP_LEVELS = 8;
7243
+ var cachedCorpus = null;
7244
+ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
7245
+ const override = env.VO_SKILLS_DIR;
7246
+ if (typeof override === "string" && override.length > 0) {
7247
+ const abs = isAbsolute(override) ? override : resolve2(startDir, override);
7248
+ return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
7249
+ }
7250
+ let dir = resolve2(startDir);
7251
+ for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
7252
+ const candidate = join13(dir, ".claude", "skills");
7253
+ if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
7254
+ const parent = dirname5(dir);
7255
+ if (parent === dir) break;
7256
+ dir = parent;
7257
+ }
7258
+ return null;
7259
+ }
7260
+ function loadCorpus() {
7261
+ const skillsDir = resolveSkillsDir();
7262
+ if (skillsDir === null) {
7263
+ return {
7264
+ skills: [],
7265
+ skillsDir: null,
7266
+ unavailableReason: "No skills directory found. Set VO_SKILLS_DIR or run inside a repo with .claude/skills."
7267
+ };
7268
+ }
7269
+ try {
7270
+ return { skills: loadSkillsFromDir(skillsDir), skillsDir, unavailableReason: null };
7271
+ } catch (err) {
7272
+ const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
7273
+ return { skills: [], skillsDir, unavailableReason: message };
7274
+ }
7275
+ }
7276
+ function getCorpus(refresh) {
7277
+ if (refresh || cachedCorpus === null) {
7278
+ cachedCorpus = loadCorpus();
7279
+ }
7280
+ return cachedCorpus;
7281
+ }
7282
+ async function handleSkillList(_deps, rawInput) {
7283
+ const input = rawInput ?? {};
7284
+ const refresh = input.refresh === true;
7285
+ const corpus = getCorpus(refresh);
7286
+ return jsonContent({
7287
+ corpus_available: corpus.unavailableReason === null,
7288
+ skills_dir: corpus.skillsDir,
7289
+ unavailable_reason: corpus.unavailableReason,
7290
+ skill_count: corpus.skills.length,
7291
+ skills: corpus.skills.map((s) => ({ name: s.name, description: s.description }))
7292
+ });
7293
+ }
7294
+ async function handleSkillGet(_deps, rawInput) {
7295
+ const input = rawInput ?? {};
7296
+ if (typeof input.name !== "string" || input.name.trim().length === 0) {
7297
+ throw invalidParams(GET_TOOL_NAME, 'input field "name" (non-empty string) is required');
7298
+ }
7299
+ const requested = input.name.trim();
7300
+ const corpus = getCorpus(false);
7301
+ if (corpus.unavailableReason !== null) {
7302
+ return jsonContent({
7303
+ corpus_available: false,
7304
+ unavailable_reason: corpus.unavailableReason,
7305
+ skill: null
7306
+ });
7307
+ }
7308
+ const skill = corpus.skills.find((s) => s.name === requested);
7309
+ if (skill === void 0) {
7310
+ throw invalidParams(
7311
+ GET_TOOL_NAME,
7312
+ `unknown skill "${requested}". Known skills: ${corpus.skills.map((s) => s.name).join(", ")}`
7313
+ );
7314
+ }
7315
+ return jsonContent({
7316
+ corpus_available: true,
7317
+ skill: {
7318
+ name: skill.name,
7319
+ description: skill.description,
7320
+ instructions: skill.body,
7321
+ source_path: skill.sourcePath
7322
+ }
7323
+ });
7324
+ }
7325
+
5252
7326
  // src/server.ts
5253
7327
  function buildToolRegistry() {
5254
7328
  return {
@@ -5402,7 +7476,7 @@ function buildToolRegistry() {
5402
7476
  description: description19,
5403
7477
  inputSchema: inputSchema19
5404
7478
  },
5405
- handler: handleReportSessionState
7479
+ handler: handlePreparedJobMode
5406
7480
  },
5407
7481
  [TOOL_NAME20]: {
5408
7482
  definition: {
@@ -5410,7 +7484,7 @@ function buildToolRegistry() {
5410
7484
  description: description20,
5411
7485
  inputSchema: inputSchema20
5412
7486
  },
5413
- handler: handleSpawnSuccessor
7487
+ handler: handleReportSessionState
5414
7488
  },
5415
7489
  [TOOL_NAME21]: {
5416
7490
  definition: {
@@ -5418,7 +7492,7 @@ function buildToolRegistry() {
5418
7492
  description: description21,
5419
7493
  inputSchema: inputSchema21
5420
7494
  },
5421
- handler: handleConciergeDispatch
7495
+ handler: handleSpawnSuccessor
5422
7496
  },
5423
7497
  [TOOL_NAME22]: {
5424
7498
  definition: {
@@ -5426,6 +7500,14 @@ function buildToolRegistry() {
5426
7500
  description: description22,
5427
7501
  inputSchema: inputSchema22
5428
7502
  },
7503
+ handler: handleConciergeDispatch
7504
+ },
7505
+ [TOOL_NAME23]: {
7506
+ definition: {
7507
+ name: TOOL_NAME23,
7508
+ description: description23,
7509
+ inputSchema: inputSchema23
7510
+ },
5429
7511
  handler: handleSyncConfig
5430
7512
  },
5431
7513
  [UPSERT_TOOL_NAME]: {
@@ -5443,11 +7525,59 @@ function buildToolRegistry() {
5443
7525
  inputSchema: contextInputSchema
5444
7526
  },
5445
7527
  handler: handlePrivateKnowledgeContext
7528
+ },
7529
+ [INVALIDATE_TOOL_NAME]: {
7530
+ definition: {
7531
+ name: INVALIDATE_TOOL_NAME,
7532
+ description: invalidateDescription,
7533
+ inputSchema: invalidateInputSchema
7534
+ },
7535
+ handler: handlePrivateKnowledgeInvalidate
7536
+ },
7537
+ [STALE_TOOL_NAME]: {
7538
+ definition: {
7539
+ name: STALE_TOOL_NAME,
7540
+ description: staleDescription,
7541
+ inputSchema: staleInputSchema
7542
+ },
7543
+ handler: handlePrivateKnowledgeStale
7544
+ },
7545
+ [POST_TOOL_NAME]: {
7546
+ definition: {
7547
+ name: POST_TOOL_NAME,
7548
+ description: postDescription,
7549
+ inputSchema: postInputSchema
7550
+ },
7551
+ handler: handleHqWhiteboardPost
7552
+ },
7553
+ [READ_TOOL_NAME]: {
7554
+ definition: {
7555
+ name: READ_TOOL_NAME,
7556
+ description: readDescription,
7557
+ inputSchema: readInputSchema
7558
+ },
7559
+ handler: handleHqWhiteboardRead
7560
+ },
7561
+ [LIST_TOOL_NAME]: {
7562
+ definition: {
7563
+ name: LIST_TOOL_NAME,
7564
+ description: listDescription,
7565
+ inputSchema: listInputSchema
7566
+ },
7567
+ handler: handleSkillList
7568
+ },
7569
+ [GET_TOOL_NAME]: {
7570
+ definition: {
7571
+ name: GET_TOOL_NAME,
7572
+ description: getDescription,
7573
+ inputSchema: getInputSchema
7574
+ },
7575
+ handler: handleSkillGet
5446
7576
  }
5447
7577
  };
5448
7578
  }
5449
7579
  function createServer(options) {
5450
- const sessionId = options.sessionId ?? randomUUID2();
7580
+ const sessionId = options.sessionId ?? randomUUID3();
5451
7581
  const mode = createLocalMode();
5452
7582
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
5453
7583
  const server = new Server(
@@ -5499,9 +7629,9 @@ function listToolNames() {
5499
7629
  }
5500
7630
 
5501
7631
  // src/cache/sqlite-cache.ts
5502
- import { createHash as createHash3 } from "node:crypto";
5503
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync5 } from "node:fs";
5504
- import { dirname as dirname5 } from "node:path";
7632
+ import { createHash as createHash4 } from "node:crypto";
7633
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
7634
+ import { dirname as dirname6 } from "node:path";
5505
7635
  import { DatabaseSync } from "node:sqlite";
5506
7636
 
5507
7637
  // src/cache/canonicalize.ts
@@ -5546,7 +7676,7 @@ function normalizeString(s) {
5546
7676
  function createSqliteCache(options) {
5547
7677
  const fileBacked = options.dbPath !== ":memory:";
5548
7678
  if (fileBacked) {
5549
- mkdirSync5(dirname5(options.dbPath), { recursive: true, mode: 448 });
7679
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
5550
7680
  }
5551
7681
  const versionNamespace = options.cacheVersionNamespace ?? "";
5552
7682
  const db = new DatabaseSync(options.dbPath);
@@ -5577,7 +7707,7 @@ function createSqliteCache(options) {
5577
7707
  return {
5578
7708
  keyFor(toolName, input, opts) {
5579
7709
  const canonical = canonicalize(input, opts);
5580
- const hash = createHash3("sha256");
7710
+ const hash = createHash4("sha256");
5581
7711
  if (versionNamespace.length > 0) {
5582
7712
  hash.update(versionNamespace);
5583
7713
  hash.update("|");
@@ -5669,7 +7799,7 @@ function createStubRatchetClient() {
5669
7799
  let m;
5670
7800
  while ((m = pat.regex.exec(req.source)) !== null) {
5671
7801
  findings.push({
5672
- line_excerpt: clip(m[0], 80),
7802
+ line_excerpt: clip2(m[0], 80),
5673
7803
  severity: pat.severity,
5674
7804
  code: pat.code,
5675
7805
  message: pat.message
@@ -5701,7 +7831,7 @@ function createStubRatchetClient() {
5701
7831
  }
5702
7832
  };
5703
7833
  }
5704
- function clip(s, n) {
7834
+ function clip2(s, n) {
5705
7835
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
5706
7836
  }
5707
7837
  function buildSummary2(args) {
@@ -5730,66 +7860,62 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
5730
7860
  }
5731
7861
 
5732
7862
  // src/consensus/engine-client.ts
5733
- import { randomUUID as randomUUID3 } from "node:crypto";
7863
+ import { randomUUID as randomUUID4 } from "node:crypto";
5734
7864
 
5735
7865
  // src/consensus/meta-model-caller.ts
5736
- var META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
5737
7866
  var META_CONSENSUS_MODEL = "muse-spark-1.1";
5738
- var META_MODEL_API_KEY_ENV = "MODEL_API_KEY";
5739
- var META_MODEL_API_KEY_ALIAS = "META_API";
5740
- function resolveMetaKey(env) {
5741
- return String(env[META_MODEL_API_KEY_ENV] || env[META_MODEL_API_KEY_ALIAS] || "").trim();
5742
- }
5743
- function positiveMaxTokens(value) {
5744
- const parsed = Math.floor(Number(value));
5745
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 2048;
5746
- }
5747
7867
  function createMetaModelCaller(options = {}) {
5748
- const fetchImpl = options.fetchImpl ?? fetch;
5749
- const envSource = options.envSource ?? process.env;
5750
- const reasoningEffort = options.reasoningEffort ?? "high";
5751
- return async function callMetaWithMetrics2(prompt, systemPrompt, model, maxTokens, _privacyOptions, signal) {
5752
- const key = resolveMetaKey(envSource);
5753
- if (!key) throw new Error(`Missing ${META_MODEL_API_KEY_ENV} for Meta Model API`);
5754
- const messages = [
5755
- ...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
5756
- { role: "user", content: prompt }
5757
- ];
5758
- const response = await fetchImpl(`${META_MODEL_API_BASE_URL}/chat/completions`, {
5759
- method: "POST",
5760
- headers: {
5761
- Authorization: `Bearer ${key}`,
5762
- "Content-Type": "application/json"
5763
- },
5764
- body: JSON.stringify({
5765
- model: model || META_CONSENSUS_MODEL,
5766
- messages,
5767
- max_tokens: positiveMaxTokens(maxTokens),
5768
- reasoning_effort: reasoningEffort
5769
- }),
5770
- signal
5771
- });
5772
- const payload = await response.json();
5773
- if (!response.ok) {
5774
- const message = String(payload.error?.message || response.statusText || "request failed").slice(0, 500);
5775
- throw Object.assign(new Error(`Meta Model API ${response.status}: ${message}`), { status: response.status });
5776
- }
5777
- const content = payload.choices?.[0]?.message?.content;
5778
- if (typeof content !== "string" || !content.trim()) {
5779
- throw new Error(`Meta Model API returned no text (finish=${payload.choices?.[0]?.finish_reason || "unknown"})`);
5780
- }
5781
- const inputTokens = Number(payload.usage?.prompt_tokens || 0);
5782
- const outputTokens = Number(payload.usage?.completion_tokens || 0);
5783
- return {
5784
- content,
5785
- inputTokens,
5786
- outputTokens,
5787
- totalTokens: Number(payload.usage?.total_tokens || inputTokens + outputTokens)
5788
- };
7868
+ void options;
7869
+ return async function callMetaWithMetrics2() {
7870
+ throw new Error(
7871
+ "Muse Spark direct consensus is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
7872
+ );
5789
7873
  };
5790
7874
  }
5791
7875
  var callMetaWithMetrics = createMetaModelCaller();
5792
7876
 
7877
+ // src/consensus/consensus-panel.ts
7878
+ var VO_MCP_CONSENSUS_PANEL = {
7879
+ // claude-opus-5 (2026-07-24). Opus 4.7 was STRICTLY DOMINATED, not merely old:
7880
+ // Opus 5 is $5/$25 per MTok vs Opus 4.7's $15/$75 — a 3x cost cut on this slot,
7881
+ // corroborated by our own catalog (constants/pricing/sciencePricing.ts prices
7882
+ // claude-opus-5 at 0.010 vs claude-opus-4-7 at 0.030) — AND the same 2026-07-24
7883
+ // release note REMOVED fast mode from Opus 4.7 outright: `speed: "fast"` now
7884
+ // returns an error there rather than degrading, unlike the Opus 4.6 removal.
7885
+ // Verified served: GET /v1/models/claude-opus-5 -> HTTP 200 (2026-07-30).
7886
+ //
7887
+ // Claude-5 API safety checked before this swap: Opus 5 rejects `temperature` /
7888
+ // `top_p` / `top_k` and manual `thinking.budget_tokens` with HTTP 400. Neither
7889
+ // the consensus-engine Anthropic adapter nor functions-shared `callAnthropic`
7890
+ // sends any of them, and buildAdaptiveThinking emits `thinking: {type:'adaptive'}`
7891
+ // (the supported form) — so this swap cannot 400.
7892
+ anthropic: "claude-opus-5",
7893
+ // gpt-5.6-terra (GA 2026-07-09; −20% price cut 2026-07-30). NOTE the real IDs
7894
+ // are tiered — `gpt-5.6-sol` / `-terra` / `-luna`; there is NO bare `gpt-5.6`
7895
+ // alias (verified against the served model list, 2026-07-30). Terra is the
7896
+ // cost/capability balance point and the right default for a judgment panel;
7897
+ // Sol is available if verdict quality ever needs it.
7898
+ openai: "gpt-5.6-terra",
7899
+ // gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
7900
+ // callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
7901
+ // Flash is also ~10x cheaper. 2026-06-02.
7902
+ google: "gemini-2.5-flash",
7903
+ deepseek: "deepseek-chat",
7904
+ // Muse Spark identity is owned by meta-model-caller.ts (single source of
7905
+ // truth for the meta slot); re-exported here so the panel stays complete.
7906
+ meta: META_CONSENSUS_MODEL
7907
+ };
7908
+ function getVoMcpConsensusPanel(panel = VO_MCP_CONSENSUS_PANEL) {
7909
+ for (const [provider, modelId] of Object.entries(panel)) {
7910
+ if (typeof modelId !== "string" || modelId.trim().length === 0) {
7911
+ throw new Error(
7912
+ `getVoMcpConsensusPanel: panel slot "${provider}" has a missing or blank model ID`
7913
+ );
7914
+ }
7915
+ }
7916
+ return panel;
7917
+ }
7918
+
5793
7919
  // src/consensus/engine-options.ts
5794
7920
  var AGREEMENT_GATE_ENV_VAR = "VO_CONSENSUS_AGREEMENT_GATE";
5795
7921
  function isTruthyFlag(raw) {
@@ -5843,6 +7969,14 @@ function shadowEnabled(env) {
5843
7969
  const norm = raw.trim().toLowerCase();
5844
7970
  return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
5845
7971
  }
7972
+ var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
7973
+ function resolveMinResponders(env) {
7974
+ const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
7975
+ if (raw === void 0 || raw.trim() === "") return 2;
7976
+ const parsed = Number.parseInt(raw.trim(), 10);
7977
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
7978
+ return parsed;
7979
+ }
5846
7980
  function mapShadowSynthesis(s) {
5847
7981
  if (s === void 0) return void 0;
5848
7982
  return {
@@ -5992,9 +8126,11 @@ function createEngineConsensusClient(options) {
5992
8126
  ...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
5993
8127
  ...options.env !== void 0 ? { env: options.env } : {}
5994
8128
  });
8129
+ const minResponders = resolveMinResponders(options.env);
5995
8130
  const engineOptions = {
5996
8131
  panel,
5997
8132
  ...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
8133
+ ...minResponders !== void 0 ? { min_responders: minResponders } : {},
5998
8134
  ...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
5999
8135
  // Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
6000
8136
  // Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
@@ -6003,7 +8139,7 @@ function createEngineConsensusClient(options) {
6003
8139
  shadow_synthesis: { enabled: shadowEnabled(options.env) }
6004
8140
  };
6005
8141
  const sources = request.source_urls;
6006
- const useSourceGrounded = sources !== void 0 && sources.length > 0 && typeof engine.runSourceGroundedConsensus === "function";
8142
+ const useSourceGrounded = sources !== void 0 && sources.length > 0;
6007
8143
  let response;
6008
8144
  let sourceExtras;
6009
8145
  if (useSourceGrounded) {
@@ -6047,8 +8183,13 @@ function createEngineConsensusClient(options) {
6047
8183
  synthesized_verdict: response.synthesized_verdict,
6048
8184
  per_model_verdicts: response.per_model_verdicts,
6049
8185
  degraded: response.degraded,
8186
+ ...response.quorum_failed === true ? { quorum_failed: true } : {},
6050
8187
  duration_ms: response.duration_ms,
6051
8188
  engine_version: response.engine_version,
8189
+ // Cumulative cross-round inference usage (B44-3). Absent when no panel
8190
+ // member reported usage; forwarded verbatim — the aggregator prefers it
8191
+ // over summing final-round verdicts (which under-reports deliberation).
8192
+ ...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
6052
8193
  // Phase 2 Lane D-1 — forward escalation signal when present. The
6053
8194
  // source-grounded layer's own escalation (from the citation grade)
6054
8195
  // takes precedence when set, else the synthesizer's.
@@ -6058,6 +8199,10 @@ function createEngineConsensusClient(options) {
6058
8199
  ...mapFanOutDiagnostics(response.fan_out_diagnostics) !== void 0 ? { fan_out_diagnostics: mapFanOutDiagnostics(response.fan_out_diagnostics) } : {},
6059
8200
  // Stage A7-shadow — adaptive-vs-incumbent comparison (PII-free; present iff shadow on).
6060
8201
  ...mapShadowSynthesis(response.shadow_synthesis) !== void 0 ? { shadow_synthesis: mapShadowSynthesis(response.shadow_synthesis) } : {},
8202
+ // Critique-uptake (2026-07-20 red-team fix) — verifier-critique
8203
+ // visibility report; previously computed by the engine on every
8204
+ // call but dropped at this boundary.
8205
+ ...response.critique_uptake !== void 0 ? { critique_uptake: response.critique_uptake } : {},
6061
8206
  // Source-grounded additive outputs (Tier-4 features).
6062
8207
  ...useSourceGrounded ? { source_grounded: true } : {},
6063
8208
  ...sourceExtras?.citation_grade !== void 0 ? { citation_grade: sourceExtras.citation_grade } : {},
@@ -6073,27 +8218,12 @@ function createEngineConsensusClient(options) {
6073
8218
  }
6074
8219
  };
6075
8220
  }
6076
- var DEFAULT_MODELS = {
6077
- // These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
6078
- // intent — current production model ids. Per handoff §C-3 these MUST come
6079
- // from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
6080
- // for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
6081
- anthropic: "claude-opus-4-7",
6082
- openai: "gpt-5",
6083
- // gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
6084
- // callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
6085
- // Flash is also ~10x cheaper. 2026-06-02.
6086
- google: "gemini-2.5-flash",
6087
- deepseek: "deepseek-chat",
6088
- meta: META_CONSENSUS_MODEL
6089
- };
8221
+ var DEFAULT_MODELS = getVoMcpConsensusPanel();
6090
8222
  function probeProviders(env = process.env) {
6091
8223
  const out = [];
6092
8224
  if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
6093
8225
  if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
6094
8226
  if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
6095
- if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
6096
- if ((env[META_MODEL_API_KEY_ENV] ?? "").trim().length > 0 || (env[META_MODEL_API_KEY_ALIAS] ?? "").trim().length > 0) out.push("meta");
6097
8227
  return out;
6098
8228
  }
6099
8229
  async function loadFactoryAndCallers(injectedEngine, injectedShared) {
@@ -6138,25 +8268,20 @@ async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
6138
8268
  const callerByProvider = {
6139
8269
  anthropic: loaded.shared.callAnthropicWithMetrics,
6140
8270
  openai: loaded.shared.callOpenAIWithMetrics,
6141
- google: loaded.shared.callGeminiWithMetrics,
6142
- deepseek: loaded.shared.callDeepSeekWithMetrics,
6143
- meta: options.metaCaller ?? callMetaWithMetrics
8271
+ google: loaded.shared.callGeminiWithMetrics
6144
8272
  };
6145
8273
  const modelByProvider = {
6146
8274
  anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
6147
8275
  openai: options.models?.openai ?? DEFAULT_MODELS.openai,
6148
- google: options.models?.google ?? DEFAULT_MODELS.google,
6149
- deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek,
6150
- meta: options.models?.meta ?? DEFAULT_MODELS.meta
8276
+ google: options.models?.google ?? DEFAULT_MODELS.google
6151
8277
  };
6152
- 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;
6153
8278
  const panel = [];
6154
8279
  for (const p of providers) {
6155
8280
  try {
6156
8281
  const adapter = loaded.engine.createAdapter(p, {
6157
8282
  model: modelByProvider[p],
6158
8283
  caller: callerByProvider[p],
6159
- envSource: adapterEnv
8284
+ envSource: env
6160
8285
  });
6161
8286
  panel.push(adapter);
6162
8287
  } catch {