@algosuite/vo-mcp 0.2.0-beta.4 → 0.2.0-beta.42

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 existsSync7, 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 description23 = frontmatter[1].match(/^description:\s*(.+)$/m);
510
+ if (description23 && description23[1].trim()) return description23[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 (!existsSync7(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,
@@ -1216,6 +1546,42 @@ function toEventPerModelVerdicts(src) {
1216
1546
  };
1217
1547
  });
1218
1548
  }
1549
+ function aggregateEventTokenUsage(src, engineUsage) {
1550
+ if (engineUsage !== void 0) {
1551
+ const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
1552
+ const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
1553
+ return {
1554
+ per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
1555
+ per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
1556
+ total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
1557
+ };
1558
+ }
1559
+ const tokensIn = {};
1560
+ const tokensOut = {};
1561
+ let anyTokensIn = false;
1562
+ let anyTokensOut = false;
1563
+ let costMicroUsd = 0;
1564
+ let anyCost = false;
1565
+ for (const v of src) {
1566
+ if (typeof v.input_tokens === "number") {
1567
+ tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
1568
+ anyTokensIn = true;
1569
+ }
1570
+ if (typeof v.output_tokens === "number") {
1571
+ tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
1572
+ anyTokensOut = true;
1573
+ }
1574
+ if (typeof v.cost_micro_usd === "number") {
1575
+ costMicroUsd += v.cost_micro_usd;
1576
+ anyCost = true;
1577
+ }
1578
+ }
1579
+ return {
1580
+ per_model_tokens_in: anyTokensIn ? tokensIn : null,
1581
+ per_model_tokens_out: anyTokensOut ? tokensOut : null,
1582
+ total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
1583
+ };
1584
+ }
1219
1585
  function toEventSynthesizedVerdict(src) {
1220
1586
  return {
1221
1587
  verdict: src.verdict,
@@ -1587,7 +1953,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
1587
1953
  synthesized_verdict: synthForEvent,
1588
1954
  consensus_confidence: engineResult.synthesized_verdict.confidence,
1589
1955
  duration_ms: engineResult.duration_ms,
1590
- consensus_engine_version: engineResult.engine_version
1956
+ consensus_engine_version: engineResult.engine_version,
1957
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
1591
1958
  };
1592
1959
  const payload = {
1593
1960
  verdict: engineResult.synthesized_verdict.verdict,
@@ -1769,7 +2136,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
1769
2136
  synthesized_verdict: synthForEvent,
1770
2137
  consensus_confidence: engineResult.synthesized_verdict.confidence,
1771
2138
  duration_ms: engineResult.duration_ms,
1772
- consensus_engine_version: engineResult.engine_version
2139
+ consensus_engine_version: engineResult.engine_version,
2140
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
1773
2141
  };
1774
2142
  const payload = {
1775
2143
  verdict: engineResult.synthesized_verdict.verdict,
@@ -1778,6 +2146,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
1778
2146
  synthesized_verdict: synthForEvent,
1779
2147
  engine_version: engineResult.engine_version,
1780
2148
  degraded: engineResult.degraded,
2149
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
2150
+ // The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
2151
+ ...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
1781
2152
  gate_type: gateType,
1782
2153
  ...kbResult.error !== null ? { kb_unavailable: true } : {},
1783
2154
  ...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
@@ -2031,7 +2402,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2031
2402
  duration_ms: engineResult.duration_ms,
2032
2403
  consensus_engine_version: engineResult.engine_version,
2033
2404
  per_model_verdicts: perModelForEvent,
2034
- synthesized_verdict: synthForEvent
2405
+ synthesized_verdict: synthForEvent,
2406
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
2035
2407
  };
2036
2408
  const payload = {
2037
2409
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2040,6 +2412,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2040
2412
  synthesized_verdict: synthForEvent,
2041
2413
  engine_version: engineResult.engine_version,
2042
2414
  degraded: engineResult.degraded,
2415
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
2416
+ // The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
2417
+ // NOTE: a content-hash cache hit replays the ORIGINAL call's receipt_id (same claim, same verdict, no new spend) —
2418
+ // a receipt asserts the stage ran for this claim, not one-receipt-per-call.
2419
+ ...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
2043
2420
  gate_type: gateType,
2044
2421
  // ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
2045
2422
  // Feature 2 (calibrated-confidence) — ON by default; the engine attaches
@@ -2058,7 +2435,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2058
2435
  ...engineResult.low_confidence_sources !== void 0 ? { low_confidence_sources: engineResult.low_confidence_sources } : {},
2059
2436
  // Escalation (from citation grade or human-tiebreak synthesizer).
2060
2437
  ...engineResult.escalation_required !== void 0 ? { escalation_required: engineResult.escalation_required } : {},
2061
- ...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {}
2438
+ ...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {},
2439
+ // Critique-uptake (2026-07-20 red-team fix) — the engine computes this
2440
+ // on every call; this spread closes the gap where the visibility report
2441
+ // was itself silently dropped at the payload boundary.
2442
+ ...engineResult.critique_uptake !== void 0 ? { critique_uptake: engineResult.critique_uptake } : {}
2062
2443
  };
2063
2444
  const envelope = {
2064
2445
  tool: TOOL_NAME4,
@@ -2234,7 +2615,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
2234
2615
  synthesized_verdict: synthForEvent,
2235
2616
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2236
2617
  duration_ms: engineResult.duration_ms,
2237
- consensus_engine_version: engineResult.engine_version
2618
+ consensus_engine_version: engineResult.engine_version,
2619
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
2238
2620
  };
2239
2621
  const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
2240
2622
  const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
@@ -3719,7 +4101,8 @@ Produce the JSON dispatch plan now.`;
3719
4101
  duration_ms: engineResult.duration_ms,
3720
4102
  consensus_engine_version: engineResult.engine_version,
3721
4103
  per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
3722
- synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
4104
+ synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
4105
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
3723
4106
  };
3724
4107
  deps.events.append(enrichedEvent);
3725
4108
  return jsonContent(envelope);
@@ -3813,7 +4196,7 @@ async function buildCloudOrStubResponse(args) {
3813
4196
  }
3814
4197
 
3815
4198
  // 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")';
4199
+ 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
4200
  var HEAL_GATE_TYPE = "admin-action";
3818
4201
 
3819
4202
  // src/tools/heal/trigger-heal.ts
@@ -3834,7 +4217,7 @@ var inputSchema8 = {
3834
4217
  },
3835
4218
  additionalProperties: false
3836
4219
  };
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.";
4220
+ 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
4221
  function isToolInput8(v) {
3839
4222
  if (typeof v !== "object" || v === null) return false;
3840
4223
  const o = v;
@@ -3891,7 +4274,7 @@ var inputSchema9 = {
3891
4274
  },
3892
4275
  additionalProperties: false
3893
4276
  };
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.";
4277
+ 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
4278
  function isToolInput9(v) {
3896
4279
  if (typeof v !== "object" || v === null) return false;
3897
4280
  const o = v;
@@ -3978,7 +4361,7 @@ var inputSchema10 = {
3978
4361
  required: ["attempt_id"],
3979
4362
  additionalProperties: false
3980
4363
  };
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.";
4364
+ 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
4365
  function isToolInput10(v) {
3983
4366
  if (typeof v !== "object" || v === null) return false;
3984
4367
  const o = v;
@@ -4025,7 +4408,7 @@ var inputSchema11 = {
4025
4408
  required: ["run_id"],
4026
4409
  additionalProperties: false
4027
4410
  };
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.";
4411
+ 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
4412
  function isToolInput11(v) {
4030
4413
  if (typeof v !== "object" || v === null) return false;
4031
4414
  const o = v;
@@ -4070,7 +4453,7 @@ var inputSchema12 = {
4070
4453
  properties: {},
4071
4454
  additionalProperties: false
4072
4455
  };
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.";
4456
+ 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
4457
  function isToolInput12(v) {
4075
4458
  if (typeof v !== "object" || v === null) return false;
4076
4459
  return true;
@@ -4093,7 +4476,7 @@ async function handleGetWorkflowRuns(deps, rawInput, _signal) {
4093
4476
  }
4094
4477
 
4095
4478
  // 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")';
4479
+ 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
4480
  var PR_GATE_TYPE = "admin-action";
4098
4481
 
4099
4482
  // src/tools/pr/list-pending-prs.ts
@@ -4105,7 +4488,7 @@ var inputSchema13 = {
4105
4488
  properties: {},
4106
4489
  additionalProperties: false
4107
4490
  };
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.";
4491
+ 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
4492
  function isToolInput13(v) {
4110
4493
  return typeof v === "object" && v !== null;
4111
4494
  }
@@ -4141,7 +4524,7 @@ var inputSchema14 = {
4141
4524
  required: ["pr_number"],
4142
4525
  additionalProperties: false
4143
4526
  };
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.";
4527
+ 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
4528
  function isToolInput14(v) {
4146
4529
  if (typeof v !== "object" || v === null) return false;
4147
4530
  const o = v;
@@ -4185,7 +4568,7 @@ var inputSchema15 = {
4185
4568
  required: ["pr_number"],
4186
4569
  additionalProperties: false
4187
4570
  };
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.";
4571
+ 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
4572
  function isToolInput15(v) {
4190
4573
  if (typeof v !== "object" || v === null) return false;
4191
4574
  const o = v;
@@ -4223,7 +4606,7 @@ var inputSchema16 = {
4223
4606
  properties: {},
4224
4607
  additionalProperties: false
4225
4608
  };
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.";
4609
+ 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
4610
  function isToolInput16(v) {
4228
4611
  return typeof v === "object" && v !== null;
4229
4612
  }
@@ -4257,7 +4640,7 @@ var inputSchema17 = {
4257
4640
  required: ["pr_number"],
4258
4641
  additionalProperties: false
4259
4642
  };
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.";
4643
+ 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
4644
  function isToolInput17(v) {
4262
4645
  if (typeof v !== "object" || v === null) return false;
4263
4646
  const o = v;
@@ -4333,7 +4716,7 @@ function buildPrompt4(pr, notes) {
4333
4716
  const lines = [
4334
4717
  "You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
4335
4718
  "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.",
4719
+ "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
4720
  "",
4338
4721
  `PR #${pr.number}: ${pr.title}`,
4339
4722
  `Source: ${pr.source ?? "unknown"}`,
@@ -4401,7 +4784,7 @@ async function handleReviewMerge(deps, rawInput, signal) {
4401
4784
  }
4402
4785
  if (pr === null) {
4403
4786
  return emit(
4404
- emptyPayload("hold", `PR #${prNumber} is not among open VO PRs (already merged/closed, or not a VO-source PR).`, null)
4787
+ emptyPayload("hold", `PR #${prNumber} is not among open AlgoHQ PRs (already merged/closed, or not an AlgoHQ-source PR).`, null)
4405
4788
  );
4406
4789
  }
4407
4790
  const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
@@ -4461,7 +4844,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
4461
4844
  duration_ms: result.duration_ms,
4462
4845
  consensus_engine_version: result.engine_version,
4463
4846
  per_model_verdicts: perModel,
4464
- synthesized_verdict: synth
4847
+ synthesized_verdict: synth,
4848
+ ...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
4465
4849
  });
4466
4850
  }
4467
4851
 
@@ -4496,6 +4880,8 @@ function suggestedHandoffPath(session_id, isoTimestamp) {
4496
4880
  }
4497
4881
 
4498
4882
  // src/tools/session/report-session-state.ts
4883
+ init_auth_token_source();
4884
+ init_credential_store();
4499
4885
  var TOOL_NAME19 = "vo_report_session_state";
4500
4886
  var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
4501
4887
  var MAX_GOAL_CHARS = 500;
@@ -4546,7 +4932,7 @@ var inputSchema19 = {
4546
4932
  required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
4547
4933
  additionalProperties: false
4548
4934
  };
4549
- 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 + VO_CONTROL_PLANE_ADMIN_TOKEN + VO_TENANT_ID are set; 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).";
4935
+ var description19 = "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).";
4550
4936
  function isStringArray2(v, maxItems) {
4551
4937
  if (!Array.isArray(v)) return false;
4552
4938
  if (v.length > maxItems) return false;
@@ -4571,14 +4957,39 @@ function isToolInput19(v) {
4571
4957
  }
4572
4958
  return true;
4573
4959
  }
4574
- function getCloudConfig() {
4575
- const url = process.env["VO_CONTROL_PLANE_URL"];
4576
- const token = process.env["VO_CONTROL_PLANE_ADMIN_TOKEN"];
4577
- const tenant_id = process.env["VO_TENANT_ID"];
4578
- if (!url || !token || !tenant_id) return null;
4579
- return { url, token, tenant_id };
4960
+ async function fetchCloudIdentity(url, token, fetchFn) {
4961
+ try {
4962
+ const response = await fetchFn(`${url}/api/v1/auth/me`, {
4963
+ method: "GET",
4964
+ headers: {
4965
+ "Authorization": `Bearer ${token}`
4966
+ }
4967
+ });
4968
+ if (!response.ok) return null;
4969
+ const data = await response.json();
4970
+ if (!data.ok || !data.provisioned || !data.operator_id || !data.tenant_id) return null;
4971
+ return { operator_id: data.operator_id, tenant_id: data.tenant_id };
4972
+ } catch {
4973
+ return null;
4974
+ }
4580
4975
  }
4581
- async function tryCloudReportState(cloud, input) {
4976
+ async function getCloudConfig(fetchFn = fetch) {
4977
+ const url = process.env["VO_CONTROL_PLANE_URL"]?.trim();
4978
+ if (!url) return null;
4979
+ const tokenSource = createAuthTokenSourceFromEnv(
4980
+ process.env,
4981
+ fetchFn,
4982
+ () => readStoredCredential(process.env)
4983
+ );
4984
+ const token = await tokenSource?.getToken();
4985
+ if (!token) return null;
4986
+ const tenant_id = process.env["VO_TENANT_ID"]?.trim();
4987
+ if (tenant_id) return { url, token, tenant_id };
4988
+ const identity = await fetchCloudIdentity(url, token, fetchFn);
4989
+ if (!identity) return null;
4990
+ return { url, token, tenant_id: identity.tenant_id, operator_id: identity.operator_id };
4991
+ }
4992
+ async function tryCloudReportState(cloud, input, fetchFn = fetch) {
4582
4993
  try {
4583
4994
  const reportBody = {
4584
4995
  context_used_pct: input.context_used_pct
@@ -4591,7 +5002,7 @@ async function tryCloudReportState(cloud, input) {
4591
5002
  reportBody["recent_tool_uses"] = input.recent_tool_uses;
4592
5003
  }
4593
5004
  const reportUrl = `${cloud.url}/api/v1/session/${input.session_id}/report-state`;
4594
- let response = await fetch(reportUrl, {
5005
+ let response = await fetchFn(reportUrl, {
4595
5006
  method: "POST",
4596
5007
  headers: {
4597
5008
  "Content-Type": "application/json",
@@ -4601,7 +5012,7 @@ async function tryCloudReportState(cloud, input) {
4601
5012
  });
4602
5013
  if (response.status === 404) {
4603
5014
  const allocateBody = {
4604
- operator_id: input.operator_id,
5015
+ operator_id: cloud.operator_id ?? input.operator_id,
4605
5016
  tenant_id: cloud.tenant_id,
4606
5017
  agent_type: input.agent_type,
4607
5018
  current_goal: input.current_goal ?? "Interactive session"
@@ -4610,7 +5021,7 @@ async function tryCloudReportState(cloud, input) {
4610
5021
  allocateBody["initial_context_used_pct"] = input.context_used_pct;
4611
5022
  }
4612
5023
  const allocateUrl = `${cloud.url}/api/v1/session`;
4613
- const allocateResponse = await fetch(allocateUrl, {
5024
+ const allocateResponse = await fetchFn(allocateUrl, {
4614
5025
  method: "POST",
4615
5026
  headers: {
4616
5027
  "Content-Type": "application/json",
@@ -4621,7 +5032,10 @@ async function tryCloudReportState(cloud, input) {
4621
5032
  if (!allocateResponse.ok) {
4622
5033
  return null;
4623
5034
  }
4624
- response = await fetch(reportUrl, {
5035
+ const allocateData = await allocateResponse.json();
5036
+ const retrySessionId = typeof allocateData.session?.session_id === "string" && allocateData.session.session_id.length > 0 ? allocateData.session.session_id : input.session_id;
5037
+ const retryReportUrl = `${cloud.url}/api/v1/session/${retrySessionId}/report-state`;
5038
+ response = await fetchFn(retryReportUrl, {
4625
5039
  method: "POST",
4626
5040
  headers: {
4627
5041
  "Content-Type": "application/json",
@@ -4660,7 +5074,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
4660
5074
  `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}).`
4661
5075
  );
4662
5076
  }
4663
- const cloud = getCloudConfig();
5077
+ const cloud = await getCloudConfig();
4664
5078
  if (cloud !== null) {
4665
5079
  const cloudPayload = await tryCloudReportState(cloud, rawInput);
4666
5080
  if (cloudPayload !== null) {
@@ -4686,9 +5100,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
4686
5100
 
4687
5101
  // src/tools/session/spawn-successor.ts
4688
5102
  import { spawn } from "node:child_process";
5103
+ import { homedir as homedir5 } from "node:os";
5104
+ import { join as join7 } from "node:path";
5105
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5106
+
5107
+ // src/swarm/tier-binding.ts
5108
+ var SWARM_TIERS = Object.freeze([
5109
+ "tier1_subscription",
5110
+ "tier1_local",
5111
+ "tier2_user_key",
5112
+ "tier3_platform_key",
5113
+ "refused",
5114
+ "unresolved"
5115
+ ]);
5116
+ var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
5117
+ "tier1_subscription",
5118
+ "tier1_local",
5119
+ "tier2_user_key",
5120
+ "tier3_platform_key"
5121
+ ]);
5122
+ var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
5123
+ var MAX_BOUND_SUBAGENTS = 20;
5124
+ function isPositiveCap(cap) {
5125
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
5126
+ }
5127
+ function unresolvedBinding(swarmId, nowIso, reason) {
5128
+ return {
5129
+ schema_version: 1,
5130
+ swarm_id: swarmId,
5131
+ tier: "unresolved",
5132
+ agent: null,
5133
+ reason,
5134
+ exhausted_agents: [],
5135
+ subagent_budget: 0,
5136
+ spend_cap_usd: null,
5137
+ resolved_at: nowIso
5138
+ };
5139
+ }
5140
+ function serializeSwarmTierBinding(binding) {
5141
+ return JSON.stringify(binding);
5142
+ }
5143
+ function parseSwarmTierBinding(raw, nowIso) {
5144
+ if (typeof raw !== "string" || raw.trim().length === 0) {
5145
+ return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
5146
+ }
5147
+ let parsed;
5148
+ try {
5149
+ parsed = JSON.parse(raw);
5150
+ } catch {
5151
+ return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
5152
+ }
5153
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5154
+ return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
5155
+ }
5156
+ const o = parsed;
5157
+ const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
5158
+ if (o["schema_version"] !== 1) {
5159
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
5160
+ }
5161
+ const tier = o["tier"];
5162
+ if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
5163
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
5164
+ }
5165
+ const budget = o["subagent_budget"];
5166
+ const cap = o["spend_cap_usd"];
5167
+ const capNum = isPositiveCap(cap) ? cap : null;
5168
+ if (tier === "tier3_platform_key" && capNum === null) {
5169
+ return unresolvedBinding(
5170
+ swarmId,
5171
+ nowIso,
5172
+ "inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
5173
+ );
5174
+ }
5175
+ return {
5176
+ schema_version: 1,
5177
+ swarm_id: swarmId,
5178
+ tier,
5179
+ agent: typeof o["agent"] === "string" ? o["agent"] : null,
5180
+ reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
5181
+ exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
5182
+ subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
5183
+ spend_cap_usd: capNum,
5184
+ resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
5185
+ };
5186
+ }
5187
+ function inheritSwarmTierBinding(env, nowIso) {
5188
+ return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
5189
+ }
5190
+ function bindingEnvFragment(binding) {
5191
+ return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
5192
+ }
5193
+ function childBindingEnvFragment(binding, allocatedCapUsd = null) {
5194
+ return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
5195
+ }
5196
+ function admitSubagentSpawn(binding, spawnsSoFar = 0) {
5197
+ if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
5198
+ return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
5199
+ }
5200
+ if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
5201
+ return {
5202
+ allowed: false,
5203
+ reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
5204
+ };
5205
+ }
5206
+ if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
5207
+ return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
5208
+ }
5209
+ if (spawnsSoFar >= binding.subagent_budget) {
5210
+ return {
5211
+ allowed: false,
5212
+ reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
5213
+ };
5214
+ }
5215
+ return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
5216
+ }
5217
+ function childBinding(binding, allocatedCapUsd = null) {
5218
+ const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
5219
+ const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
5220
+ return {
5221
+ ...binding,
5222
+ subagent_budget: Math.max(0, binding.subagent_budget - 1),
5223
+ // A child never carries more than its parent, whatever the ledger says: a
5224
+ // forged or hand-edited pool cannot inflate a descendant above the binding
5225
+ // it descends from.
5226
+ spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
5227
+ };
5228
+ }
5229
+ function agentBindingRefusal(binding, requestedAgent) {
5230
+ const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
5231
+ if (requested.length === 0) return null;
5232
+ if (binding.agent !== null && requested === binding.agent) return null;
5233
+ 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)`;
5234
+ }
5235
+
5236
+ // src/swarm/successor-launch.ts
5237
+ var AGENT_LAUNCH_SHAPES = Object.freeze({
5238
+ claude: {
5239
+ bin: "claude",
5240
+ baseArgs: ["-p", "--permission-mode", "acceptEdits"],
5241
+ enforcesMaxTurns: true,
5242
+ maxTurnsFlag: "--max-turns",
5243
+ windowsShellSafe: true
5244
+ },
5245
+ codex: {
5246
+ bin: "codex",
5247
+ baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
5248
+ enforcesMaxTurns: false,
5249
+ // `-` makes codex read the prompt from stdin (injection-safe), matching how
5250
+ // codex-runner.mjs already spawns it.
5251
+ trailingArgs: ["-"],
5252
+ // `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
5253
+ // unverified, so win32 refuses rather than risking a mangled sandbox flag.
5254
+ windowsShellSafe: false
5255
+ }
5256
+ });
5257
+ function resolveSuccessorLaunch(input) {
5258
+ const agent = typeof input.agent === "string" ? input.agent.trim() : "";
5259
+ if (!agent) {
5260
+ return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
5261
+ }
5262
+ const shape = AGENT_LAUNCH_SHAPES[agent];
5263
+ if (!shape) {
5264
+ const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
5265
+ return {
5266
+ ok: false,
5267
+ reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
5268
+ };
5269
+ }
5270
+ const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
5271
+ if (wantsMaxTurns && !shape.enforcesMaxTurns) {
5272
+ return {
5273
+ ok: false,
5274
+ reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
5275
+ };
5276
+ }
5277
+ const platform = input.platform ?? process.platform;
5278
+ if (platform === "win32" && !shape.windowsShellSafe) {
5279
+ return {
5280
+ ok: false,
5281
+ 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`
5282
+ };
5283
+ }
5284
+ const args = [...shape.baseArgs];
5285
+ if (wantsMaxTurns && shape.maxTurnsFlag) {
5286
+ args.push(shape.maxTurnsFlag, String(input.maxTurns));
5287
+ }
5288
+ if (shape.trailingArgs) args.push(...shape.trailingArgs);
5289
+ return { ok: true, agent, bin: shape.bin, args };
5290
+ }
5291
+
5292
+ // src/swarm/spawn-ledger.ts
5293
+ import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
4689
5294
  import { homedir as homedir4 } from "node:os";
4690
5295
  import { join as join6 } from "node:path";
4691
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5296
+ var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
5297
+ function resolveLedgerDir(env) {
5298
+ const override = env[SWARM_LEDGER_DIR_ENV];
5299
+ if (typeof override === "string" && override.trim().length > 0) return override.trim();
5300
+ return join6(homedir4(), ".vo", "swarm-ledger");
5301
+ }
5302
+ function sanitizeSwarmId(raw) {
5303
+ if (typeof raw !== "string") return null;
5304
+ const id = raw.trim();
5305
+ if (id.length === 0 || id.length > 128) return null;
5306
+ if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
5307
+ if (id === "." || id === "..") return null;
5308
+ return id;
5309
+ }
5310
+ var CEILING_FILE = "ceiling.json";
5311
+ function createExclusive(path3, contents) {
5312
+ let fd;
5313
+ try {
5314
+ fd = openSync(path3, "wx");
5315
+ } catch {
5316
+ return false;
5317
+ }
5318
+ try {
5319
+ writeFileSync3(fd, contents, "utf8");
5320
+ } finally {
5321
+ closeSync(fd);
5322
+ }
5323
+ return true;
5324
+ }
5325
+ function capToCents(cap) {
5326
+ return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
5327
+ }
5328
+ function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
5329
+ const path3 = join6(swarmDir, CEILING_FILE);
5330
+ const head = JSON.stringify({
5331
+ ceiling: proposedCeiling,
5332
+ cap_cents: proposedCapCents,
5333
+ recorded_at: nowIso
5334
+ });
5335
+ if (createExclusive(path3, head)) {
5336
+ return { ceiling: proposedCeiling, capCents: proposedCapCents };
5337
+ }
5338
+ let parsed;
5339
+ try {
5340
+ parsed = JSON.parse(readFileSync6(path3, "utf8"));
5341
+ } catch {
5342
+ return null;
5343
+ }
5344
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
5345
+ const record = parsed;
5346
+ const recorded = record["ceiling"];
5347
+ if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
5348
+ const recordedCap = record["cap_cents"];
5349
+ const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
5350
+ return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
5351
+ }
5352
+ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
5353
+ const id = sanitizeSwarmId(swarmId);
5354
+ if (id === null) {
5355
+ return {
5356
+ ok: false,
5357
+ 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`
5358
+ };
5359
+ }
5360
+ const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
5361
+ if (proposed < 1) {
5362
+ return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
5363
+ }
5364
+ const swarmDir = join6(dir, id);
5365
+ try {
5366
+ mkdirSync3(swarmDir, { recursive: true });
5367
+ } catch (err) {
5368
+ return {
5369
+ ok: false,
5370
+ reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
5371
+ };
5372
+ }
5373
+ const wantedCents = capToCents(proposedCapUsd);
5374
+ const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
5375
+ if (head === null) {
5376
+ return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
5377
+ }
5378
+ const { ceiling, capCents } = head;
5379
+ const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
5380
+ if (wantedCents > 0 && shareCents < 1) {
5381
+ return {
5382
+ ok: false,
5383
+ 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`
5384
+ };
5385
+ }
5386
+ for (let slot = 0; slot < ceiling; slot++) {
5387
+ const debitedCents = shareCents;
5388
+ const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
5389
+ const claimed = createExclusive(
5390
+ join6(swarmDir, `slot-${slot}.json`),
5391
+ JSON.stringify({
5392
+ slot,
5393
+ ceiling,
5394
+ pid: process.pid,
5395
+ claimed_at: nowIso,
5396
+ // The debit record. Durable and atomic with the claim: this file is
5397
+ // created with O_EXCL, so exactly one claimant ever writes this line.
5398
+ cap_cents_pool: capCents,
5399
+ cap_cents_debited: debitedCents,
5400
+ cap_cents_remaining: remainingCents
5401
+ })
5402
+ );
5403
+ if (claimed) {
5404
+ return {
5405
+ ok: true,
5406
+ slot,
5407
+ ceiling,
5408
+ remaining: ceiling - slot - 1,
5409
+ capUsd: debitedCents > 0 ? debitedCents / 100 : null,
5410
+ capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
5411
+ };
5412
+ }
5413
+ }
5414
+ return {
5415
+ ok: false,
5416
+ reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
5417
+ };
5418
+ };
5419
+
5420
+ // src/tools/session/spawn-successor.ts
4692
5421
  var TOOL_NAME20 = "vo_spawn_successor";
4693
5422
  var MAX_HANDOFF_BYTES = 64e3;
4694
5423
  var inputSchema20 = {
@@ -4709,11 +5438,16 @@ var inputSchema20 = {
4709
5438
  max_turns: {
4710
5439
  type: "number",
4711
5440
  description: "Optional --max-turns bound for the successor."
5441
+ },
5442
+ agent: {
5443
+ type: "string",
5444
+ 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.`
4712
5445
  }
4713
5446
  },
4714
5447
  required: [],
4715
5448
  additionalProperties: false
4716
5449
  };
5450
+ var RETIRED_COUNTER_INPUT = "spawns_so_far";
4717
5451
  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.";
4718
5452
  function isToolInput20(v) {
4719
5453
  if (typeof v !== "object" || v === null) return false;
@@ -4722,12 +5456,18 @@ function isToolInput20(v) {
4722
5456
  if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
4723
5457
  if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
4724
5458
  if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
5459
+ if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
4725
5460
  return true;
4726
5461
  }
4727
- function newestHandoff(dir = join6(homedir4(), ".vo", "handoffs")) {
5462
+ function retiredCounterRefusal(v) {
5463
+ if (typeof v !== "object" || v === null) return null;
5464
+ if (!(RETIRED_COUNTER_INPUT in v)) return null;
5465
+ 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.`;
5466
+ }
5467
+ function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
4728
5468
  try {
4729
- 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);
4730
- return entries.length > 0 && entries[0] ? join6(dir, entries[0].f) : null;
5469
+ const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
5470
+ return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
4731
5471
  } catch {
4732
5472
  return null;
4733
5473
  }
@@ -4743,7 +5483,7 @@ var MANDATORY_READS = [
4743
5483
  function buildSuccessorPrompt(handoffMarkdown, goal) {
4744
5484
  const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
4745
5485
  const lines = [
4746
- "You are the SUCCESSOR agent for a Virtual Office lane. The previous session",
5486
+ "You are the SUCCESSOR agent for an AlgoHQ lane. The previous session",
4747
5487
  "exhausted its context and wrote the handoff below. Read it fully, verify its",
4748
5488
  '"verification needed" items against live state (a handoff is a claim, not',
4749
5489
  "evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
@@ -4754,7 +5494,7 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
4754
5494
  "NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
4755
5495
  "(verified-answer-only, no fake green); verify-before-act + human merge approval;",
4756
5496
  "never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
4757
- "branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and VO changes update",
5497
+ "branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and AlgoHQ changes update",
4758
5498
  "the roadmap in the same PR.",
4759
5499
  "",
4760
5500
  "--- HANDOFF ---",
@@ -4771,9 +5511,87 @@ function buildSuccessorArgs(maxTurns) {
4771
5511
  }
4772
5512
  return args;
4773
5513
  }
5514
+ function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
5515
+ const rawBinding = env[SWARM_TIER_BINDING_ENV];
5516
+ const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
5517
+ if (!hasBinding) {
5518
+ const explicit = input.agent?.trim();
5519
+ if (explicit) {
5520
+ const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
5521
+ if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
5522
+ return {
5523
+ ok: true,
5524
+ bin: resolved2.bin,
5525
+ args: resolved2.args,
5526
+ agent: resolved2.agent,
5527
+ tier: "unbound",
5528
+ bound: false,
5529
+ env: {},
5530
+ slot: null,
5531
+ capUsd: null,
5532
+ capRemainingUsd: null
5533
+ };
5534
+ }
5535
+ return {
5536
+ ok: true,
5537
+ bin: "claude",
5538
+ args: buildSuccessorArgs(input.max_turns),
5539
+ agent: "claude",
5540
+ tier: "unbound",
5541
+ bound: false,
5542
+ env: {},
5543
+ slot: null,
5544
+ capUsd: null,
5545
+ capRemainingUsd: null
5546
+ };
5547
+ }
5548
+ const binding = inheritSwarmTierBinding(env, nowIso);
5549
+ const admission = admitSubagentSpawn(binding);
5550
+ if (!admission.allowed) {
5551
+ return { ok: false, reason: admission.reason, tier: binding.tier };
5552
+ }
5553
+ const agentRefusal = agentBindingRefusal(binding, input.agent);
5554
+ if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
5555
+ const resolved = resolveSuccessorLaunch({
5556
+ agent: binding.agent,
5557
+ maxTurns: input.max_turns,
5558
+ platform
5559
+ });
5560
+ if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
5561
+ const slot = claim({
5562
+ swarmId: binding.swarm_id,
5563
+ proposedCeiling: binding.subagent_budget,
5564
+ // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
5565
+ // child's cap is DEBITED from it below, not recomputed from this binding.
5566
+ proposedCapUsd: binding.spend_cap_usd,
5567
+ dir: resolveLedgerDir(env),
5568
+ nowIso
5569
+ });
5570
+ if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
5571
+ return {
5572
+ ok: true,
5573
+ bin: resolved.bin,
5574
+ args: resolved.args,
5575
+ agent: resolved.agent,
5576
+ tier: binding.tier,
5577
+ bound: true,
5578
+ // Re-export the same TIER with a DECREMENTED budget and the spend cap the
5579
+ // ledger just DEBITED. Exporting the binding verbatim (what this did before
5580
+ // #9312) meant the child re-read the full budget and every generation
5581
+ // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
5582
+ // bounded a chain but not a tree: three siblings each re-halved the parent's
5583
+ // untouched $50 and walked away with $75 between them.
5584
+ env: childBindingEnvFragment(binding, slot.capUsd),
5585
+ slot: slot.slot,
5586
+ capUsd: slot.capUsd,
5587
+ capRemainingUsd: slot.capRemainingUsd
5588
+ };
5589
+ }
4774
5590
  async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
5591
+ const retired = retiredCounterRefusal(rawInput);
5592
+ if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
4775
5593
  if (!isToolInput20(rawInput)) {
4776
- throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
5594
+ throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
4777
5595
  }
4778
5596
  const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
4779
5597
  if (!handoffPath || !existsSync4(handoffPath)) {
@@ -4786,20 +5604,37 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
4786
5604
  }
4787
5605
  });
4788
5606
  }
4789
- const handoff = readFileSync6(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
5607
+ const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
4790
5608
  const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
4791
- const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join6(homedir4(), ".vo", "successors");
4792
- mkdirSync3(logDir, { recursive: true });
4793
- const logPath = join6(logDir, `successor-${Date.now()}.log`);
4794
- const logFd = openSync(logPath, "a");
4795
- const child = spawnImpl("claude", buildSuccessorArgs(rawInput.max_turns), {
5609
+ const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
5610
+ if (!plan.ok) {
5611
+ return jsonContent({
5612
+ tool: TOOL_NAME20,
5613
+ schema_version: 1,
5614
+ payload: {
5615
+ spawned: false,
5616
+ reason: `swarm tier binding refused this spawn: ${plan.reason}`,
5617
+ tier: plan.tier,
5618
+ handoff_path: handoffPath
5619
+ }
5620
+ });
5621
+ }
5622
+ const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
5623
+ mkdirSync4(logDir, { recursive: true });
5624
+ const logPath = join7(logDir, `successor-${Date.now()}.log`);
5625
+ const logFd = openSync2(logPath, "a");
5626
+ const child = spawnImpl(plan.bin, [...plan.args], {
4796
5627
  cwd: rawInput.cwd?.trim() || process.cwd(),
4797
5628
  detached: true,
4798
5629
  stdio: ["pipe", logFd, logFd],
4799
- // Windows: `claude` is a .cmd shimneeds a shell to resolve. The prompt
4800
- // goes via STDIN below, never argv, so the shell never sees it.
5630
+ // Windows: the agent CLIs are .cmd shimsthey need a shell to resolve.
5631
+ // The prompt goes via STDIN below, never argv, so the shell never sees it.
4801
5632
  shell: process.platform === "win32",
4802
- windowsHide: true
5633
+ windowsHide: true,
5634
+ // Carry the SAME binding to the child. Without this the successor inherits
5635
+ // no tier and re-resolves its own — which is the split-payer defect one
5636
+ // generation down.
5637
+ ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
4803
5638
  });
4804
5639
  let spawnError = null;
4805
5640
  child.on("error", (e) => {
@@ -4815,7 +5650,20 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
4815
5650
  return jsonContent({
4816
5651
  tool: TOOL_NAME20,
4817
5652
  schema_version: 1,
4818
- payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, handoff_path: handoffPath } : { spawned: true, pid: child.pid ?? null, log_path: logPath, handoff_path: handoffPath }
5653
+ payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
5654
+ spawned: true,
5655
+ pid: child.pid ?? null,
5656
+ log_path: logPath,
5657
+ handoff_path: handoffPath,
5658
+ agent: plan.agent,
5659
+ tier: plan.tier,
5660
+ tier_bound: plan.bound,
5661
+ ledger_slot: plan.slot,
5662
+ // The debit, surfaced so an operator can reconcile a fan-out's spend
5663
+ // against the pool without reading the ledger directory by hand.
5664
+ ledger_cap_usd: plan.capUsd,
5665
+ ledger_cap_remaining_usd: plan.capRemainingUsd
5666
+ }
4819
5667
  });
4820
5668
  }
4821
5669
 
@@ -4901,49 +5749,294 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
4901
5749
  }
4902
5750
 
4903
5751
  // src/tools/memory/sync-config.ts
4904
- import { homedir as homedir5 } from "node:os";
4905
- import { join as join7 } from "node:path";
4906
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "node:fs";
4907
- var TOOL_NAME22 = "vo_sync_config";
4908
- var inputSchema22 = {
4909
- type: "object",
4910
- properties: {
4911
- action: {
4912
- type: "string",
4913
- enum: ["pull", "push"],
4914
- description: "pull: download cloud memory to local files. push: upload local files to cloud."
4915
- },
4916
- cwd: {
4917
- type: "string",
4918
- description: "Working directory to derive project slug from (default: process.cwd())."
4919
- }
4920
- },
4921
- required: ["action"],
4922
- additionalProperties: false
4923
- };
4924
- 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.";
4925
- function isToolInput22(v) {
4926
- if (typeof v !== "object" || v === null) return false;
4927
- const o = v;
4928
- if (o["action"] !== "pull" && o["action"] !== "push") return false;
4929
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
4930
- return true;
5752
+ import { existsSync as existsSync8 } from "node:fs";
5753
+ import { homedir as homedir7 } from "node:os";
5754
+ import { join as join11 } from "node:path";
5755
+
5756
+ // src/tools/memory/memory-sync-http.ts
5757
+ init_safe_memory_file();
5758
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
5759
+
5760
+ // src/tools/memory/sync-lock.ts
5761
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
5762
+ import { hostname } from "node:os";
5763
+ import { join as join8 } from "node:path";
5764
+ import { randomUUID as randomUUID2 } from "node:crypto";
5765
+
5766
+ // src/tools/memory/sync-lock-liveness.ts
5767
+ import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
5768
+ function defaultIsProcessAlive(pid) {
5769
+ try {
5770
+ process.kill(pid, 0);
5771
+ return true;
5772
+ } catch (err) {
5773
+ return err.code === "EPERM";
5774
+ }
4931
5775
  }
4932
- function deriveProjectSlug(cwd) {
4933
- return cwd.replace(/\\/g, "/").replace(/\/+$/g, "").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
5776
+ function toPayload(parsed) {
5777
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
5778
+ const record = parsed;
5779
+ const token = record["token"];
5780
+ const host = record["hostname"];
5781
+ if (typeof token !== "string" || token.length === 0) return null;
5782
+ const pid = record["pid"];
5783
+ const acquiredAtMs = record["acquiredAtMs"];
5784
+ return {
5785
+ pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
5786
+ hostname: typeof host === "string" ? host : "",
5787
+ sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
5788
+ token,
5789
+ acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
5790
+ acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
5791
+ };
4934
5792
  }
4935
- function getMemoryDir(cwd) {
4936
- const slug = deriveProjectSlug(cwd);
4937
- return join7(homedir5(), ".claude", "projects", slug, "memory");
5793
+ function readLockRecord(path3) {
5794
+ let raw;
5795
+ try {
5796
+ raw = readFileSync8(path3, "utf8");
5797
+ } catch {
5798
+ return null;
5799
+ }
5800
+ try {
5801
+ return { raw, payload: toPayload(JSON.parse(raw)) };
5802
+ } catch {
5803
+ return { raw, payload: null };
5804
+ }
5805
+ }
5806
+ function lockAgeMs(record, path3, nowMs) {
5807
+ let startedMs = Number.NaN;
5808
+ if (record.payload) {
5809
+ if (Number.isFinite(record.payload.acquiredAtMs)) {
5810
+ startedMs = record.payload.acquiredAtMs;
5811
+ } else if (record.payload.acquiredAt) {
5812
+ startedMs = Date.parse(record.payload.acquiredAt);
5813
+ }
5814
+ }
5815
+ if (!Number.isFinite(startedMs)) {
5816
+ try {
5817
+ startedMs = statSync4(path3).mtimeMs;
5818
+ } catch {
5819
+ return null;
5820
+ }
5821
+ }
5822
+ const age = nowMs - startedMs;
5823
+ return Number.isFinite(age) && age >= 0 ? age : null;
5824
+ }
5825
+ function classifyHolderLiveness(record, isProcessAlive, thisHost) {
5826
+ const payload = record.payload;
5827
+ if (payload === null) return "unknown";
5828
+ if (payload.pid <= 0) return "unknown";
5829
+ if (thisHost.length === 0) return "unknown";
5830
+ if (payload.hostname !== thisHost) return "unknown";
5831
+ return isProcessAlive(payload.pid) ? "alive" : "dead";
5832
+ }
5833
+ function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
5834
+ const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
5835
+ if (liveness === "alive") return false;
5836
+ if (liveness === "dead") return true;
5837
+ return ageMs !== null && ageMs > ttlMs;
5838
+ }
5839
+
5840
+ // src/tools/memory/sync-lock.ts
5841
+ var MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
5842
+ var DEFAULT_LOCK_TTL_MS = 15 * 6e4;
5843
+ var DEFAULT_LOCK_WAIT_MS = 1e4;
5844
+ var INITIAL_BACKOFF_MS = 25;
5845
+ var MAX_BACKOFF_MS = 500;
5846
+ var BACKOFF_FACTOR = 1.6;
5847
+ function positiveOr(value, fallback) {
5848
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
5849
+ }
5850
+ function createExclusive2(path3, contents) {
5851
+ let fd;
5852
+ try {
5853
+ fd = openSync3(path3, "wx");
5854
+ } catch (err) {
5855
+ const code = err.code;
5856
+ return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
5857
+ }
5858
+ try {
5859
+ writeFileSync4(fd, contents, "utf8");
5860
+ } catch (err) {
5861
+ closeSync2(fd);
5862
+ try {
5863
+ unlinkSync2(path3);
5864
+ } catch {
5865
+ }
5866
+ return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
5867
+ }
5868
+ closeSync2(fd);
5869
+ return { ok: true };
5870
+ }
5871
+ function removeAbandoned(path3, expectedRaw) {
5872
+ let current;
5873
+ try {
5874
+ current = readFileSync9(path3, "utf8");
5875
+ } catch {
5876
+ return;
5877
+ }
5878
+ if (current !== expectedRaw) return;
5879
+ try {
5880
+ unlinkSync2(path3);
5881
+ } catch {
5882
+ }
4938
5883
  }
5884
+ function makeRelease(path3, token) {
5885
+ let released = false;
5886
+ return () => {
5887
+ if (released) return;
5888
+ released = true;
5889
+ let raw;
5890
+ try {
5891
+ raw = readFileSync9(path3, "utf8");
5892
+ } catch {
5893
+ return;
5894
+ }
5895
+ let stillOurs;
5896
+ try {
5897
+ stillOurs = toPayload(JSON.parse(raw))?.token === token;
5898
+ } catch {
5899
+ stillOurs = false;
5900
+ }
5901
+ if (!stillOurs) return;
5902
+ try {
5903
+ unlinkSync2(path3);
5904
+ } catch {
5905
+ }
5906
+ };
5907
+ }
5908
+ function describeHolder(record) {
5909
+ const payload = record?.payload;
5910
+ if (!payload) return "an unreadable lock file";
5911
+ return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
5912
+ }
5913
+ async function acquireMemorySyncLock(options) {
5914
+ const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
5915
+ const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
5916
+ const now = options.now ?? Date.now;
5917
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
5918
+ setTimeout(resolve3, ms);
5919
+ }));
5920
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
5921
+ const thisHost = hostname();
5922
+ const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
5923
+ if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
5924
+ const deadline = now() + waitMs;
5925
+ let backoffMs = INITIAL_BACKOFF_MS;
5926
+ let tookOverFrom = null;
5927
+ let holderDescription = "another session";
5928
+ for (; ; ) {
5929
+ const acquiredAtMs = now();
5930
+ const payload = {
5931
+ pid: process.pid,
5932
+ hostname: thisHost,
5933
+ sessionId: options.sessionId ?? null,
5934
+ token: randomUUID2(),
5935
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
5936
+ acquiredAtMs
5937
+ };
5938
+ const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
5939
+ `);
5940
+ if (created.ok) {
5941
+ return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
5942
+ }
5943
+ if (!created.exists) {
5944
+ throw new Error(
5945
+ `memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
5946
+ );
5947
+ }
5948
+ const record = readLockRecord(path3);
5949
+ let reclaimed = false;
5950
+ if (record) {
5951
+ holderDescription = describeHolder(record);
5952
+ const age = lockAgeMs(record, path3, now());
5953
+ if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
5954
+ tookOverFrom = record.payload;
5955
+ removeAbandoned(path3, record.raw);
5956
+ reclaimed = true;
5957
+ }
5958
+ }
5959
+ if (now() >= deadline) {
5960
+ throw new Error(
5961
+ `memory sync lock ${path3} 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.`
5962
+ );
5963
+ }
5964
+ if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
5965
+ await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
5966
+ if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
5967
+ }
5968
+ }
5969
+ async function withMemorySyncLock(options, fn) {
5970
+ const handle = await acquireMemorySyncLock(options);
5971
+ try {
5972
+ return await fn(handle);
5973
+ } finally {
5974
+ handle.release();
5975
+ }
5976
+ }
5977
+
5978
+ // src/tools/memory/memory-index-merge.ts
5979
+ var MEMORY_INDEX_FILE = "MEMORY.md";
5980
+ function isMemoryIndexFile(fileName) {
5981
+ return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
5982
+ }
5983
+ var INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
5984
+ function indexRowKey(line) {
5985
+ const match = INDEX_ROW_RE.exec(line);
5986
+ if (!match) return null;
5987
+ let target = match[1].trim();
5988
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
5989
+ target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
5990
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
5991
+ target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
5992
+ target = target.replace(/^(?:\.\/)+/, "");
5993
+ }
5994
+ return target.length > 0 ? target.toLowerCase() : null;
5995
+ }
5996
+ function mergeMemoryIndex(localContent, cloudContent) {
5997
+ if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
5998
+ return { content: localContent, addedFromCloud: [] };
5999
+ }
6000
+ const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
6001
+ const localLines = localContent.split(/\r?\n/);
6002
+ const localKeys = /* @__PURE__ */ new Set();
6003
+ let lastLocalRowIndex = -1;
6004
+ for (let i = 0; i < localLines.length; i++) {
6005
+ const key = indexRowKey(localLines[i]);
6006
+ if (key === null) continue;
6007
+ localKeys.add(key);
6008
+ lastLocalRowIndex = i;
6009
+ }
6010
+ const addedFromCloud = [];
6011
+ const seenCloudKeys = /* @__PURE__ */ new Set();
6012
+ for (const rawLine of cloudContent.split(/\r?\n/)) {
6013
+ const key = indexRowKey(rawLine);
6014
+ if (key === null) continue;
6015
+ if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
6016
+ seenCloudKeys.add(key);
6017
+ addedFromCloud.push(rawLine.replace(/\r$/, ""));
6018
+ }
6019
+ if (addedFromCloud.length === 0) {
6020
+ return { content: localContent, addedFromCloud: [] };
6021
+ }
6022
+ const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
6023
+ return { content: merged.join(eol), addedFromCloud };
6024
+ }
6025
+
6026
+ // src/tools/memory/memory-sync-http.ts
6027
+ init_bounded_sync();
6028
+ init_memory_push_cache();
4939
6029
  async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
4940
6030
  const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
4941
- const response = await fetchFn(url, {
4942
- method: "GET",
4943
- headers: {
4944
- authorization: `Bearer ${token}`
4945
- }
4946
- });
6031
+ const response = await withRequestTimeout(
6032
+ url,
6033
+ () => fetchFn(url, {
6034
+ method: "GET",
6035
+ headers: {
6036
+ authorization: `Bearer ${token}`
6037
+ }
6038
+ })
6039
+ );
4947
6040
  if (response.status !== 200) {
4948
6041
  const text = await response.text();
4949
6042
  throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
@@ -4952,104 +6045,250 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
4952
6045
  if (!data.ok || !Array.isArray(data.entries)) {
4953
6046
  throw new Error("GET /api/v1/agent-config/memory/me response missing ok=true or entries array");
4954
6047
  }
4955
- mkdirSync4(memoryDir, { recursive: true });
6048
+ const writes = data.entries.map((entry) => ({
6049
+ entry,
6050
+ filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
6051
+ }));
6052
+ mkdirSync6(memoryDir, { recursive: true });
4956
6053
  const files = [];
4957
- for (const entry of data.entries) {
4958
- const filePath = join7(memoryDir, entry.file_name);
4959
- writeFileSync3(filePath, entry.content, "utf8");
6054
+ for (const { entry, filePath } of writes) {
6055
+ writeFileSync6(filePath, entry.content, "utf8");
4960
6056
  files.push(entry.file_name);
4961
6057
  }
4962
6058
  return { pulled: data.entries.length, files };
4963
6059
  }
4964
- async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
6060
+ function listPushableFiles(memoryDir) {
6061
+ return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
6062
+ }
6063
+ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
6064
+ deadline.check();
6065
+ if (item.memoryId !== null) {
6066
+ const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
6067
+ const updateBody = { content: item.content, session_id: sessionId };
6068
+ const updateResponse = await withRequestTimeout(
6069
+ updateUrl,
6070
+ () => fetchFn(updateUrl, {
6071
+ method: "PUT",
6072
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6073
+ body: JSON.stringify(updateBody)
6074
+ })
6075
+ );
6076
+ if (updateResponse.status !== 200) {
6077
+ const text = await updateResponse.text();
6078
+ throw new Error(
6079
+ `PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
6080
+ );
6081
+ }
6082
+ const updateData = JSON.parse(await updateResponse.text());
6083
+ if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
6084
+ return "updated";
6085
+ }
6086
+ const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
6087
+ const createBody = {
6088
+ entry_type: item.entryType,
6089
+ file_name: item.fileName,
6090
+ content: item.content,
6091
+ session_id: sessionId
6092
+ };
6093
+ const createResponse = await withRequestTimeout(
6094
+ createUrl,
6095
+ () => fetchFn(createUrl, {
6096
+ method: "POST",
6097
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6098
+ body: JSON.stringify(createBody)
6099
+ })
6100
+ );
6101
+ if (createResponse.status !== 200 && createResponse.status !== 201) {
6102
+ const text = await createResponse.text();
6103
+ throw new Error(
6104
+ `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
6105
+ );
6106
+ }
6107
+ const createData = JSON.parse(await createResponse.text());
6108
+ if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
6109
+ return "created";
6110
+ }
6111
+ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
6112
+ const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
4965
6113
  if (!existsSync5(memoryDir)) {
4966
- return { pushed: 0, created: 0, updated: 0 };
6114
+ return empty;
4967
6115
  }
4968
- const localFiles = readdirSync4(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
6116
+ const localFiles = listPushableFiles(memoryDir).map((f) => ({
4969
6117
  file_name: f,
4970
- content: readFileSync7(join7(memoryDir, f), "utf8"),
4971
- entry_type: f === "MEMORY.md" ? "index" : "topic"
6118
+ content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
6119
+ entry_type: isMemoryIndexFile(f) ? "index" : "topic"
4972
6120
  }));
4973
6121
  if (localFiles.length === 0) {
4974
- return { pushed: 0, created: 0, updated: 0 };
6122
+ return empty;
4975
6123
  }
6124
+ const deadline = options.deadline ?? createSyncDeadline();
6125
+ const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
6126
+ deadline.check();
4976
6127
  const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
4977
- const getResponse = await fetchFn(getUrl, {
4978
- method: "GET",
4979
- headers: {
4980
- authorization: `Bearer ${token}`
4981
- }
4982
- });
6128
+ const getResponse = await withRequestTimeout(
6129
+ getUrl,
6130
+ () => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
6131
+ );
4983
6132
  const existingMap = /* @__PURE__ */ new Map();
4984
6133
  if (getResponse.status === 200) {
4985
6134
  const getData = JSON.parse(await getResponse.text());
4986
6135
  if (getData.ok && Array.isArray(getData.entries)) {
4987
6136
  for (const entry of getData.entries) {
4988
- existingMap.set(entry.file_name, entry.memory_id);
6137
+ existingMap.set(entry.file_name, {
6138
+ memoryId: entry.memory_id,
6139
+ content: typeof entry.content === "string" ? entry.content : ""
6140
+ });
4989
6141
  }
4990
6142
  }
4991
6143
  }
6144
+ const toUpload = [];
6145
+ let skipped = 0;
6146
+ for (const localFile of localFiles) {
6147
+ const existing = existingMap.get(localFile.file_name);
6148
+ let content = localFile.content;
6149
+ let rowsPreserved = 0;
6150
+ if (localFile.entry_type === "index") {
6151
+ const merged = mergeMemoryIndex(localFile.content, existing?.content);
6152
+ content = merged.content;
6153
+ rowsPreserved = merged.addedFromCloud.length;
6154
+ }
6155
+ const payloadHash = sha256(content);
6156
+ if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
6157
+ skipped++;
6158
+ continue;
6159
+ }
6160
+ toUpload.push({
6161
+ fileName: localFile.file_name,
6162
+ entryType: localFile.entry_type,
6163
+ content,
6164
+ payloadHash,
6165
+ memoryId: existing?.memoryId ?? null,
6166
+ rowsPreserved
6167
+ });
6168
+ }
6169
+ const outcomes = await mapWithConcurrency(
6170
+ toUpload,
6171
+ options.concurrency ?? PUSH_CONCURRENCY,
6172
+ (item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
6173
+ );
4992
6174
  let created = 0;
4993
6175
  let updated = 0;
4994
- for (const localFile of localFiles) {
4995
- const memoryId = existingMap.get(localFile.file_name);
4996
- if (memoryId) {
4997
- const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${memoryId}`;
4998
- const updateBody = {
4999
- content: localFile.content,
5000
- session_id: sessionId
5001
- };
5002
- const updateResponse = await fetchFn(updateUrl, {
5003
- method: "PUT",
5004
- headers: {
5005
- authorization: `Bearer ${token}`,
5006
- "content-type": "application/json"
5007
- },
5008
- body: JSON.stringify(updateBody)
5009
- });
5010
- if (updateResponse.status !== 200) {
5011
- const text = await updateResponse.text();
5012
- throw new Error(
5013
- `PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
5014
- );
5015
- }
5016
- const updateData = JSON.parse(await updateResponse.text());
5017
- if (!updateData.ok) {
5018
- throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
5019
- }
5020
- updated++;
5021
- } else {
5022
- const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5023
- const createBody = {
5024
- entry_type: localFile.entry_type,
5025
- file_name: localFile.file_name,
5026
- content: localFile.content,
5027
- session_id: sessionId
5028
- };
5029
- const createResponse = await fetchFn(createUrl, {
5030
- method: "POST",
5031
- headers: {
5032
- authorization: `Bearer ${token}`,
5033
- "content-type": "application/json"
5034
- },
5035
- body: JSON.stringify(createBody)
5036
- });
5037
- if (createResponse.status !== 200 && createResponse.status !== 201) {
5038
- const text = await createResponse.text();
5039
- throw new Error(
5040
- `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
5041
- );
5042
- }
5043
- const createData = JSON.parse(await createResponse.text());
5044
- if (!createData.ok) {
5045
- throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
5046
- }
5047
- created++;
6176
+ let indexRowsPreserved = 0;
6177
+ let firstError;
6178
+ for (let i = 0; i < outcomes.length; i++) {
6179
+ const outcome = outcomes[i];
6180
+ const item = toUpload[i];
6181
+ if (outcome.ok) {
6182
+ if (outcome.value === "created") created++;
6183
+ else updated++;
6184
+ indexRowsPreserved += item.rowsPreserved;
6185
+ recordMemoryPush(cache, item.fileName, item.payloadHash);
6186
+ } else if (firstError === void 0) {
6187
+ firstError = outcome.error;
5048
6188
  }
5049
6189
  }
5050
- return { pushed: localFiles.length, created, updated };
6190
+ pruneMissing(cache, localFiles.map((f) => f.file_name));
6191
+ if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
6192
+ if (firstError !== void 0) throw firstError;
6193
+ return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
5051
6194
  }
5052
- async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
6195
+
6196
+ // src/tools/memory/sync-config.ts
6197
+ init_bounded_sync();
6198
+ init_memory_push_cache();
6199
+
6200
+ // src/tools/memory/sync-kill-switch.ts
6201
+ import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
6202
+ import { homedir as homedir6 } from "node:os";
6203
+ import { join as join10 } from "node:path";
6204
+ var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
6205
+ var MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
6206
+ var NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
6207
+ var MAX_LOGGED_VALUE = 32;
6208
+ function memorySyncSentinelPath(home) {
6209
+ return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
6210
+ }
6211
+ function isKillSwitchValueOn(raw) {
6212
+ if (raw === void 0 || raw === null) return false;
6213
+ const v = raw.trim().toLowerCase();
6214
+ if (v === "") return false;
6215
+ return !NEGATIONS.has(v);
6216
+ }
6217
+ function clip(raw) {
6218
+ const v = raw.trim();
6219
+ return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
6220
+ }
6221
+ function evaluateMemorySyncKillSwitch(deps = {}) {
6222
+ const env = deps.env ?? process.env;
6223
+ const home = deps.home ?? homedir6();
6224
+ const fileExists = deps.fileExists ?? existsSync6;
6225
+ const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
6226
+ const fired = [];
6227
+ const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
6228
+ if (isKillSwitchValueOn(rawEnv)) {
6229
+ fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
6230
+ }
6231
+ const sentinel = memorySyncSentinelPath(home);
6232
+ let sentinelPresent;
6233
+ try {
6234
+ sentinelPresent = fileExists(sentinel);
6235
+ } catch {
6236
+ sentinelPresent = false;
6237
+ }
6238
+ if (sentinelPresent) {
6239
+ let contents = "";
6240
+ let readable = true;
6241
+ try {
6242
+ contents = readFile3(sentinel);
6243
+ } catch {
6244
+ readable = false;
6245
+ }
6246
+ if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
6247
+ fired.push(`sentinel file ${sentinel}`);
6248
+ }
6249
+ }
6250
+ if (fired.length === 0) return { disabled: false, reason: null };
6251
+ return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
6252
+ }
6253
+
6254
+ // src/tools/memory/sync-config.ts
6255
+ var TOOL_NAME22 = "vo_sync_config";
6256
+ var inputSchema22 = {
6257
+ type: "object",
6258
+ properties: {
6259
+ action: {
6260
+ type: "string",
6261
+ enum: ["pull", "push"],
6262
+ description: "pull: download cloud memory to local files. push: upload local files to cloud."
6263
+ },
6264
+ cwd: {
6265
+ type: "string",
6266
+ description: "Working directory to derive project slug from (default: process.cwd())."
6267
+ }
6268
+ },
6269
+ required: ["action"],
6270
+ additionalProperties: false
6271
+ };
6272
+ 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. Serialized across concurrent sessions by an exclusive lock in the memory dir.";
6273
+ function isToolInput22(v) {
6274
+ if (typeof v !== "object" || v === null) return false;
6275
+ const o = v;
6276
+ if (o["action"] !== "pull" && o["action"] !== "push") return false;
6277
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
6278
+ return true;
6279
+ }
6280
+ function deriveProjectSlug(cwd) {
6281
+ return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
6282
+ }
6283
+ function getMemoryDir(cwd) {
6284
+ const slug = deriveProjectSlug(cwd);
6285
+ return join11(homedir7(), ".claude", "projects", slug, "memory");
6286
+ }
6287
+ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
6288
+ const killSwitch = evaluateMemorySyncKillSwitch();
6289
+ if (killSwitch.disabled) {
6290
+ return { synced: false, reason: killSwitch.reason };
6291
+ }
5053
6292
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
5054
6293
  if (!controlPlaneUrl) {
5055
6294
  return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
@@ -5066,20 +6305,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
5066
6305
  }
5067
6306
  const memoryDir = getMemoryDir(cwd);
5068
6307
  const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
5069
- try {
5070
- if (action === "pull") {
5071
- const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
5072
- return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
5073
- }
5074
- const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
6308
+ if (action === "push" && !existsSync8(memoryDir)) {
5075
6309
  return {
5076
6310
  synced: true,
5077
6311
  action: "push",
5078
- pushed: result.pushed,
5079
- created: result.created,
5080
- updated: result.updated,
6312
+ pushed: 0,
6313
+ created: 0,
6314
+ updated: 0,
6315
+ skipped: 0,
6316
+ index_rows_preserved: 0,
6317
+ knowledge_upserted: 0,
6318
+ knowledge_failed: 0,
5081
6319
  memory_dir: memoryDir
5082
6320
  };
6321
+ }
6322
+ try {
6323
+ return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
6324
+ const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
6325
+ if (action === "pull") {
6326
+ const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
6327
+ return {
6328
+ synced: true,
6329
+ action: "pull",
6330
+ pulled: result2.pulled,
6331
+ files: result2.files,
6332
+ memory_dir: memoryDir,
6333
+ ...takeover
6334
+ };
6335
+ }
6336
+ const deadline = createSyncDeadline();
6337
+ const cache = readPushCache(memoryDir, baseUrl);
6338
+ let result;
6339
+ try {
6340
+ result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
6341
+ } catch (err) {
6342
+ writePushCache(memoryDir, cache);
6343
+ throw err;
6344
+ }
6345
+ let bridge;
6346
+ try {
6347
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
6348
+ bridge = await upsertMemoryFilesAsKnowledge2({
6349
+ controlPlaneUrl: baseUrl,
6350
+ token,
6351
+ memoryDir,
6352
+ fetchFn,
6353
+ cache,
6354
+ deadline
6355
+ });
6356
+ } catch (err) {
6357
+ bridge = {
6358
+ upserted: 0,
6359
+ failed: 1,
6360
+ skipped: 0,
6361
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
6362
+ };
6363
+ }
6364
+ writePushCache(memoryDir, cache);
6365
+ return {
6366
+ synced: true,
6367
+ action: "push",
6368
+ pushed: result.pushed,
6369
+ created: result.created,
6370
+ updated: result.updated,
6371
+ skipped: result.skipped,
6372
+ index_rows_preserved: result.indexRowsPreserved,
6373
+ memory_dir: memoryDir,
6374
+ knowledge_upserted: bridge.upserted,
6375
+ knowledge_failed: bridge.failed,
6376
+ knowledge_skipped: bridge.skipped,
6377
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
6378
+ ...takeover
6379
+ };
6380
+ });
5083
6381
  } catch (err) {
5084
6382
  const message = err instanceof Error ? err.message : String(err);
5085
6383
  return { synced: false, reason: `Sync failed: ${message}` };
@@ -5097,6 +6395,505 @@ async function handleSyncConfig(deps, rawInput, _signal, fetchFn = globalThis.fe
5097
6395
  return jsonContent({ tool: TOOL_NAME22, schema_version: 1, payload: result });
5098
6396
  }
5099
6397
 
6398
+ // src/tools/memory/private-knowledge.ts
6399
+ var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
6400
+ var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
6401
+ var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
6402
+ var STALE_TOOL_NAME = "vo_private_knowledge_stale";
6403
+ var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
6404
+ var PRECISION_CHAR_BUDGET = 12e3;
6405
+ var upsertInputSchema = {
6406
+ type: "object",
6407
+ properties: {
6408
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
6409
+ source_path: { type: "string", description: "Stable private source identifier; not exposed to other users." },
6410
+ title: { type: "string", description: 'Descriptive, retrieval-friendly title (e.g. "AlgoTax OCR redaction architecture", not "notes") \u2014 retrieval matches on it.' },
6411
+ content: { type: "string", description: "Private knowledge text to store server-side. Keep each entry tight and focused (~1-3 pages, under ~12k chars); split larger corpora into separate entries." }
6412
+ },
6413
+ required: ["knowledge_class", "source_path", "title", "content"],
6414
+ additionalProperties: false
6415
+ };
6416
+ var contextInputSchema = {
6417
+ type: "object",
6418
+ properties: {
6419
+ query: { type: "string" },
6420
+ limit: { type: "number", minimum: 1, maximum: 50 },
6421
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES }
6422
+ },
6423
+ required: ["query"],
6424
+ additionalProperties: false
6425
+ };
6426
+ var invalidateInputSchema = {
6427
+ type: "object",
6428
+ properties: {
6429
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
6430
+ 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." }
6431
+ },
6432
+ required: ["knowledge_class", "source_path"],
6433
+ additionalProperties: false
6434
+ };
6435
+ var staleInputSchema = {
6436
+ type: "object",
6437
+ properties: {
6438
+ 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." },
6439
+ limit: { type: "number", minimum: 1, maximum: 500 }
6440
+ },
6441
+ additionalProperties: false
6442
+ };
6443
+ 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.`;
6444
+ 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.";
6445
+ 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.";
6446
+ 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.`;
6447
+ function isStaleInput(value) {
6448
+ if (value === void 0 || value === null) return true;
6449
+ if (typeof value !== "object") return false;
6450
+ const input = value;
6451
+ if (input["days"] !== void 0 && typeof input["days"] !== "number") return false;
6452
+ if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
6453
+ return true;
6454
+ }
6455
+ function isKnowledgeClass(value) {
6456
+ return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
6457
+ }
6458
+ function isUpsertInput(value) {
6459
+ if (typeof value !== "object" || value === null) return false;
6460
+ const input = value;
6461
+ return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
6462
+ }
6463
+ function isInvalidateInput(value) {
6464
+ if (typeof value !== "object" || value === null) return false;
6465
+ const input = value;
6466
+ return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
6467
+ }
6468
+ function isContextInput(value) {
6469
+ if (typeof value !== "object" || value === null) return false;
6470
+ const input = value;
6471
+ if (typeof input["query"] !== "string") return false;
6472
+ if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
6473
+ if (input["knowledge_class"] !== void 0 && !isKnowledgeClass(input["knowledge_class"])) return false;
6474
+ return true;
6475
+ }
6476
+ async function getCloudAuth(fetchFn) {
6477
+ const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.replace(/\/+$/, "");
6478
+ if (!controlPlaneUrl) {
6479
+ return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
6480
+ }
6481
+ const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
6482
+ const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
6483
+ const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
6484
+ if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
6485
+ const token = await tokenSource.getToken();
6486
+ if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
6487
+ return { ok: true, controlPlaneUrl, token };
6488
+ }
6489
+ async function callPrivateKnowledge(path3, body, fetchFn) {
6490
+ const auth = await getCloudAuth(fetchFn);
6491
+ if (!auth.ok) return { ok: false, reason: auth.reason };
6492
+ const response = await fetchFn(`${auth.controlPlaneUrl}${path3}`, {
6493
+ method: "POST",
6494
+ headers: {
6495
+ authorization: `Bearer ${auth.token}`,
6496
+ "content-type": "application/json"
6497
+ },
6498
+ body: JSON.stringify(body)
6499
+ });
6500
+ const text = await response.text();
6501
+ let parsed;
6502
+ try {
6503
+ parsed = text ? JSON.parse(text) : null;
6504
+ } catch {
6505
+ parsed = null;
6506
+ }
6507
+ if (response.status < 200 || response.status >= 300) {
6508
+ return { ok: false, status: response.status, response: parsed ?? text };
6509
+ }
6510
+ return parsed;
6511
+ }
6512
+ async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
6513
+ if (!isUpsertInput(rawInput)) {
6514
+ throw invalidParams(UPSERT_TOOL_NAME, "expected { knowledge_class, source_path, title, content }.");
6515
+ }
6516
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private", rawInput, fetchFn);
6517
+ const envelope = {
6518
+ tool: UPSERT_TOOL_NAME,
6519
+ schema_version: 1,
6520
+ payload
6521
+ };
6522
+ if (rawInput.content.length > PRECISION_CHAR_BUDGET) {
6523
+ envelope.precision_note = `content is ${rawInput.content.length} chars (> ${PRECISION_CHAR_BUDGET}). Tight 1-3 page entries retrieve better \u2014 consider splitting into focused entries, then re-test retrieval via ${CONTEXT_TOOL_NAME}.`;
6524
+ }
6525
+ return jsonContent(envelope);
6526
+ }
6527
+ async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
6528
+ if (!isInvalidateInput(rawInput)) {
6529
+ throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
6530
+ }
6531
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
6532
+ return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
6533
+ }
6534
+ async function handlePrivateKnowledgeStale(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
6535
+ if (!isStaleInput(rawInput)) {
6536
+ throw invalidParams(STALE_TOOL_NAME, "expected { optional days, optional limit }.");
6537
+ }
6538
+ const auth = await getCloudAuth(fetchFn);
6539
+ if (!auth.ok) return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload: { ok: false, reason: auth.reason } });
6540
+ const params = new URLSearchParams();
6541
+ if (rawInput?.days !== void 0) params.set("days", String(Math.trunc(rawInput.days)));
6542
+ if (rawInput?.limit !== void 0) params.set("limit", String(Math.trunc(rawInput.limit)));
6543
+ const qs = params.toString();
6544
+ const response = await fetchFn(`${auth.controlPlaneUrl}/api/v1/knowledge/private/stale${qs ? `?${qs}` : ""}`, {
6545
+ method: "GET",
6546
+ headers: { authorization: `Bearer ${auth.token}` }
6547
+ });
6548
+ const text = await response.text();
6549
+ let parsed;
6550
+ try {
6551
+ parsed = text ? JSON.parse(text) : null;
6552
+ } catch {
6553
+ parsed = null;
6554
+ }
6555
+ const payload = response.status < 200 || response.status >= 300 ? { ok: false, status: response.status, response: parsed ?? text } : parsed;
6556
+ return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload });
6557
+ }
6558
+ async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
6559
+ if (!isContextInput(rawInput)) {
6560
+ throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
6561
+ }
6562
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private/context", rawInput, fetchFn);
6563
+ return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
6564
+ }
6565
+
6566
+ // src/tools/hq/whiteboard.ts
6567
+ init_auth_token_source();
6568
+ init_credential_store();
6569
+ var POST_TOOL_NAME = "hq_whiteboard_post";
6570
+ var READ_TOOL_NAME = "hq_whiteboard_read";
6571
+ 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.";
6572
+ 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.";
6573
+ var postInputSchema = {
6574
+ type: "object",
6575
+ properties: {
6576
+ from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
6577
+ type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
6578
+ content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
6579
+ targetAgent: { type: "string", maxLength: 100 },
6580
+ tester: { type: "string", maxLength: 100 },
6581
+ tier: { type: "string", maxLength: 32 }
6582
+ },
6583
+ required: ["from", "type", "content"],
6584
+ additionalProperties: false
6585
+ };
6586
+ var readInputSchema = {
6587
+ type: "object",
6588
+ properties: {
6589
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
6590
+ since: { type: "string", description: "Optional ISO-8601 lower bound." },
6591
+ type: { type: "string", minLength: 1, maxLength: 64 }
6592
+ },
6593
+ additionalProperties: false
6594
+ };
6595
+ function resolveTimeoutMs() {
6596
+ const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
6597
+ return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
6598
+ }
6599
+ function isRecord(value) {
6600
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6601
+ }
6602
+ function onlyKeys(value, allowed) {
6603
+ return Object.keys(value).every((key) => allowed.includes(key));
6604
+ }
6605
+ function isBoundedString(value, min, max) {
6606
+ return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
6607
+ }
6608
+ function parsePostInput(value) {
6609
+ if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
6610
+ if (!isBoundedString(value["from"], 1, 100)) return null;
6611
+ if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
6612
+ if (!isBoundedString(value["content"], 1, 500)) return null;
6613
+ for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
6614
+ if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
6615
+ }
6616
+ return {
6617
+ from: value["from"].trim(),
6618
+ type: value["type"].trim(),
6619
+ content: value["content"].trim(),
6620
+ ...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
6621
+ ...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
6622
+ ...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
6623
+ };
6624
+ }
6625
+ function parseReadInput(value) {
6626
+ if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
6627
+ if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
6628
+ if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
6629
+ if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
6630
+ return {
6631
+ ...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
6632
+ ...typeof value["since"] === "string" ? { since: value["since"] } : {},
6633
+ ...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
6634
+ };
6635
+ }
6636
+ async function resolveCloud(fetchFn) {
6637
+ const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
6638
+ if (!url) return null;
6639
+ try {
6640
+ const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
6641
+ const token = await source?.getToken();
6642
+ return token ? { url, token } : null;
6643
+ } catch {
6644
+ return null;
6645
+ }
6646
+ }
6647
+ async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
6648
+ const cloud = await resolveCloud(fetchFn);
6649
+ if (!cloud) {
6650
+ return {
6651
+ ok: false,
6652
+ error: "hq_whiteboard_not_configured",
6653
+ message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
6654
+ };
6655
+ }
6656
+ const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
6657
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
6658
+ const query = new URLSearchParams();
6659
+ if (method === "GET") {
6660
+ const input = bodyOrQuery;
6661
+ query.set("limit", String(input.limit ?? 25));
6662
+ if (input.since) query.set("since", input.since);
6663
+ if (input.type) query.set("type", input.type);
6664
+ }
6665
+ try {
6666
+ const response = await fetchFn(
6667
+ `${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
6668
+ {
6669
+ method,
6670
+ headers: {
6671
+ Authorization: `Bearer ${cloud.token}`,
6672
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
6673
+ },
6674
+ ...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
6675
+ signal: requestSignal
6676
+ }
6677
+ );
6678
+ const text = await response.text();
6679
+ let payload;
6680
+ try {
6681
+ payload = JSON.parse(text);
6682
+ } catch {
6683
+ payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
6684
+ }
6685
+ if (!response.ok) {
6686
+ return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
6687
+ }
6688
+ return payload;
6689
+ } catch (error) {
6690
+ return {
6691
+ ok: false,
6692
+ error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
6693
+ message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
6694
+ };
6695
+ }
6696
+ }
6697
+ async function handleHqWhiteboardPost(_deps, rawInput, signal) {
6698
+ const input = parsePostInput(rawInput);
6699
+ if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
6700
+ return jsonContent(await callWhiteboard("POST", input, signal));
6701
+ }
6702
+ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
6703
+ const input = parseReadInput(rawInput);
6704
+ if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
6705
+ return jsonContent(await callWhiteboard("GET", input, signal));
6706
+ }
6707
+
6708
+ // src/tools/skills/skill-corpus.ts
6709
+ import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
6710
+ import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
6711
+
6712
+ // ../skill-registry/src/loader.ts
6713
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
6714
+ import { join as join12 } from "node:path";
6715
+ var InvalidSkillFrontmatterError = class extends Error {
6716
+ constructor(skillFile, reason) {
6717
+ super(`Invalid frontmatter in ${skillFile}: ${reason}`);
6718
+ this.skillFile = skillFile;
6719
+ this.reason = reason;
6720
+ }
6721
+ skillFile;
6722
+ reason;
6723
+ name = "InvalidSkillFrontmatterError";
6724
+ };
6725
+ var FRONTMATTER_DELIMITER = "---";
6726
+ function parseFrontmatter(rawInput, sourcePath) {
6727
+ const raw = rawInput.replace(/\r\n/g, "\n");
6728
+ if (!raw.startsWith(`${FRONTMATTER_DELIMITER}
6729
+ `)) {
6730
+ throw new InvalidSkillFrontmatterError(sourcePath, 'file does not start with frontmatter delimiter "---"');
6731
+ }
6732
+ const afterFirst = raw.slice(FRONTMATTER_DELIMITER.length + 1);
6733
+ const closingIdx = afterFirst.indexOf(`
6734
+ ${FRONTMATTER_DELIMITER}
6735
+ `);
6736
+ if (closingIdx === -1) {
6737
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing closing frontmatter delimiter "---"');
6738
+ }
6739
+ const frontmatterText = afterFirst.slice(0, closingIdx);
6740
+ const body = afterFirst.slice(closingIdx + `
6741
+ ${FRONTMATTER_DELIMITER}
6742
+ `.length);
6743
+ let name = "";
6744
+ let description23 = "";
6745
+ for (const line of frontmatterText.split("\n")) {
6746
+ const trimmed = line.trim();
6747
+ if (trimmed.length === 0) continue;
6748
+ const colonIdx = trimmed.indexOf(":");
6749
+ if (colonIdx === -1) continue;
6750
+ const key = trimmed.slice(0, colonIdx).trim();
6751
+ const value = trimmed.slice(colonIdx + 1).trim();
6752
+ if (key === "name") name = value;
6753
+ else if (key === "description") description23 = value;
6754
+ }
6755
+ if (name.length === 0) {
6756
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "name"');
6757
+ }
6758
+ if (description23.length === 0) {
6759
+ throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "description"');
6760
+ }
6761
+ return { name, description: description23, body };
6762
+ }
6763
+ function loadSkillsFromDir(skillsDir) {
6764
+ const entries = readdirSync6(skillsDir);
6765
+ const skills = [];
6766
+ for (const entry of entries) {
6767
+ const entryPath = join12(skillsDir, entry);
6768
+ let stat;
6769
+ try {
6770
+ stat = statSync5(entryPath);
6771
+ } catch {
6772
+ continue;
6773
+ }
6774
+ if (!stat.isDirectory()) continue;
6775
+ const skillFile = join12(entryPath, "SKILL.md");
6776
+ let raw;
6777
+ try {
6778
+ raw = readFileSync14(skillFile, "utf8");
6779
+ } catch {
6780
+ continue;
6781
+ }
6782
+ const { name, description: description23, body } = parseFrontmatter(raw, skillFile);
6783
+ skills.push({ name, description: description23, body, sourcePath: skillFile });
6784
+ }
6785
+ return [...skills].sort((a, b) => a.name.localeCompare(b.name));
6786
+ }
6787
+
6788
+ // src/tools/skills/skill-corpus.ts
6789
+ var LIST_TOOL_NAME = "vo_skill_list";
6790
+ var GET_TOOL_NAME = "vo_skill_get";
6791
+ 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.";
6792
+ 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.";
6793
+ var listInputSchema = {
6794
+ type: "object",
6795
+ properties: {
6796
+ refresh: {
6797
+ type: "boolean",
6798
+ description: "Re-scan the skills directory instead of using the cached corpus."
6799
+ }
6800
+ },
6801
+ required: []
6802
+ };
6803
+ var getInputSchema = {
6804
+ type: "object",
6805
+ properties: {
6806
+ name: {
6807
+ type: "string",
6808
+ description: "Skill name exactly as returned by vo_skill_list."
6809
+ }
6810
+ },
6811
+ required: ["name"]
6812
+ };
6813
+ var MAX_WALK_UP_LEVELS = 8;
6814
+ var cachedCorpus = null;
6815
+ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
6816
+ const override = env.VO_SKILLS_DIR;
6817
+ if (typeof override === "string" && override.length > 0) {
6818
+ const abs = isAbsolute(override) ? override : resolve2(startDir, override);
6819
+ return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
6820
+ }
6821
+ let dir = resolve2(startDir);
6822
+ for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
6823
+ const candidate = join13(dir, ".claude", "skills");
6824
+ if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
6825
+ const parent = dirname5(dir);
6826
+ if (parent === dir) break;
6827
+ dir = parent;
6828
+ }
6829
+ return null;
6830
+ }
6831
+ function loadCorpus() {
6832
+ const skillsDir = resolveSkillsDir();
6833
+ if (skillsDir === null) {
6834
+ return {
6835
+ skills: [],
6836
+ skillsDir: null,
6837
+ unavailableReason: "No skills directory found. Set VO_SKILLS_DIR or run inside a repo with .claude/skills."
6838
+ };
6839
+ }
6840
+ try {
6841
+ return { skills: loadSkillsFromDir(skillsDir), skillsDir, unavailableReason: null };
6842
+ } catch (err) {
6843
+ const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
6844
+ return { skills: [], skillsDir, unavailableReason: message };
6845
+ }
6846
+ }
6847
+ function getCorpus(refresh) {
6848
+ if (refresh || cachedCorpus === null) {
6849
+ cachedCorpus = loadCorpus();
6850
+ }
6851
+ return cachedCorpus;
6852
+ }
6853
+ async function handleSkillList(_deps, rawInput) {
6854
+ const input = rawInput ?? {};
6855
+ const refresh = input.refresh === true;
6856
+ const corpus = getCorpus(refresh);
6857
+ return jsonContent({
6858
+ corpus_available: corpus.unavailableReason === null,
6859
+ skills_dir: corpus.skillsDir,
6860
+ unavailable_reason: corpus.unavailableReason,
6861
+ skill_count: corpus.skills.length,
6862
+ skills: corpus.skills.map((s) => ({ name: s.name, description: s.description }))
6863
+ });
6864
+ }
6865
+ async function handleSkillGet(_deps, rawInput) {
6866
+ const input = rawInput ?? {};
6867
+ if (typeof input.name !== "string" || input.name.trim().length === 0) {
6868
+ throw invalidParams(GET_TOOL_NAME, 'input field "name" (non-empty string) is required');
6869
+ }
6870
+ const requested = input.name.trim();
6871
+ const corpus = getCorpus(false);
6872
+ if (corpus.unavailableReason !== null) {
6873
+ return jsonContent({
6874
+ corpus_available: false,
6875
+ unavailable_reason: corpus.unavailableReason,
6876
+ skill: null
6877
+ });
6878
+ }
6879
+ const skill = corpus.skills.find((s) => s.name === requested);
6880
+ if (skill === void 0) {
6881
+ throw invalidParams(
6882
+ GET_TOOL_NAME,
6883
+ `unknown skill "${requested}". Known skills: ${corpus.skills.map((s) => s.name).join(", ")}`
6884
+ );
6885
+ }
6886
+ return jsonContent({
6887
+ corpus_available: true,
6888
+ skill: {
6889
+ name: skill.name,
6890
+ description: skill.description,
6891
+ instructions: skill.body,
6892
+ source_path: skill.sourcePath
6893
+ }
6894
+ });
6895
+ }
6896
+
5100
6897
  // src/server.ts
5101
6898
  function buildToolRegistry() {
5102
6899
  return {
@@ -5275,11 +7072,75 @@ function buildToolRegistry() {
5275
7072
  inputSchema: inputSchema22
5276
7073
  },
5277
7074
  handler: handleSyncConfig
7075
+ },
7076
+ [UPSERT_TOOL_NAME]: {
7077
+ definition: {
7078
+ name: UPSERT_TOOL_NAME,
7079
+ description: upsertDescription,
7080
+ inputSchema: upsertInputSchema
7081
+ },
7082
+ handler: handlePrivateKnowledgeUpsert
7083
+ },
7084
+ [CONTEXT_TOOL_NAME]: {
7085
+ definition: {
7086
+ name: CONTEXT_TOOL_NAME,
7087
+ description: contextDescription,
7088
+ inputSchema: contextInputSchema
7089
+ },
7090
+ handler: handlePrivateKnowledgeContext
7091
+ },
7092
+ [INVALIDATE_TOOL_NAME]: {
7093
+ definition: {
7094
+ name: INVALIDATE_TOOL_NAME,
7095
+ description: invalidateDescription,
7096
+ inputSchema: invalidateInputSchema
7097
+ },
7098
+ handler: handlePrivateKnowledgeInvalidate
7099
+ },
7100
+ [STALE_TOOL_NAME]: {
7101
+ definition: {
7102
+ name: STALE_TOOL_NAME,
7103
+ description: staleDescription,
7104
+ inputSchema: staleInputSchema
7105
+ },
7106
+ handler: handlePrivateKnowledgeStale
7107
+ },
7108
+ [POST_TOOL_NAME]: {
7109
+ definition: {
7110
+ name: POST_TOOL_NAME,
7111
+ description: postDescription,
7112
+ inputSchema: postInputSchema
7113
+ },
7114
+ handler: handleHqWhiteboardPost
7115
+ },
7116
+ [READ_TOOL_NAME]: {
7117
+ definition: {
7118
+ name: READ_TOOL_NAME,
7119
+ description: readDescription,
7120
+ inputSchema: readInputSchema
7121
+ },
7122
+ handler: handleHqWhiteboardRead
7123
+ },
7124
+ [LIST_TOOL_NAME]: {
7125
+ definition: {
7126
+ name: LIST_TOOL_NAME,
7127
+ description: listDescription,
7128
+ inputSchema: listInputSchema
7129
+ },
7130
+ handler: handleSkillList
7131
+ },
7132
+ [GET_TOOL_NAME]: {
7133
+ definition: {
7134
+ name: GET_TOOL_NAME,
7135
+ description: getDescription,
7136
+ inputSchema: getInputSchema
7137
+ },
7138
+ handler: handleSkillGet
5278
7139
  }
5279
7140
  };
5280
7141
  }
5281
7142
  function createServer(options) {
5282
- const sessionId = options.sessionId ?? randomUUID2();
7143
+ const sessionId = options.sessionId ?? randomUUID3();
5283
7144
  const mode = createLocalMode();
5284
7145
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
5285
7146
  const server = new Server(
@@ -5331,9 +7192,9 @@ function listToolNames() {
5331
7192
  }
5332
7193
 
5333
7194
  // src/cache/sqlite-cache.ts
5334
- import { createHash as createHash3 } from "node:crypto";
5335
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync5 } from "node:fs";
5336
- import { dirname as dirname5 } from "node:path";
7195
+ import { createHash as createHash4 } from "node:crypto";
7196
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
7197
+ import { dirname as dirname6 } from "node:path";
5337
7198
  import { DatabaseSync } from "node:sqlite";
5338
7199
 
5339
7200
  // src/cache/canonicalize.ts
@@ -5378,7 +7239,7 @@ function normalizeString(s) {
5378
7239
  function createSqliteCache(options) {
5379
7240
  const fileBacked = options.dbPath !== ":memory:";
5380
7241
  if (fileBacked) {
5381
- mkdirSync5(dirname5(options.dbPath), { recursive: true, mode: 448 });
7242
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
5382
7243
  }
5383
7244
  const versionNamespace = options.cacheVersionNamespace ?? "";
5384
7245
  const db = new DatabaseSync(options.dbPath);
@@ -5409,7 +7270,7 @@ function createSqliteCache(options) {
5409
7270
  return {
5410
7271
  keyFor(toolName, input, opts) {
5411
7272
  const canonical = canonicalize(input, opts);
5412
- const hash = createHash3("sha256");
7273
+ const hash = createHash4("sha256");
5413
7274
  if (versionNamespace.length > 0) {
5414
7275
  hash.update(versionNamespace);
5415
7276
  hash.update("|");
@@ -5501,7 +7362,7 @@ function createStubRatchetClient() {
5501
7362
  let m;
5502
7363
  while ((m = pat.regex.exec(req.source)) !== null) {
5503
7364
  findings.push({
5504
- line_excerpt: clip(m[0], 80),
7365
+ line_excerpt: clip2(m[0], 80),
5505
7366
  severity: pat.severity,
5506
7367
  code: pat.code,
5507
7368
  message: pat.message
@@ -5533,7 +7394,7 @@ function createStubRatchetClient() {
5533
7394
  }
5534
7395
  };
5535
7396
  }
5536
- function clip(s, n) {
7397
+ function clip2(s, n) {
5537
7398
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
5538
7399
  }
5539
7400
  function buildSummary2(args) {
@@ -5562,7 +7423,61 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
5562
7423
  }
5563
7424
 
5564
7425
  // src/consensus/engine-client.ts
5565
- import { randomUUID as randomUUID3 } from "node:crypto";
7426
+ import { randomUUID as randomUUID4 } from "node:crypto";
7427
+
7428
+ // src/consensus/meta-model-caller.ts
7429
+ var META_CONSENSUS_MODEL = "muse-spark-1.1";
7430
+ function createMetaModelCaller(options = {}) {
7431
+ void options;
7432
+ return async function callMetaWithMetrics2() {
7433
+ throw new Error(
7434
+ "Muse Spark direct consensus is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
7435
+ );
7436
+ };
7437
+ }
7438
+ var callMetaWithMetrics = createMetaModelCaller();
7439
+
7440
+ // src/consensus/consensus-panel.ts
7441
+ var VO_MCP_CONSENSUS_PANEL = {
7442
+ // claude-opus-5 (2026-07-24). Opus 4.7 was STRICTLY DOMINATED, not merely old:
7443
+ // Opus 5 is $5/$25 per MTok vs Opus 4.7's $15/$75 — a 3x cost cut on this slot,
7444
+ // corroborated by our own catalog (constants/pricing/sciencePricing.ts prices
7445
+ // claude-opus-5 at 0.010 vs claude-opus-4-7 at 0.030) — AND the same 2026-07-24
7446
+ // release note REMOVED fast mode from Opus 4.7 outright: `speed: "fast"` now
7447
+ // returns an error there rather than degrading, unlike the Opus 4.6 removal.
7448
+ // Verified served: GET /v1/models/claude-opus-5 -> HTTP 200 (2026-07-30).
7449
+ //
7450
+ // Claude-5 API safety checked before this swap: Opus 5 rejects `temperature` /
7451
+ // `top_p` / `top_k` and manual `thinking.budget_tokens` with HTTP 400. Neither
7452
+ // the consensus-engine Anthropic adapter nor functions-shared `callAnthropic`
7453
+ // sends any of them, and buildAdaptiveThinking emits `thinking: {type:'adaptive'}`
7454
+ // (the supported form) — so this swap cannot 400.
7455
+ anthropic: "claude-opus-5",
7456
+ // gpt-5.6-terra (GA 2026-07-09; −20% price cut 2026-07-30). NOTE the real IDs
7457
+ // are tiered — `gpt-5.6-sol` / `-terra` / `-luna`; there is NO bare `gpt-5.6`
7458
+ // alias (verified against the served model list, 2026-07-30). Terra is the
7459
+ // cost/capability balance point and the right default for a judgment panel;
7460
+ // Sol is available if verdict quality ever needs it.
7461
+ openai: "gpt-5.6-terra",
7462
+ // gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
7463
+ // callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
7464
+ // Flash is also ~10x cheaper. 2026-06-02.
7465
+ google: "gemini-2.5-flash",
7466
+ deepseek: "deepseek-chat",
7467
+ // Muse Spark identity is owned by meta-model-caller.ts (single source of
7468
+ // truth for the meta slot); re-exported here so the panel stays complete.
7469
+ meta: META_CONSENSUS_MODEL
7470
+ };
7471
+ function getVoMcpConsensusPanel(panel = VO_MCP_CONSENSUS_PANEL) {
7472
+ for (const [provider, modelId] of Object.entries(panel)) {
7473
+ if (typeof modelId !== "string" || modelId.trim().length === 0) {
7474
+ throw new Error(
7475
+ `getVoMcpConsensusPanel: panel slot "${provider}" has a missing or blank model ID`
7476
+ );
7477
+ }
7478
+ }
7479
+ return panel;
7480
+ }
5566
7481
 
5567
7482
  // src/consensus/engine-options.ts
5568
7483
  var AGREEMENT_GATE_ENV_VAR = "VO_CONSENSUS_AGREEMENT_GATE";
@@ -5617,6 +7532,14 @@ function shadowEnabled(env) {
5617
7532
  const norm = raw.trim().toLowerCase();
5618
7533
  return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
5619
7534
  }
7535
+ var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
7536
+ function resolveMinResponders(env) {
7537
+ const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
7538
+ if (raw === void 0 || raw.trim() === "") return 2;
7539
+ const parsed = Number.parseInt(raw.trim(), 10);
7540
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
7541
+ return parsed;
7542
+ }
5620
7543
  function mapShadowSynthesis(s) {
5621
7544
  if (s === void 0) return void 0;
5622
7545
  return {
@@ -5766,9 +7689,11 @@ function createEngineConsensusClient(options) {
5766
7689
  ...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
5767
7690
  ...options.env !== void 0 ? { env: options.env } : {}
5768
7691
  });
7692
+ const minResponders = resolveMinResponders(options.env);
5769
7693
  const engineOptions = {
5770
7694
  panel,
5771
7695
  ...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
7696
+ ...minResponders !== void 0 ? { min_responders: minResponders } : {},
5772
7697
  ...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
5773
7698
  // Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
5774
7699
  // Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
@@ -5821,8 +7746,13 @@ function createEngineConsensusClient(options) {
5821
7746
  synthesized_verdict: response.synthesized_verdict,
5822
7747
  per_model_verdicts: response.per_model_verdicts,
5823
7748
  degraded: response.degraded,
7749
+ ...response.quorum_failed === true ? { quorum_failed: true } : {},
5824
7750
  duration_ms: response.duration_ms,
5825
7751
  engine_version: response.engine_version,
7752
+ // Cumulative cross-round inference usage (B44-3). Absent when no panel
7753
+ // member reported usage; forwarded verbatim — the aggregator prefers it
7754
+ // over summing final-round verdicts (which under-reports deliberation).
7755
+ ...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
5826
7756
  // Phase 2 Lane D-1 — forward escalation signal when present. The
5827
7757
  // source-grounded layer's own escalation (from the citation grade)
5828
7758
  // takes precedence when set, else the synthesizer's.
@@ -5832,6 +7762,10 @@ function createEngineConsensusClient(options) {
5832
7762
  ...mapFanOutDiagnostics(response.fan_out_diagnostics) !== void 0 ? { fan_out_diagnostics: mapFanOutDiagnostics(response.fan_out_diagnostics) } : {},
5833
7763
  // Stage A7-shadow — adaptive-vs-incumbent comparison (PII-free; present iff shadow on).
5834
7764
  ...mapShadowSynthesis(response.shadow_synthesis) !== void 0 ? { shadow_synthesis: mapShadowSynthesis(response.shadow_synthesis) } : {},
7765
+ // Critique-uptake (2026-07-20 red-team fix) — verifier-critique
7766
+ // visibility report; previously computed by the engine on every
7767
+ // call but dropped at this boundary.
7768
+ ...response.critique_uptake !== void 0 ? { critique_uptake: response.critique_uptake } : {},
5835
7769
  // Source-grounded additive outputs (Tier-4 features).
5836
7770
  ...useSourceGrounded ? { source_grounded: true } : {},
5837
7771
  ...sourceExtras?.citation_grade !== void 0 ? { citation_grade: sourceExtras.citation_grade } : {},
@@ -5847,25 +7781,12 @@ function createEngineConsensusClient(options) {
5847
7781
  }
5848
7782
  };
5849
7783
  }
5850
- var DEFAULT_MODELS = {
5851
- // These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
5852
- // intent — current production model ids. Per handoff §C-3 these MUST come
5853
- // from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
5854
- // for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
5855
- anthropic: "claude-opus-4-7",
5856
- openai: "gpt-5",
5857
- // gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
5858
- // callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
5859
- // Flash is also ~10x cheaper. 2026-06-02.
5860
- google: "gemini-2.5-flash",
5861
- deepseek: "deepseek-chat"
5862
- };
7784
+ var DEFAULT_MODELS = getVoMcpConsensusPanel();
5863
7785
  function probeProviders(env = process.env) {
5864
7786
  const out = [];
5865
7787
  if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
5866
7788
  if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
5867
7789
  if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
5868
- if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
5869
7790
  return out;
5870
7791
  }
5871
7792
  async function loadFactoryAndCallers(injectedEngine, injectedShared) {
@@ -5910,14 +7831,12 @@ async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
5910
7831
  const callerByProvider = {
5911
7832
  anthropic: loaded.shared.callAnthropicWithMetrics,
5912
7833
  openai: loaded.shared.callOpenAIWithMetrics,
5913
- google: loaded.shared.callGeminiWithMetrics,
5914
- deepseek: loaded.shared.callDeepSeekWithMetrics
7834
+ google: loaded.shared.callGeminiWithMetrics
5915
7835
  };
5916
7836
  const modelByProvider = {
5917
7837
  anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
5918
7838
  openai: options.models?.openai ?? DEFAULT_MODELS.openai,
5919
- google: options.models?.google ?? DEFAULT_MODELS.google,
5920
- deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek
7839
+ google: options.models?.google ?? DEFAULT_MODELS.google
5921
7840
  };
5922
7841
  const panel = [];
5923
7842
  for (const p of providers) {