@algosuite/vo-mcp 0.2.0-beta.49 → 0.2.0-beta.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +595 -187
- package/dist/cli.js.map +4 -4
- package/dist/index.js +452 -157
- package/dist/index.js.map +4 -4
- package/dist/runner-cli.js +539 -98
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +100 -32
- package/dist/runner-supervisor.js.map +4 -4
- package/package.json +1 -1
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
|
|
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 (!
|
|
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
|
|
811
|
-
if (!existsSync2(
|
|
810
|
+
const path4 = opts.path ?? defaultOverridePath();
|
|
811
|
+
if (!existsSync2(path4)) {
|
|
812
812
|
return { override: null, source_path: null };
|
|
813
813
|
}
|
|
814
|
-
const raw = readFileSync2(
|
|
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 ${
|
|
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:
|
|
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 ${
|
|
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(
|
|
957
|
+
function matchesAnyGlob(path4, globs) {
|
|
958
958
|
for (const g of globs) {
|
|
959
|
-
if (globToRegExp(g).test(
|
|
959
|
+
if (globToRegExp(g).test(path4)) return true;
|
|
960
960
|
}
|
|
961
961
|
return false;
|
|
962
962
|
}
|
|
@@ -1490,7 +1490,17 @@ function assertWithinByteCap(toolName, fieldName, value, maxBytes) {
|
|
|
1490
1490
|
);
|
|
1491
1491
|
}
|
|
1492
1492
|
}
|
|
1493
|
+
var REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
1494
|
+
function subjectFromEnv(env = process.env) {
|
|
1495
|
+
const code_task_id = (env["VO_CODE_TASK_ID"] ?? "").trim().slice(0, 120);
|
|
1496
|
+
const repo = (env["VO_CODE_TASK_REPO"] ?? "").trim();
|
|
1497
|
+
const subject = {};
|
|
1498
|
+
if (code_task_id) subject.code_task_id = code_task_id;
|
|
1499
|
+
if (repo && REPO_RE.test(repo) && repo.length <= 200) subject.repo = repo;
|
|
1500
|
+
return subject.code_task_id || subject.repo ? subject : null;
|
|
1501
|
+
}
|
|
1493
1502
|
function buildBaseEvent(args) {
|
|
1503
|
+
const subject = args.subject === void 0 ? subjectFromEnv() : args.subject;
|
|
1494
1504
|
return {
|
|
1495
1505
|
schema_version: 1,
|
|
1496
1506
|
event_id: args.eventId ?? randomUUID(),
|
|
@@ -1516,7 +1526,12 @@ function buildBaseEvent(args) {
|
|
|
1516
1526
|
downstream_outcome: null,
|
|
1517
1527
|
vo_mcp_version: VO_MCP_VERSION,
|
|
1518
1528
|
consensus_engine_version: null,
|
|
1519
|
-
cache_hit: false
|
|
1529
|
+
cache_hit: false,
|
|
1530
|
+
// OMIT the key when there is no subject (rather than `subject: null`): the
|
|
1531
|
+
// ingest schema is `.strict()`, so an event with no subject stays valid on a
|
|
1532
|
+
// sink that has not learned the field yet — only subject-carrying events
|
|
1533
|
+
// depend on the sink being current (deploy ordering: sink before producer).
|
|
1534
|
+
...subject ? { subject } : {}
|
|
1520
1535
|
};
|
|
1521
1536
|
}
|
|
1522
1537
|
function jsonContent(value) {
|
|
@@ -1582,6 +1597,16 @@ function aggregateEventTokenUsage(src, engineUsage) {
|
|
|
1582
1597
|
total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
|
|
1583
1598
|
};
|
|
1584
1599
|
}
|
|
1600
|
+
function trajectoryFromEngine(result, known = {}) {
|
|
1601
|
+
const rounds = typeof result.token_usage?.rounds_counted === "number" && Number.isInteger(result.token_usage.rounds_counted) && result.token_usage.rounds_counted >= 0 ? result.token_usage.rounds_counted : null;
|
|
1602
|
+
const called = result.fan_out_diagnostics?.models_called;
|
|
1603
|
+
const turns = typeof called === "number" && Number.isInteger(called) && called >= 0 ? called : rounds !== null && rounds > 0 ? result.per_model_verdicts.length * rounds : null;
|
|
1604
|
+
const sourceGrounded = result.citation_grade !== void 0 || result.low_confidence_sources !== void 0;
|
|
1605
|
+
const tool_calls = typeof known.tool_calls === "number" && Number.isInteger(known.tool_calls) && known.tool_calls >= 0 ? known.tool_calls : sourceGrounded ? null : 0;
|
|
1606
|
+
if (rounds === null && turns === null && tool_calls === null) return {};
|
|
1607
|
+
const trajectory = { rounds, turns, tool_calls };
|
|
1608
|
+
return { trajectory };
|
|
1609
|
+
}
|
|
1585
1610
|
function toEventSynthesizedVerdict(src) {
|
|
1586
1611
|
return {
|
|
1587
1612
|
verdict: src.verdict,
|
|
@@ -1954,7 +1979,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
|
|
|
1954
1979
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1955
1980
|
duration_ms: engineResult.duration_ms,
|
|
1956
1981
|
consensus_engine_version: engineResult.engine_version,
|
|
1957
|
-
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
1982
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
1983
|
+
...trajectoryFromEngine(engineResult)
|
|
1958
1984
|
};
|
|
1959
1985
|
const payload = {
|
|
1960
1986
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2137,7 +2163,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2137
2163
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2138
2164
|
duration_ms: engineResult.duration_ms,
|
|
2139
2165
|
consensus_engine_version: engineResult.engine_version,
|
|
2140
|
-
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2166
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
2167
|
+
...trajectoryFromEngine(engineResult)
|
|
2141
2168
|
};
|
|
2142
2169
|
const payload = {
|
|
2143
2170
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2403,7 +2430,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2403
2430
|
consensus_engine_version: engineResult.engine_version,
|
|
2404
2431
|
per_model_verdicts: perModelForEvent,
|
|
2405
2432
|
synthesized_verdict: synthForEvent,
|
|
2406
|
-
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2433
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
2434
|
+
...trajectoryFromEngine(engineResult)
|
|
2407
2435
|
};
|
|
2408
2436
|
const payload = {
|
|
2409
2437
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2616,7 +2644,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
|
|
|
2616
2644
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2617
2645
|
duration_ms: engineResult.duration_ms,
|
|
2618
2646
|
consensus_engine_version: engineResult.engine_version,
|
|
2619
|
-
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2647
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
2648
|
+
...trajectoryFromEngine(engineResult)
|
|
2620
2649
|
};
|
|
2621
2650
|
const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
|
|
2622
2651
|
const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
|
|
@@ -4102,7 +4131,8 @@ Produce the JSON dispatch plan now.`;
|
|
|
4102
4131
|
consensus_engine_version: engineResult.engine_version,
|
|
4103
4132
|
per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
|
|
4104
4133
|
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
|
|
4105
|
-
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
4134
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
4135
|
+
...trajectoryFromEngine(engineResult)
|
|
4106
4136
|
};
|
|
4107
4137
|
deps.events.append(enrichedEvent);
|
|
4108
4138
|
return jsonContent(envelope);
|
|
@@ -4112,10 +4142,10 @@ Produce the JSON dispatch plan now.`;
|
|
|
4112
4142
|
init_auth_token_source();
|
|
4113
4143
|
init_credential_store();
|
|
4114
4144
|
var AdminCallableError = class extends Error {
|
|
4115
|
-
constructor(status,
|
|
4145
|
+
constructor(status, path4, message) {
|
|
4116
4146
|
super(message);
|
|
4117
4147
|
this.status = status;
|
|
4118
|
-
this.path =
|
|
4148
|
+
this.path = path4;
|
|
4119
4149
|
this.name = "AdminCallableError";
|
|
4120
4150
|
}
|
|
4121
4151
|
status;
|
|
@@ -4672,6 +4702,7 @@ async function handleRejectAndRetry(deps, rawInput, _signal) {
|
|
|
4672
4702
|
// src/tools/pr/review-merge.ts
|
|
4673
4703
|
var TOOL_NAME18 = "vo_review_merge";
|
|
4674
4704
|
var LIST_PATH = "/api/v1/admin/pr/list";
|
|
4705
|
+
var DEFAULT_REVIEW_REPO = "Algosuite-ai/Nexus";
|
|
4675
4706
|
var ENGINE_GATE = "final-deep-verify";
|
|
4676
4707
|
var EVENT_GATE = "merge-review";
|
|
4677
4708
|
var UNAVAILABLE_REASON = "vo_review_merge needs cloud mode to fetch PR context \u2014 set VO_CONTROL_PLANE_URL + VO_CONTROL_PLANE_ADMIN_TOKEN in the MCP env. (It is read-only; it never merges.)";
|
|
@@ -4736,6 +4767,11 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4736
4767
|
if (rawInput.notes !== void 0) normalizedInput.notes = rawInput.notes;
|
|
4737
4768
|
const inputJson = JSON.stringify(normalizedInput);
|
|
4738
4769
|
const key = deps.cache.keyFor(TOOL_NAME18, normalizedInput);
|
|
4770
|
+
const subject = {
|
|
4771
|
+
...subjectFromEnv() ?? {},
|
|
4772
|
+
repo: subjectFromEnv()?.repo ?? DEFAULT_REVIEW_REPO,
|
|
4773
|
+
pr_number: prNumber
|
|
4774
|
+
};
|
|
4739
4775
|
const baseEvent = buildBaseEvent({
|
|
4740
4776
|
tool: TOOL_NAME18,
|
|
4741
4777
|
gateType: EVENT_GATE,
|
|
@@ -4743,7 +4779,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4743
4779
|
inputExcerpt: inputJson.slice(0, 300),
|
|
4744
4780
|
inputSizeBytes: bytesOf(inputJson),
|
|
4745
4781
|
session: deps.session,
|
|
4746
|
-
now: deps.now()
|
|
4782
|
+
now: deps.now(),
|
|
4783
|
+
subject
|
|
4747
4784
|
});
|
|
4748
4785
|
const emit = (payload2, eventExtra) => {
|
|
4749
4786
|
deps.events.append(eventExtra ? { ...baseEvent, ...eventExtra } : baseEvent);
|
|
@@ -4845,7 +4882,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4845
4882
|
consensus_engine_version: result.engine_version,
|
|
4846
4883
|
per_model_verdicts: perModel,
|
|
4847
4884
|
synthesized_verdict: synth,
|
|
4848
|
-
...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
|
|
4885
|
+
...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage),
|
|
4886
|
+
...trajectoryFromEngine(result)
|
|
4849
4887
|
});
|
|
4850
4888
|
}
|
|
4851
4889
|
|
|
@@ -5102,7 +5140,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
5102
5140
|
import { spawn } from "node:child_process";
|
|
5103
5141
|
import { homedir as homedir5 } from "node:os";
|
|
5104
5142
|
import { join as join7 } from "node:path";
|
|
5105
|
-
import { existsSync as
|
|
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";
|
|
5106
5144
|
|
|
5107
5145
|
// src/swarm/tier-binding.ts
|
|
5108
5146
|
var SWARM_TIERS = Object.freeze([
|
|
@@ -5308,10 +5346,10 @@ function sanitizeSwarmId(raw) {
|
|
|
5308
5346
|
return id;
|
|
5309
5347
|
}
|
|
5310
5348
|
var CEILING_FILE = "ceiling.json";
|
|
5311
|
-
function createExclusive(
|
|
5349
|
+
function createExclusive(path4, contents) {
|
|
5312
5350
|
let fd;
|
|
5313
5351
|
try {
|
|
5314
|
-
fd = openSync(
|
|
5352
|
+
fd = openSync(path4, "wx");
|
|
5315
5353
|
} catch {
|
|
5316
5354
|
return false;
|
|
5317
5355
|
}
|
|
@@ -5326,18 +5364,18 @@ function capToCents(cap) {
|
|
|
5326
5364
|
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
5327
5365
|
}
|
|
5328
5366
|
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
5329
|
-
const
|
|
5367
|
+
const path4 = join6(swarmDir, CEILING_FILE);
|
|
5330
5368
|
const head = JSON.stringify({
|
|
5331
5369
|
ceiling: proposedCeiling,
|
|
5332
5370
|
cap_cents: proposedCapCents,
|
|
5333
5371
|
recorded_at: nowIso
|
|
5334
5372
|
});
|
|
5335
|
-
if (createExclusive(
|
|
5373
|
+
if (createExclusive(path4, head)) {
|
|
5336
5374
|
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
5337
5375
|
}
|
|
5338
5376
|
let parsed;
|
|
5339
5377
|
try {
|
|
5340
|
-
parsed = JSON.parse(readFileSync6(
|
|
5378
|
+
parsed = JSON.parse(readFileSync6(path4, "utf8"));
|
|
5341
5379
|
} catch {
|
|
5342
5380
|
return null;
|
|
5343
5381
|
}
|
|
@@ -5417,6 +5455,288 @@ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso })
|
|
|
5417
5455
|
};
|
|
5418
5456
|
};
|
|
5419
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
|
+
|
|
5420
5740
|
// src/tools/session/spawn-successor.ts
|
|
5421
5741
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
5422
5742
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
@@ -5448,7 +5768,7 @@ var inputSchema20 = {
|
|
|
5448
5768
|
additionalProperties: false
|
|
5449
5769
|
};
|
|
5450
5770
|
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
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.";
|
|
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.";
|
|
5452
5772
|
function isToolInput20(v) {
|
|
5453
5773
|
if (typeof v !== "object" || v === null) return false;
|
|
5454
5774
|
const o = v;
|
|
@@ -5466,7 +5786,7 @@ function retiredCounterRefusal(v) {
|
|
|
5466
5786
|
}
|
|
5467
5787
|
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
5468
5788
|
try {
|
|
5469
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, 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);
|
|
5470
5790
|
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
5471
5791
|
} catch {
|
|
5472
5792
|
return null;
|
|
@@ -5504,97 +5824,14 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
|
5504
5824
|
if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
|
|
5505
5825
|
return lines.join("\n");
|
|
5506
5826
|
}
|
|
5507
|
-
function
|
|
5508
|
-
const args = ["-p", "--permission-mode", "acceptEdits"];
|
|
5509
|
-
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
5510
|
-
args.push("--max-turns", String(maxTurns));
|
|
5511
|
-
}
|
|
5512
|
-
return args;
|
|
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
|
-
}
|
|
5590
|
-
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
5827
|
+
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
|
|
5591
5828
|
const retired = retiredCounterRefusal(rawInput);
|
|
5592
5829
|
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
5593
5830
|
if (!isToolInput20(rawInput)) {
|
|
5594
5831
|
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
5595
5832
|
}
|
|
5596
5833
|
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
5597
|
-
if (!handoffPath || !
|
|
5834
|
+
if (!handoffPath || !existsSync5(handoffPath)) {
|
|
5598
5835
|
return jsonContent({
|
|
5599
5836
|
tool: TOOL_NAME20,
|
|
5600
5837
|
schema_version: 1,
|
|
@@ -5619,27 +5856,52 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5619
5856
|
}
|
|
5620
5857
|
});
|
|
5621
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
|
+
}
|
|
5622
5878
|
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
5623
5879
|
mkdirSync4(logDir, { recursive: true });
|
|
5624
5880
|
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
5625
5881
|
const logFd = openSync2(logPath, "a");
|
|
5626
|
-
const child = spawnImpl(
|
|
5882
|
+
const child = spawnImpl(resolvedBin, [...plan.args], {
|
|
5627
5883
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
5628
5884
|
detached: true,
|
|
5629
5885
|
stdio: ["pipe", logFd, logFd],
|
|
5630
|
-
//
|
|
5631
|
-
//
|
|
5632
|
-
|
|
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,
|
|
5633
5891
|
windowsHide: true,
|
|
5892
|
+
windowsVerbatimArguments: false,
|
|
5634
5893
|
// Carry the SAME binding to the child. Without this the successor inherits
|
|
5635
5894
|
// no tier and re-resolves its own — which is the split-payer defect one
|
|
5636
5895
|
// generation down.
|
|
5637
5896
|
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
5638
5897
|
});
|
|
5898
|
+
closeSync2(logFd);
|
|
5639
5899
|
let spawnError = null;
|
|
5640
5900
|
child.on("error", (e) => {
|
|
5641
5901
|
spawnError = e.message;
|
|
5642
5902
|
});
|
|
5903
|
+
child.stdin.on?.("error", () => {
|
|
5904
|
+
});
|
|
5643
5905
|
try {
|
|
5644
5906
|
child.stdin.write(prompt);
|
|
5645
5907
|
child.stdin.end();
|
|
@@ -5647,10 +5909,40 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5647
5909
|
}
|
|
5648
5910
|
child.unref();
|
|
5649
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
|
+
}
|
|
5650
5942
|
return jsonContent({
|
|
5651
5943
|
tool: TOOL_NAME20,
|
|
5652
5944
|
schema_version: 1,
|
|
5653
|
-
payload:
|
|
5945
|
+
payload: {
|
|
5654
5946
|
spawned: true,
|
|
5655
5947
|
pid: child.pid ?? null,
|
|
5656
5948
|
log_path: logPath,
|
|
@@ -5662,7 +5954,10 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5662
5954
|
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
5663
5955
|
// against the pool without reading the ledger directory by hand.
|
|
5664
5956
|
ledger_cap_usd: plan.capUsd,
|
|
5665
|
-
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
|
|
5666
5961
|
}
|
|
5667
5962
|
});
|
|
5668
5963
|
}
|
|
@@ -5749,22 +6044,22 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
5749
6044
|
}
|
|
5750
6045
|
|
|
5751
6046
|
// src/tools/memory/sync-config.ts
|
|
5752
|
-
import { existsSync as
|
|
6047
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
5753
6048
|
import { homedir as homedir7 } from "node:os";
|
|
5754
6049
|
import { join as join11 } from "node:path";
|
|
5755
6050
|
|
|
5756
6051
|
// src/tools/memory/memory-sync-http.ts
|
|
5757
6052
|
init_safe_memory_file();
|
|
5758
|
-
import { existsSync as
|
|
6053
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
5759
6054
|
|
|
5760
6055
|
// src/tools/memory/sync-lock.ts
|
|
5761
|
-
import { closeSync as
|
|
6056
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5762
6057
|
import { hostname } from "node:os";
|
|
5763
6058
|
import { join as join8 } from "node:path";
|
|
5764
6059
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5765
6060
|
|
|
5766
6061
|
// src/tools/memory/sync-lock-liveness.ts
|
|
5767
|
-
import { statSync as
|
|
6062
|
+
import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
5768
6063
|
function defaultIsProcessAlive(pid) {
|
|
5769
6064
|
try {
|
|
5770
6065
|
process.kill(pid, 0);
|
|
@@ -5790,10 +6085,10 @@ function toPayload(parsed) {
|
|
|
5790
6085
|
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
5791
6086
|
};
|
|
5792
6087
|
}
|
|
5793
|
-
function readLockRecord(
|
|
6088
|
+
function readLockRecord(path4) {
|
|
5794
6089
|
let raw;
|
|
5795
6090
|
try {
|
|
5796
|
-
raw = readFileSync8(
|
|
6091
|
+
raw = readFileSync8(path4, "utf8");
|
|
5797
6092
|
} catch {
|
|
5798
6093
|
return null;
|
|
5799
6094
|
}
|
|
@@ -5803,7 +6098,7 @@ function readLockRecord(path3) {
|
|
|
5803
6098
|
return { raw, payload: null };
|
|
5804
6099
|
}
|
|
5805
6100
|
}
|
|
5806
|
-
function lockAgeMs(record,
|
|
6101
|
+
function lockAgeMs(record, path4, nowMs) {
|
|
5807
6102
|
let startedMs = Number.NaN;
|
|
5808
6103
|
if (record.payload) {
|
|
5809
6104
|
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
@@ -5814,7 +6109,7 @@ function lockAgeMs(record, path3, nowMs) {
|
|
|
5814
6109
|
}
|
|
5815
6110
|
if (!Number.isFinite(startedMs)) {
|
|
5816
6111
|
try {
|
|
5817
|
-
startedMs =
|
|
6112
|
+
startedMs = statSync5(path4).mtimeMs;
|
|
5818
6113
|
} catch {
|
|
5819
6114
|
return null;
|
|
5820
6115
|
}
|
|
@@ -5847,10 +6142,10 @@ var BACKOFF_FACTOR = 1.6;
|
|
|
5847
6142
|
function positiveOr(value, fallback) {
|
|
5848
6143
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
5849
6144
|
}
|
|
5850
|
-
function createExclusive2(
|
|
6145
|
+
function createExclusive2(path4, contents) {
|
|
5851
6146
|
let fd;
|
|
5852
6147
|
try {
|
|
5853
|
-
fd = openSync3(
|
|
6148
|
+
fd = openSync3(path4, "wx");
|
|
5854
6149
|
} catch (err) {
|
|
5855
6150
|
const code = err.code;
|
|
5856
6151
|
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -5858,37 +6153,37 @@ function createExclusive2(path3, contents) {
|
|
|
5858
6153
|
try {
|
|
5859
6154
|
writeFileSync4(fd, contents, "utf8");
|
|
5860
6155
|
} catch (err) {
|
|
5861
|
-
|
|
6156
|
+
closeSync3(fd);
|
|
5862
6157
|
try {
|
|
5863
|
-
unlinkSync2(
|
|
6158
|
+
unlinkSync2(path4);
|
|
5864
6159
|
} catch {
|
|
5865
6160
|
}
|
|
5866
6161
|
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
5867
6162
|
}
|
|
5868
|
-
|
|
6163
|
+
closeSync3(fd);
|
|
5869
6164
|
return { ok: true };
|
|
5870
6165
|
}
|
|
5871
|
-
function removeAbandoned(
|
|
6166
|
+
function removeAbandoned(path4, expectedRaw) {
|
|
5872
6167
|
let current;
|
|
5873
6168
|
try {
|
|
5874
|
-
current = readFileSync9(
|
|
6169
|
+
current = readFileSync9(path4, "utf8");
|
|
5875
6170
|
} catch {
|
|
5876
6171
|
return;
|
|
5877
6172
|
}
|
|
5878
6173
|
if (current !== expectedRaw) return;
|
|
5879
6174
|
try {
|
|
5880
|
-
unlinkSync2(
|
|
6175
|
+
unlinkSync2(path4);
|
|
5881
6176
|
} catch {
|
|
5882
6177
|
}
|
|
5883
6178
|
}
|
|
5884
|
-
function makeRelease(
|
|
6179
|
+
function makeRelease(path4, token) {
|
|
5885
6180
|
let released = false;
|
|
5886
6181
|
return () => {
|
|
5887
6182
|
if (released) return;
|
|
5888
6183
|
released = true;
|
|
5889
6184
|
let raw;
|
|
5890
6185
|
try {
|
|
5891
|
-
raw = readFileSync9(
|
|
6186
|
+
raw = readFileSync9(path4, "utf8");
|
|
5892
6187
|
} catch {
|
|
5893
6188
|
return;
|
|
5894
6189
|
}
|
|
@@ -5900,7 +6195,7 @@ function makeRelease(path3, token) {
|
|
|
5900
6195
|
}
|
|
5901
6196
|
if (!stillOurs) return;
|
|
5902
6197
|
try {
|
|
5903
|
-
unlinkSync2(
|
|
6198
|
+
unlinkSync2(path4);
|
|
5904
6199
|
} catch {
|
|
5905
6200
|
}
|
|
5906
6201
|
};
|
|
@@ -5919,7 +6214,7 @@ async function acquireMemorySyncLock(options) {
|
|
|
5919
6214
|
}));
|
|
5920
6215
|
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
5921
6216
|
const thisHost = hostname();
|
|
5922
|
-
const
|
|
6217
|
+
const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
5923
6218
|
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
5924
6219
|
const deadline = now() + waitMs;
|
|
5925
6220
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
@@ -5935,30 +6230,30 @@ async function acquireMemorySyncLock(options) {
|
|
|
5935
6230
|
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
5936
6231
|
acquiredAtMs
|
|
5937
6232
|
};
|
|
5938
|
-
const created = createExclusive2(
|
|
6233
|
+
const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
|
|
5939
6234
|
`);
|
|
5940
6235
|
if (created.ok) {
|
|
5941
|
-
return { path:
|
|
6236
|
+
return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
|
|
5942
6237
|
}
|
|
5943
6238
|
if (!created.exists) {
|
|
5944
6239
|
throw new Error(
|
|
5945
|
-
`memory sync lock ${
|
|
6240
|
+
`memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
5946
6241
|
);
|
|
5947
6242
|
}
|
|
5948
|
-
const record = readLockRecord(
|
|
6243
|
+
const record = readLockRecord(path4);
|
|
5949
6244
|
let reclaimed = false;
|
|
5950
6245
|
if (record) {
|
|
5951
6246
|
holderDescription = describeHolder(record);
|
|
5952
|
-
const age = lockAgeMs(record,
|
|
6247
|
+
const age = lockAgeMs(record, path4, now());
|
|
5953
6248
|
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
5954
6249
|
tookOverFrom = record.payload;
|
|
5955
|
-
removeAbandoned(
|
|
6250
|
+
removeAbandoned(path4, record.raw);
|
|
5956
6251
|
reclaimed = true;
|
|
5957
6252
|
}
|
|
5958
6253
|
}
|
|
5959
6254
|
if (now() >= deadline) {
|
|
5960
6255
|
throw new Error(
|
|
5961
|
-
`memory sync lock ${
|
|
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.`
|
|
5962
6257
|
);
|
|
5963
6258
|
}
|
|
5964
6259
|
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
@@ -6110,7 +6405,7 @@ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadl
|
|
|
6110
6405
|
}
|
|
6111
6406
|
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
6112
6407
|
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
6113
|
-
if (!
|
|
6408
|
+
if (!existsSync6(memoryDir)) {
|
|
6114
6409
|
return empty;
|
|
6115
6410
|
}
|
|
6116
6411
|
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
@@ -6198,7 +6493,7 @@ init_bounded_sync();
|
|
|
6198
6493
|
init_memory_push_cache();
|
|
6199
6494
|
|
|
6200
6495
|
// src/tools/memory/sync-kill-switch.ts
|
|
6201
|
-
import { existsSync as
|
|
6496
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
|
|
6202
6497
|
import { homedir as homedir6 } from "node:os";
|
|
6203
6498
|
import { join as join10 } from "node:path";
|
|
6204
6499
|
var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
@@ -6221,7 +6516,7 @@ function clip(raw) {
|
|
|
6221
6516
|
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
6222
6517
|
const env = deps.env ?? process.env;
|
|
6223
6518
|
const home = deps.home ?? homedir6();
|
|
6224
|
-
const fileExists = deps.fileExists ??
|
|
6519
|
+
const fileExists = deps.fileExists ?? existsSync7;
|
|
6225
6520
|
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
6226
6521
|
const fired = [];
|
|
6227
6522
|
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
@@ -6305,7 +6600,7 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch,
|
|
|
6305
6600
|
}
|
|
6306
6601
|
const memoryDir = getMemoryDir(cwd);
|
|
6307
6602
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
6308
|
-
if (action === "push" && !
|
|
6603
|
+
if (action === "push" && !existsSync9(memoryDir)) {
|
|
6309
6604
|
return {
|
|
6310
6605
|
synced: true,
|
|
6311
6606
|
action: "push",
|
|
@@ -6486,10 +6781,10 @@ async function getCloudAuth(fetchFn) {
|
|
|
6486
6781
|
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
6487
6782
|
return { ok: true, controlPlaneUrl, token };
|
|
6488
6783
|
}
|
|
6489
|
-
async function callPrivateKnowledge(
|
|
6784
|
+
async function callPrivateKnowledge(path4, body, fetchFn) {
|
|
6490
6785
|
const auth = await getCloudAuth(fetchFn);
|
|
6491
6786
|
if (!auth.ok) return { ok: false, reason: auth.reason };
|
|
6492
|
-
const response = await fetchFn(`${auth.controlPlaneUrl}${
|
|
6787
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
|
|
6493
6788
|
method: "POST",
|
|
6494
6789
|
headers: {
|
|
6495
6790
|
authorization: `Bearer ${auth.token}`,
|
|
@@ -6706,11 +7001,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
6706
7001
|
}
|
|
6707
7002
|
|
|
6708
7003
|
// src/tools/skills/skill-corpus.ts
|
|
6709
|
-
import { existsSync as
|
|
7004
|
+
import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
|
|
6710
7005
|
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
6711
7006
|
|
|
6712
7007
|
// ../skill-registry/src/loader.ts
|
|
6713
|
-
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as
|
|
7008
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
|
|
6714
7009
|
import { join as join12 } from "node:path";
|
|
6715
7010
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
6716
7011
|
constructor(skillFile, reason) {
|
|
@@ -6767,7 +7062,7 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
6767
7062
|
const entryPath = join12(skillsDir, entry);
|
|
6768
7063
|
let stat;
|
|
6769
7064
|
try {
|
|
6770
|
-
stat =
|
|
7065
|
+
stat = statSync6(entryPath);
|
|
6771
7066
|
} catch {
|
|
6772
7067
|
continue;
|
|
6773
7068
|
}
|
|
@@ -6816,12 +7111,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
6816
7111
|
const override = env.VO_SKILLS_DIR;
|
|
6817
7112
|
if (typeof override === "string" && override.length > 0) {
|
|
6818
7113
|
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
6819
|
-
return
|
|
7114
|
+
return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
|
|
6820
7115
|
}
|
|
6821
7116
|
let dir = resolve2(startDir);
|
|
6822
7117
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
6823
7118
|
const candidate = join13(dir, ".claude", "skills");
|
|
6824
|
-
if (
|
|
7119
|
+
if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
|
|
6825
7120
|
const parent = dirname5(dir);
|
|
6826
7121
|
if (parent === dir) break;
|
|
6827
7122
|
dir = parent;
|