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

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
@@ -502,7 +502,7 @@ __export(memory_knowledge_bridge_exports, {
502
502
  extractMemoryTitle: () => extractMemoryTitle,
503
503
  upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
504
504
  });
505
- import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
505
+ import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
506
506
  function extractMemoryTitle(fileName, content) {
507
507
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
508
508
  if (frontmatter) {
@@ -517,7 +517,7 @@ async function upsertMemoryFilesAsKnowledge(options) {
517
517
  const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
518
518
  let files;
519
519
  try {
520
- if (!existsSync7(memoryDir)) {
520
+ if (!existsSync8(memoryDir)) {
521
521
  return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
522
522
  }
523
523
  files = readdirSync5(memoryDir).filter(
@@ -807,24 +807,24 @@ function defaultOverridePath() {
807
807
  return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
808
808
  }
809
809
  function loadTenantOverride(opts = {}) {
810
- const path3 = opts.path ?? defaultOverridePath();
811
- if (!existsSync2(path3)) {
810
+ const path4 = opts.path ?? defaultOverridePath();
811
+ if (!existsSync2(path4)) {
812
812
  return { override: null, source_path: null };
813
813
  }
814
- const raw = readFileSync2(path3, "utf8");
814
+ const raw = readFileSync2(path4, "utf8");
815
815
  let parsed;
816
816
  try {
817
817
  parsed = JSON.parse(raw);
818
818
  } catch (err) {
819
819
  const m = err instanceof Error ? err.message : String(err);
820
- throw new Error(`vo-arch-defaults: invalid JSON in override ${path3}: ${m}`, { cause: err });
820
+ throw new Error(`vo-arch-defaults: invalid JSON in override ${path4}: ${m}`, { cause: err });
821
821
  }
822
822
  try {
823
823
  const override = parseOverride(parsed);
824
- return { override, source_path: path3 };
824
+ return { override, source_path: path4 };
825
825
  } catch (err) {
826
826
  const m = err instanceof Error ? err.message : String(err);
827
- throw new Error(`vo-arch-defaults: override schema validation failed for ${path3}: ${m}`, { cause: err });
827
+ throw new Error(`vo-arch-defaults: override schema validation failed for ${path4}: ${m}`, { cause: err });
828
828
  }
829
829
  }
830
830
 
@@ -954,9 +954,9 @@ function globToRegExp(glob) {
954
954
  }
955
955
  return new RegExp("^" + out + "$");
956
956
  }
957
- function matchesAnyGlob(path3, globs) {
957
+ function matchesAnyGlob(path4, globs) {
958
958
  for (const g of globs) {
959
- if (globToRegExp(g).test(path3)) return true;
959
+ if (globToRegExp(g).test(path4)) return true;
960
960
  }
961
961
  return false;
962
962
  }
@@ -4142,10 +4142,10 @@ Produce the JSON dispatch plan now.`;
4142
4142
  init_auth_token_source();
4143
4143
  init_credential_store();
4144
4144
  var AdminCallableError = class extends Error {
4145
- constructor(status, path3, message) {
4145
+ constructor(status, path4, message) {
4146
4146
  super(message);
4147
4147
  this.status = status;
4148
- this.path = path3;
4148
+ this.path = path4;
4149
4149
  this.name = "AdminCallableError";
4150
4150
  }
4151
4151
  status;
@@ -5140,7 +5140,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
5140
5140
  import { spawn } from "node:child_process";
5141
5141
  import { homedir as homedir5 } from "node:os";
5142
5142
  import { join as join7 } from "node:path";
5143
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5143
+ import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
5144
5144
 
5145
5145
  // src/swarm/tier-binding.ts
5146
5146
  var SWARM_TIERS = Object.freeze([
@@ -5346,10 +5346,10 @@ function sanitizeSwarmId(raw) {
5346
5346
  return id;
5347
5347
  }
5348
5348
  var CEILING_FILE = "ceiling.json";
5349
- function createExclusive(path3, contents) {
5349
+ function createExclusive(path4, contents) {
5350
5350
  let fd;
5351
5351
  try {
5352
- fd = openSync(path3, "wx");
5352
+ fd = openSync(path4, "wx");
5353
5353
  } catch {
5354
5354
  return false;
5355
5355
  }
@@ -5364,18 +5364,18 @@ function capToCents(cap) {
5364
5364
  return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
5365
5365
  }
5366
5366
  function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
5367
- const path3 = join6(swarmDir, CEILING_FILE);
5367
+ const path4 = join6(swarmDir, CEILING_FILE);
5368
5368
  const head = JSON.stringify({
5369
5369
  ceiling: proposedCeiling,
5370
5370
  cap_cents: proposedCapCents,
5371
5371
  recorded_at: nowIso
5372
5372
  });
5373
- if (createExclusive(path3, head)) {
5373
+ if (createExclusive(path4, head)) {
5374
5374
  return { ceiling: proposedCeiling, capCents: proposedCapCents };
5375
5375
  }
5376
5376
  let parsed;
5377
5377
  try {
5378
- parsed = JSON.parse(readFileSync6(path3, "utf8"));
5378
+ parsed = JSON.parse(readFileSync6(path4, "utf8"));
5379
5379
  } catch {
5380
5380
  return null;
5381
5381
  }
@@ -5455,6 +5455,288 @@ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso })
5455
5455
  };
5456
5456
  };
5457
5457
 
5458
+ // src/swarm/spawn-plan.ts
5459
+ function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
5460
+ const rawBinding = env[SWARM_TIER_BINDING_ENV];
5461
+ const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
5462
+ if (!hasBinding) {
5463
+ const explicit = input.agent?.trim();
5464
+ const resolved2 = resolveSuccessorLaunch({ agent: explicit || "claude", maxTurns: input.max_turns, platform });
5465
+ if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
5466
+ return {
5467
+ ok: true,
5468
+ bin: resolved2.bin,
5469
+ args: resolved2.args,
5470
+ agent: resolved2.agent,
5471
+ tier: "unbound",
5472
+ bound: false,
5473
+ env: {},
5474
+ slot: null,
5475
+ capUsd: null,
5476
+ capRemainingUsd: null
5477
+ };
5478
+ }
5479
+ const binding = inheritSwarmTierBinding(env, nowIso);
5480
+ const admission = admitSubagentSpawn(binding);
5481
+ if (!admission.allowed) {
5482
+ return { ok: false, reason: admission.reason, tier: binding.tier };
5483
+ }
5484
+ const agentRefusal = agentBindingRefusal(binding, input.agent);
5485
+ if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
5486
+ const resolved = resolveSuccessorLaunch({
5487
+ agent: binding.agent,
5488
+ maxTurns: input.max_turns,
5489
+ platform
5490
+ });
5491
+ if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
5492
+ const slot = claim({
5493
+ swarmId: binding.swarm_id,
5494
+ proposedCeiling: binding.subagent_budget,
5495
+ // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
5496
+ // child's cap is DEBITED from it below, not recomputed from this binding.
5497
+ proposedCapUsd: binding.spend_cap_usd,
5498
+ dir: resolveLedgerDir(env),
5499
+ nowIso
5500
+ });
5501
+ if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
5502
+ return {
5503
+ ok: true,
5504
+ bin: resolved.bin,
5505
+ args: resolved.args,
5506
+ agent: resolved.agent,
5507
+ tier: binding.tier,
5508
+ bound: true,
5509
+ // Re-export the same TIER with a DECREMENTED budget and the spend cap the
5510
+ // ledger just DEBITED. Exporting the binding verbatim (what this did before
5511
+ // #9312) meant the child re-read the full budget and every generation
5512
+ // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
5513
+ // bounded a chain but not a tree: three siblings each re-halved the parent's
5514
+ // untouched $50 and walked away with $75 between them.
5515
+ env: childBindingEnvFragment(binding, slot.capUsd),
5516
+ slot: slot.slot,
5517
+ capUsd: slot.capUsd,
5518
+ capRemainingUsd: slot.capRemainingUsd
5519
+ };
5520
+ }
5521
+
5522
+ // ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
5523
+ import { existsSync as existsSync4, realpathSync } from "node:fs";
5524
+ import { win32 as path3 } from "node:path";
5525
+ import { spawnSync as spawnSync2 } from "node:child_process";
5526
+ var NATIVE_CLAUDE_PARTS = [
5527
+ "node_modules",
5528
+ "@anthropic-ai",
5529
+ "claude-code",
5530
+ "bin",
5531
+ "claude.exe"
5532
+ ];
5533
+ function pathValue(env) {
5534
+ for (const key of ["Path", "PATH", "path"]) {
5535
+ if (typeof env?.[key] === "string") return env[key];
5536
+ }
5537
+ return "";
5538
+ }
5539
+ function cleanPathSegment(value) {
5540
+ const trimmed = String(value || "").trim();
5541
+ return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
5542
+ }
5543
+ function envValue(env, name) {
5544
+ const exact = env?.[name];
5545
+ if (typeof exact === "string") return exact.trim();
5546
+ const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
5547
+ return typeof env?.[key] === "string" ? env[key].trim() : "";
5548
+ }
5549
+ function userClaudeCandidates(bin, env) {
5550
+ if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
5551
+ const userProfile = envValue(env, "USERPROFILE");
5552
+ const appData = envValue(env, "APPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Roaming") : "");
5553
+ const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Local") : "");
5554
+ const candidates = [];
5555
+ if (appData) {
5556
+ const npmBin = path3.join(appData, "npm");
5557
+ candidates.push(
5558
+ path3.join(npmBin, "claude.exe"),
5559
+ path3.join(npmBin, "claude.cmd"),
5560
+ path3.join(npmBin, "claude.ps1"),
5561
+ path3.join(npmBin, "claude"),
5562
+ path3.join(npmBin, ...NATIVE_CLAUDE_PARTS)
5563
+ );
5564
+ }
5565
+ if (userProfile) candidates.push(path3.join(userProfile, ".local", "bin", "claude.exe"));
5566
+ if (localAppData) {
5567
+ candidates.push(
5568
+ path3.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
5569
+ path3.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
5570
+ );
5571
+ }
5572
+ return candidates;
5573
+ }
5574
+ function pathCandidates(bin, env) {
5575
+ if (path3.isAbsolute(bin) || /[\\/]/u.test(bin)) {
5576
+ return [path3.resolve(bin)];
5577
+ }
5578
+ const extension = path3.extname(bin);
5579
+ const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path3.join(directory, bin)] : [
5580
+ path3.join(directory, `${bin}.exe`),
5581
+ path3.join(directory, `${bin}.cmd`),
5582
+ path3.join(directory, `${bin}.ps1`),
5583
+ path3.join(directory, bin)
5584
+ ]);
5585
+ const seen = /* @__PURE__ */ new Set();
5586
+ return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
5587
+ const key = candidate.toLowerCase();
5588
+ if (seen.has(key)) return false;
5589
+ seen.add(key);
5590
+ return true;
5591
+ });
5592
+ }
5593
+ function canonicalExistingPath(candidate, exists, canonicalize2) {
5594
+ if (!exists(candidate)) return null;
5595
+ try {
5596
+ return canonicalize2(candidate);
5597
+ } catch {
5598
+ return null;
5599
+ }
5600
+ }
5601
+ function resolveWindowsClaudeExecutable({
5602
+ bin = "claude",
5603
+ env = process.env,
5604
+ exists = existsSync4,
5605
+ canonicalize: canonicalize2 = realpathSync
5606
+ } = {}) {
5607
+ const requested = String(bin || "").trim();
5608
+ if (!requested || requested.includes("\0")) {
5609
+ throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
5610
+ }
5611
+ for (const candidate of pathCandidates(requested, env)) {
5612
+ const found = canonicalExistingPath(candidate, exists, canonicalize2);
5613
+ if (!found) continue;
5614
+ if (path3.extname(found).toLowerCase() === ".exe") return found;
5615
+ const native = path3.join(path3.dirname(found), ...NATIVE_CLAUDE_PARTS);
5616
+ const resolvedNative = canonicalExistingPath(native, exists, canonicalize2);
5617
+ if (resolvedNative) return resolvedNative;
5618
+ }
5619
+ const error = new Error(
5620
+ `Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
5621
+ );
5622
+ error.code = "ENOENT";
5623
+ throw error;
5624
+ }
5625
+
5626
+ // src/swarm/successor-windows-exe.ts
5627
+ var resolveWindowsClaudeExe = (bin, env) => resolveWindowsClaudeExecutable({ bin, env });
5628
+ function resolveNativeWindowsExecutable(bin, env = process.env, resolve3 = resolveWindowsClaudeExe) {
5629
+ try {
5630
+ return { ok: true, bin: resolve3(bin, env) };
5631
+ } catch (error) {
5632
+ const message = error instanceof Error ? error.message : String(error);
5633
+ return {
5634
+ ok: false,
5635
+ reason: `could not resolve a native Windows executable for '${bin}': ${message}`
5636
+ };
5637
+ }
5638
+ }
5639
+
5640
+ // src/swarm/successor-liveness.ts
5641
+ import { statSync as statSync3 } from "node:fs";
5642
+ var DEFAULT_EARLY_EXIT_SEC = 10;
5643
+ var DEFAULT_NO_OUTPUT_SEC = 0;
5644
+ var DEFAULT_POLL_MS = 200;
5645
+ var KILL_ESCALATION_MS = 2e3;
5646
+ function positiveSeconds(raw, fallback) {
5647
+ if (raw === void 0) return fallback;
5648
+ const n = Number(raw.trim());
5649
+ return Number.isFinite(n) && n > 0 ? n : fallback;
5650
+ }
5651
+ function resolveLivenessConfigFromEnv(env = process.env, explicitOverride = {}) {
5652
+ const earlyExitMs = explicitOverride.earlyExitMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_EXIT_CHECK_SEC"], DEFAULT_EARLY_EXIT_SEC) * 1e3;
5653
+ const noOutputMs = explicitOverride.noOutputMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_OUTPUT_CHECK_SEC"], DEFAULT_NO_OUTPUT_SEC) * 1e3;
5654
+ return { earlyExitMs, noOutputMs };
5655
+ }
5656
+ function defaultStatLogBytes(path4) {
5657
+ try {
5658
+ return statSync3(path4).size;
5659
+ } catch {
5660
+ return 0;
5661
+ }
5662
+ }
5663
+ function defaultKillChild(child) {
5664
+ try {
5665
+ child.kill("SIGTERM");
5666
+ } catch {
5667
+ }
5668
+ const escalation = setTimeout(() => {
5669
+ try {
5670
+ child.kill("SIGKILL");
5671
+ } catch {
5672
+ }
5673
+ }, KILL_ESCALATION_MS);
5674
+ escalation.unref();
5675
+ }
5676
+ function checkSuccessorLiveness(child, logPath, config, deps = {}) {
5677
+ const statLogBytes = deps.statLogBytes ?? defaultStatLogBytes;
5678
+ const now = deps.now ?? Date.now;
5679
+ const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS;
5680
+ const killChild = deps.killChild ?? defaultKillChild;
5681
+ const outputGateEnabled = Number.isFinite(config.noOutputMs) && config.noOutputMs > 0;
5682
+ const startedAt = now();
5683
+ return new Promise((resolve3) => {
5684
+ let settled = false;
5685
+ let pollTimer = null;
5686
+ const onExit = (code, signal) => {
5687
+ const elapsedMs = now() - startedAt;
5688
+ finish({
5689
+ ok: false,
5690
+ reason: `child_exited_early: exit code ${code ?? "null"} signal ${signal ?? "none"} after ${elapsedMs}ms`,
5691
+ detail: { exitCode: code, signal, elapsedMs }
5692
+ });
5693
+ };
5694
+ const cleanup = () => {
5695
+ if (pollTimer !== null) clearInterval(pollTimer);
5696
+ child.off?.("exit", onExit);
5697
+ };
5698
+ const finish = (result) => {
5699
+ if (settled) return;
5700
+ settled = true;
5701
+ cleanup();
5702
+ if (!result.ok) {
5703
+ try {
5704
+ killChild(child);
5705
+ } catch {
5706
+ }
5707
+ }
5708
+ resolve3(result);
5709
+ };
5710
+ if (typeof child.exitCode === "number" || typeof child.signalCode === "string" && child.signalCode.length > 0) {
5711
+ finish({
5712
+ ok: false,
5713
+ reason: `child_exited_early: exit code ${child.exitCode ?? "null"} signal ${child.signalCode ?? "none"} before the liveness watch attached`,
5714
+ detail: { exitCode: child.exitCode ?? null, signal: child.signalCode ?? null, elapsedMs: 0 }
5715
+ });
5716
+ return;
5717
+ }
5718
+ child.on("exit", onExit);
5719
+ const tick = () => {
5720
+ if (settled) return;
5721
+ const elapsedMs = now() - startedAt;
5722
+ const outputSeen = outputGateEnabled ? statLogBytes(logPath) > 0 : true;
5723
+ if (outputSeen && elapsedMs >= config.earlyExitMs) {
5724
+ finish({ ok: true });
5725
+ return;
5726
+ }
5727
+ if (outputGateEnabled && !outputSeen && elapsedMs >= config.noOutputMs) {
5728
+ finish({
5729
+ ok: false,
5730
+ reason: `no_output: log stayed empty for ${elapsedMs}ms (limit ${config.noOutputMs}ms)`,
5731
+ detail: { elapsedMs, logPath }
5732
+ });
5733
+ }
5734
+ };
5735
+ pollTimer = setInterval(tick, pollIntervalMs);
5736
+ tick();
5737
+ });
5738
+ }
5739
+
5458
5740
  // src/tools/session/spawn-successor.ts
5459
5741
  var TOOL_NAME20 = "vo_spawn_successor";
5460
5742
  var MAX_HANDOFF_BYTES = 64e3;
@@ -5486,7 +5768,7 @@ var inputSchema20 = {
5486
5768
  additionalProperties: false
5487
5769
  };
5488
5770
  var RETIRED_COUNTER_INPUT = "spawns_so_far";
5489
- 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.";
5771
+ 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/. Verifies the child is actually alive (survives an early-exit window; an optional log-output window is off by default) before reporting success. Returns {spawned, pid, log_path, handoff_path}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.";
5490
5772
  function isToolInput20(v) {
5491
5773
  if (typeof v !== "object" || v === null) return false;
5492
5774
  const o = v;
@@ -5504,7 +5786,7 @@ function retiredCounterRefusal(v) {
5504
5786
  }
5505
5787
  function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
5506
5788
  try {
5507
- 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);
5789
+ const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync4(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
5508
5790
  return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
5509
5791
  } catch {
5510
5792
  return null;
@@ -5542,97 +5824,14 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
5542
5824
  if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
5543
5825
  return lines.join("\n");
5544
5826
  }
5545
- function buildSuccessorArgs(maxTurns) {
5546
- const args = ["-p", "--permission-mode", "acceptEdits"];
5547
- if (Number.isInteger(maxTurns) && maxTurns > 0) {
5548
- args.push("--max-turns", String(maxTurns));
5549
- }
5550
- return args;
5551
- }
5552
- function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
5553
- const rawBinding = env[SWARM_TIER_BINDING_ENV];
5554
- const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
5555
- if (!hasBinding) {
5556
- const explicit = input.agent?.trim();
5557
- if (explicit) {
5558
- const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
5559
- if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
5560
- return {
5561
- ok: true,
5562
- bin: resolved2.bin,
5563
- args: resolved2.args,
5564
- agent: resolved2.agent,
5565
- tier: "unbound",
5566
- bound: false,
5567
- env: {},
5568
- slot: null,
5569
- capUsd: null,
5570
- capRemainingUsd: null
5571
- };
5572
- }
5573
- return {
5574
- ok: true,
5575
- bin: "claude",
5576
- args: buildSuccessorArgs(input.max_turns),
5577
- agent: "claude",
5578
- tier: "unbound",
5579
- bound: false,
5580
- env: {},
5581
- slot: null,
5582
- capUsd: null,
5583
- capRemainingUsd: null
5584
- };
5585
- }
5586
- const binding = inheritSwarmTierBinding(env, nowIso);
5587
- const admission = admitSubagentSpawn(binding);
5588
- if (!admission.allowed) {
5589
- return { ok: false, reason: admission.reason, tier: binding.tier };
5590
- }
5591
- const agentRefusal = agentBindingRefusal(binding, input.agent);
5592
- if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
5593
- const resolved = resolveSuccessorLaunch({
5594
- agent: binding.agent,
5595
- maxTurns: input.max_turns,
5596
- platform
5597
- });
5598
- if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
5599
- const slot = claim({
5600
- swarmId: binding.swarm_id,
5601
- proposedCeiling: binding.subagent_budget,
5602
- // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
5603
- // child's cap is DEBITED from it below, not recomputed from this binding.
5604
- proposedCapUsd: binding.spend_cap_usd,
5605
- dir: resolveLedgerDir(env),
5606
- nowIso
5607
- });
5608
- if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
5609
- return {
5610
- ok: true,
5611
- bin: resolved.bin,
5612
- args: resolved.args,
5613
- agent: resolved.agent,
5614
- tier: binding.tier,
5615
- bound: true,
5616
- // Re-export the same TIER with a DECREMENTED budget and the spend cap the
5617
- // ledger just DEBITED. Exporting the binding verbatim (what this did before
5618
- // #9312) meant the child re-read the full budget and every generation
5619
- // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
5620
- // bounded a chain but not a tree: three siblings each re-halved the parent's
5621
- // untouched $50 and walked away with $75 between them.
5622
- env: childBindingEnvFragment(binding, slot.capUsd),
5623
- slot: slot.slot,
5624
- capUsd: slot.capUsd,
5625
- capRemainingUsd: slot.capRemainingUsd
5626
- };
5627
- }
5628
- async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
5827
+ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
5629
5828
  const retired = retiredCounterRefusal(rawInput);
5630
5829
  if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
5631
5830
  if (!isToolInput20(rawInput)) {
5632
5831
  throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
5633
5832
  }
5634
5833
  const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
5635
- if (!handoffPath || !existsSync4(handoffPath)) {
5834
+ if (!handoffPath || !existsSync5(handoffPath)) {
5636
5835
  return jsonContent({
5637
5836
  tool: TOOL_NAME20,
5638
5837
  schema_version: 1,
@@ -5657,27 +5856,52 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
5657
5856
  }
5658
5857
  });
5659
5858
  }
5859
+ const platform = overrides.platform ?? process.platform;
5860
+ let resolvedBin = plan.bin;
5861
+ if (platform === "win32") {
5862
+ const resolution = resolveNativeWindowsExecutable(plan.bin, process.env, overrides.resolveWindowsExecutable);
5863
+ if (!resolution.ok) {
5864
+ return jsonContent({
5865
+ tool: TOOL_NAME20,
5866
+ schema_version: 1,
5867
+ payload: {
5868
+ spawned: false,
5869
+ reason: resolution.reason,
5870
+ agent: plan.agent,
5871
+ tier: plan.tier,
5872
+ handoff_path: handoffPath
5873
+ }
5874
+ });
5875
+ }
5876
+ resolvedBin = resolution.bin;
5877
+ }
5660
5878
  const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
5661
5879
  mkdirSync4(logDir, { recursive: true });
5662
5880
  const logPath = join7(logDir, `successor-${Date.now()}.log`);
5663
5881
  const logFd = openSync2(logPath, "a");
5664
- const child = spawnImpl(plan.bin, [...plan.args], {
5882
+ const child = spawnImpl(resolvedBin, [...plan.args], {
5665
5883
  cwd: rawInput.cwd?.trim() || process.cwd(),
5666
5884
  detached: true,
5667
5885
  stdio: ["pipe", logFd, logFd],
5668
- // Windows: the agent CLIs are .cmd shims they need a shell to resolve.
5669
- // The prompt goes via STDIN below, never argv, so the shell never sees it.
5670
- shell: process.platform === "win32",
5886
+ // Never a shell: `resolvedBin` is either the bare platform-neutral name
5887
+ // (POSIX, resolved by the OS via PATH + shebang) or the native win32 exe
5888
+ // resolved above — routing either through cmd.exe/sh is the extra layer
5889
+ // a detached, unref'd child can lose silently (2026-08-16 incident).
5890
+ shell: false,
5671
5891
  windowsHide: true,
5892
+ windowsVerbatimArguments: false,
5672
5893
  // Carry the SAME binding to the child. Without this the successor inherits
5673
5894
  // no tier and re-resolves its own — which is the split-payer defect one
5674
5895
  // generation down.
5675
5896
  ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
5676
5897
  });
5898
+ closeSync2(logFd);
5677
5899
  let spawnError = null;
5678
5900
  child.on("error", (e) => {
5679
5901
  spawnError = e.message;
5680
5902
  });
5903
+ child.stdin.on?.("error", () => {
5904
+ });
5681
5905
  try {
5682
5906
  child.stdin.write(prompt);
5683
5907
  child.stdin.end();
@@ -5685,10 +5909,40 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
5685
5909
  }
5686
5910
  child.unref();
5687
5911
  await new Promise((r) => setTimeout(r, 150));
5912
+ if (spawnError) {
5913
+ return jsonContent({
5914
+ tool: TOOL_NAME20,
5915
+ schema_version: 1,
5916
+ payload: {
5917
+ spawned: false,
5918
+ reason: `spawn failed: ${spawnError}`,
5919
+ agent: plan.agent,
5920
+ tier: plan.tier,
5921
+ handoff_path: handoffPath
5922
+ }
5923
+ });
5924
+ }
5925
+ const checkLiveness = overrides.checkLiveness ?? ((c, p) => checkSuccessorLiveness(c, p, resolveLivenessConfigFromEnv(process.env, overrides.livenessConfig), overrides.livenessDeps));
5926
+ const liveness = await checkLiveness(child, logPath);
5927
+ if (!liveness.ok) {
5928
+ return jsonContent({
5929
+ tool: TOOL_NAME20,
5930
+ schema_version: 1,
5931
+ payload: {
5932
+ spawned: false,
5933
+ reason: liveness.reason,
5934
+ pid: child.pid ?? null,
5935
+ log_path: logPath,
5936
+ agent: plan.agent,
5937
+ tier: plan.tier,
5938
+ handoff_path: handoffPath
5939
+ }
5940
+ });
5941
+ }
5688
5942
  return jsonContent({
5689
5943
  tool: TOOL_NAME20,
5690
5944
  schema_version: 1,
5691
- payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
5945
+ payload: {
5692
5946
  spawned: true,
5693
5947
  pid: child.pid ?? null,
5694
5948
  log_path: logPath,
@@ -5700,7 +5954,10 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
5700
5954
  // The debit, surfaced so an operator can reconcile a fan-out's spend
5701
5955
  // against the pool without reading the ledger directory by hand.
5702
5956
  ledger_cap_usd: plan.capUsd,
5703
- ledger_cap_remaining_usd: plan.capRemainingUsd
5957
+ ledger_cap_remaining_usd: plan.capRemainingUsd,
5958
+ // Additive (2026-08-17): true only once the child survived the
5959
+ // early-exit window (and the output window, when that gate is enabled).
5960
+ verified_alive: true
5704
5961
  }
5705
5962
  });
5706
5963
  }
@@ -5787,22 +6044,22 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
5787
6044
  }
5788
6045
 
5789
6046
  // src/tools/memory/sync-config.ts
5790
- import { existsSync as existsSync8 } from "node:fs";
6047
+ import { existsSync as existsSync9 } from "node:fs";
5791
6048
  import { homedir as homedir7 } from "node:os";
5792
6049
  import { join as join11 } from "node:path";
5793
6050
 
5794
6051
  // src/tools/memory/memory-sync-http.ts
5795
6052
  init_safe_memory_file();
5796
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
6053
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
5797
6054
 
5798
6055
  // src/tools/memory/sync-lock.ts
5799
- import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
6056
+ import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
5800
6057
  import { hostname } from "node:os";
5801
6058
  import { join as join8 } from "node:path";
5802
6059
  import { randomUUID as randomUUID2 } from "node:crypto";
5803
6060
 
5804
6061
  // src/tools/memory/sync-lock-liveness.ts
5805
- import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
6062
+ import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
5806
6063
  function defaultIsProcessAlive(pid) {
5807
6064
  try {
5808
6065
  process.kill(pid, 0);
@@ -5828,10 +6085,10 @@ function toPayload(parsed) {
5828
6085
  acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
5829
6086
  };
5830
6087
  }
5831
- function readLockRecord(path3) {
6088
+ function readLockRecord(path4) {
5832
6089
  let raw;
5833
6090
  try {
5834
- raw = readFileSync8(path3, "utf8");
6091
+ raw = readFileSync8(path4, "utf8");
5835
6092
  } catch {
5836
6093
  return null;
5837
6094
  }
@@ -5841,7 +6098,7 @@ function readLockRecord(path3) {
5841
6098
  return { raw, payload: null };
5842
6099
  }
5843
6100
  }
5844
- function lockAgeMs(record, path3, nowMs) {
6101
+ function lockAgeMs(record, path4, nowMs) {
5845
6102
  let startedMs = Number.NaN;
5846
6103
  if (record.payload) {
5847
6104
  if (Number.isFinite(record.payload.acquiredAtMs)) {
@@ -5852,7 +6109,7 @@ function lockAgeMs(record, path3, nowMs) {
5852
6109
  }
5853
6110
  if (!Number.isFinite(startedMs)) {
5854
6111
  try {
5855
- startedMs = statSync4(path3).mtimeMs;
6112
+ startedMs = statSync5(path4).mtimeMs;
5856
6113
  } catch {
5857
6114
  return null;
5858
6115
  }
@@ -5885,10 +6142,10 @@ var BACKOFF_FACTOR = 1.6;
5885
6142
  function positiveOr(value, fallback) {
5886
6143
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
5887
6144
  }
5888
- function createExclusive2(path3, contents) {
6145
+ function createExclusive2(path4, contents) {
5889
6146
  let fd;
5890
6147
  try {
5891
- fd = openSync3(path3, "wx");
6148
+ fd = openSync3(path4, "wx");
5892
6149
  } catch (err) {
5893
6150
  const code = err.code;
5894
6151
  return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
@@ -5896,37 +6153,37 @@ function createExclusive2(path3, contents) {
5896
6153
  try {
5897
6154
  writeFileSync4(fd, contents, "utf8");
5898
6155
  } catch (err) {
5899
- closeSync2(fd);
6156
+ closeSync3(fd);
5900
6157
  try {
5901
- unlinkSync2(path3);
6158
+ unlinkSync2(path4);
5902
6159
  } catch {
5903
6160
  }
5904
6161
  return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
5905
6162
  }
5906
- closeSync2(fd);
6163
+ closeSync3(fd);
5907
6164
  return { ok: true };
5908
6165
  }
5909
- function removeAbandoned(path3, expectedRaw) {
6166
+ function removeAbandoned(path4, expectedRaw) {
5910
6167
  let current;
5911
6168
  try {
5912
- current = readFileSync9(path3, "utf8");
6169
+ current = readFileSync9(path4, "utf8");
5913
6170
  } catch {
5914
6171
  return;
5915
6172
  }
5916
6173
  if (current !== expectedRaw) return;
5917
6174
  try {
5918
- unlinkSync2(path3);
6175
+ unlinkSync2(path4);
5919
6176
  } catch {
5920
6177
  }
5921
6178
  }
5922
- function makeRelease(path3, token) {
6179
+ function makeRelease(path4, token) {
5923
6180
  let released = false;
5924
6181
  return () => {
5925
6182
  if (released) return;
5926
6183
  released = true;
5927
6184
  let raw;
5928
6185
  try {
5929
- raw = readFileSync9(path3, "utf8");
6186
+ raw = readFileSync9(path4, "utf8");
5930
6187
  } catch {
5931
6188
  return;
5932
6189
  }
@@ -5938,7 +6195,7 @@ function makeRelease(path3, token) {
5938
6195
  }
5939
6196
  if (!stillOurs) return;
5940
6197
  try {
5941
- unlinkSync2(path3);
6198
+ unlinkSync2(path4);
5942
6199
  } catch {
5943
6200
  }
5944
6201
  };
@@ -5957,7 +6214,7 @@ async function acquireMemorySyncLock(options) {
5957
6214
  }));
5958
6215
  const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
5959
6216
  const thisHost = hostname();
5960
- const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
6217
+ const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
5961
6218
  if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
5962
6219
  const deadline = now() + waitMs;
5963
6220
  let backoffMs = INITIAL_BACKOFF_MS;
@@ -5973,30 +6230,30 @@ async function acquireMemorySyncLock(options) {
5973
6230
  acquiredAt: new Date(acquiredAtMs).toISOString(),
5974
6231
  acquiredAtMs
5975
6232
  };
5976
- const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
6233
+ const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
5977
6234
  `);
5978
6235
  if (created.ok) {
5979
- return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
6236
+ return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
5980
6237
  }
5981
6238
  if (!created.exists) {
5982
6239
  throw new Error(
5983
- `memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
6240
+ `memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
5984
6241
  );
5985
6242
  }
5986
- const record = readLockRecord(path3);
6243
+ const record = readLockRecord(path4);
5987
6244
  let reclaimed = false;
5988
6245
  if (record) {
5989
6246
  holderDescription = describeHolder(record);
5990
- const age = lockAgeMs(record, path3, now());
6247
+ const age = lockAgeMs(record, path4, now());
5991
6248
  if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
5992
6249
  tookOverFrom = record.payload;
5993
- removeAbandoned(path3, record.raw);
6250
+ removeAbandoned(path4, record.raw);
5994
6251
  reclaimed = true;
5995
6252
  }
5996
6253
  }
5997
6254
  if (now() >= deadline) {
5998
6255
  throw new Error(
5999
- `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.`
6256
+ `memory sync lock ${path4} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
6000
6257
  );
6001
6258
  }
6002
6259
  if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
@@ -6148,7 +6405,7 @@ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadl
6148
6405
  }
6149
6406
  async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
6150
6407
  const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
6151
- if (!existsSync5(memoryDir)) {
6408
+ if (!existsSync6(memoryDir)) {
6152
6409
  return empty;
6153
6410
  }
6154
6411
  const localFiles = listPushableFiles(memoryDir).map((f) => ({
@@ -6236,7 +6493,7 @@ init_bounded_sync();
6236
6493
  init_memory_push_cache();
6237
6494
 
6238
6495
  // src/tools/memory/sync-kill-switch.ts
6239
- import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
6496
+ import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
6240
6497
  import { homedir as homedir6 } from "node:os";
6241
6498
  import { join as join10 } from "node:path";
6242
6499
  var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
@@ -6259,7 +6516,7 @@ function clip(raw) {
6259
6516
  function evaluateMemorySyncKillSwitch(deps = {}) {
6260
6517
  const env = deps.env ?? process.env;
6261
6518
  const home = deps.home ?? homedir6();
6262
- const fileExists = deps.fileExists ?? existsSync6;
6519
+ const fileExists = deps.fileExists ?? existsSync7;
6263
6520
  const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
6264
6521
  const fired = [];
6265
6522
  const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
@@ -6343,7 +6600,7 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch,
6343
6600
  }
6344
6601
  const memoryDir = getMemoryDir(cwd);
6345
6602
  const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
6346
- if (action === "push" && !existsSync8(memoryDir)) {
6603
+ if (action === "push" && !existsSync9(memoryDir)) {
6347
6604
  return {
6348
6605
  synced: true,
6349
6606
  action: "push",
@@ -6524,10 +6781,10 @@ async function getCloudAuth(fetchFn) {
6524
6781
  if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
6525
6782
  return { ok: true, controlPlaneUrl, token };
6526
6783
  }
6527
- async function callPrivateKnowledge(path3, body, fetchFn) {
6784
+ async function callPrivateKnowledge(path4, body, fetchFn) {
6528
6785
  const auth = await getCloudAuth(fetchFn);
6529
6786
  if (!auth.ok) return { ok: false, reason: auth.reason };
6530
- const response = await fetchFn(`${auth.controlPlaneUrl}${path3}`, {
6787
+ const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
6531
6788
  method: "POST",
6532
6789
  headers: {
6533
6790
  authorization: `Bearer ${auth.token}`,
@@ -6744,11 +7001,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
6744
7001
  }
6745
7002
 
6746
7003
  // src/tools/skills/skill-corpus.ts
6747
- import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
7004
+ import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
6748
7005
  import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
6749
7006
 
6750
7007
  // ../skill-registry/src/loader.ts
6751
- import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
7008
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
6752
7009
  import { join as join12 } from "node:path";
6753
7010
  var InvalidSkillFrontmatterError = class extends Error {
6754
7011
  constructor(skillFile, reason) {
@@ -6805,7 +7062,7 @@ function loadSkillsFromDir(skillsDir) {
6805
7062
  const entryPath = join12(skillsDir, entry);
6806
7063
  let stat;
6807
7064
  try {
6808
- stat = statSync5(entryPath);
7065
+ stat = statSync6(entryPath);
6809
7066
  } catch {
6810
7067
  continue;
6811
7068
  }
@@ -6854,12 +7111,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
6854
7111
  const override = env.VO_SKILLS_DIR;
6855
7112
  if (typeof override === "string" && override.length > 0) {
6856
7113
  const abs = isAbsolute(override) ? override : resolve2(startDir, override);
6857
- return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
7114
+ return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
6858
7115
  }
6859
7116
  let dir = resolve2(startDir);
6860
7117
  for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
6861
7118
  const candidate = join13(dir, ".claude", "skills");
6862
- if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
7119
+ if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
6863
7120
  const parent = dirname5(dir);
6864
7121
  if (parent === dir) break;
6865
7122
  dir = parent;