@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/cli.js +432 -175
- package/dist/cli.js.map +4 -4
- package/dist/index.js +406 -149
- package/dist/index.js.map +4 -4
- package/dist/runner-cli.js +112 -6
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +45 -3
- package/dist/runner-supervisor.js.map +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -203,24 +203,24 @@ function defaultOverridePath() {
|
|
|
203
203
|
return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
|
|
204
204
|
}
|
|
205
205
|
function loadTenantOverride(opts = {}) {
|
|
206
|
-
const
|
|
207
|
-
if (!existsSync2(
|
|
206
|
+
const path4 = opts.path ?? defaultOverridePath();
|
|
207
|
+
if (!existsSync2(path4)) {
|
|
208
208
|
return { override: null, source_path: null };
|
|
209
209
|
}
|
|
210
|
-
const raw = readFileSync2(
|
|
210
|
+
const raw = readFileSync2(path4, "utf8");
|
|
211
211
|
let parsed;
|
|
212
212
|
try {
|
|
213
213
|
parsed = JSON.parse(raw);
|
|
214
214
|
} catch (err) {
|
|
215
215
|
const m = err instanceof Error ? err.message : String(err);
|
|
216
|
-
throw new Error(`vo-arch-defaults: invalid JSON in override ${
|
|
216
|
+
throw new Error(`vo-arch-defaults: invalid JSON in override ${path4}: ${m}`, { cause: err });
|
|
217
217
|
}
|
|
218
218
|
try {
|
|
219
219
|
const override = parseOverride(parsed);
|
|
220
|
-
return { override, source_path:
|
|
220
|
+
return { override, source_path: path4 };
|
|
221
221
|
} catch (err) {
|
|
222
222
|
const m = err instanceof Error ? err.message : String(err);
|
|
223
|
-
throw new Error(`vo-arch-defaults: override schema validation failed for ${
|
|
223
|
+
throw new Error(`vo-arch-defaults: override schema validation failed for ${path4}: ${m}`, { cause: err });
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
var init_load_override = __esm({
|
|
@@ -371,9 +371,9 @@ function globToRegExp(glob) {
|
|
|
371
371
|
}
|
|
372
372
|
return new RegExp("^" + out + "$");
|
|
373
373
|
}
|
|
374
|
-
function matchesAnyGlob(
|
|
374
|
+
function matchesAnyGlob(path4, globs) {
|
|
375
375
|
for (const g of globs) {
|
|
376
|
-
if (globToRegExp(g).test(
|
|
376
|
+
if (globToRegExp(g).test(path4)) return true;
|
|
377
377
|
}
|
|
378
378
|
return false;
|
|
379
379
|
}
|
|
@@ -1461,7 +1461,7 @@ var init_safe_memory_file = __esm({
|
|
|
1461
1461
|
});
|
|
1462
1462
|
|
|
1463
1463
|
// src/tools/memory/sync-lock-liveness.ts
|
|
1464
|
-
import { statSync as
|
|
1464
|
+
import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
1465
1465
|
function defaultIsProcessAlive(pid) {
|
|
1466
1466
|
try {
|
|
1467
1467
|
process.kill(pid, 0);
|
|
@@ -1487,10 +1487,10 @@ function toPayload(parsed) {
|
|
|
1487
1487
|
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
1488
1488
|
};
|
|
1489
1489
|
}
|
|
1490
|
-
function readLockRecord(
|
|
1490
|
+
function readLockRecord(path4) {
|
|
1491
1491
|
let raw;
|
|
1492
1492
|
try {
|
|
1493
|
-
raw = readFileSync8(
|
|
1493
|
+
raw = readFileSync8(path4, "utf8");
|
|
1494
1494
|
} catch {
|
|
1495
1495
|
return null;
|
|
1496
1496
|
}
|
|
@@ -1500,7 +1500,7 @@ function readLockRecord(path3) {
|
|
|
1500
1500
|
return { raw, payload: null };
|
|
1501
1501
|
}
|
|
1502
1502
|
}
|
|
1503
|
-
function lockAgeMs(record,
|
|
1503
|
+
function lockAgeMs(record, path4, nowMs) {
|
|
1504
1504
|
let startedMs = Number.NaN;
|
|
1505
1505
|
if (record.payload) {
|
|
1506
1506
|
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
@@ -1511,7 +1511,7 @@ function lockAgeMs(record, path3, nowMs) {
|
|
|
1511
1511
|
}
|
|
1512
1512
|
if (!Number.isFinite(startedMs)) {
|
|
1513
1513
|
try {
|
|
1514
|
-
startedMs =
|
|
1514
|
+
startedMs = statSync5(path4).mtimeMs;
|
|
1515
1515
|
} catch {
|
|
1516
1516
|
return null;
|
|
1517
1517
|
}
|
|
@@ -1540,17 +1540,17 @@ var init_sync_lock_liveness = __esm({
|
|
|
1540
1540
|
});
|
|
1541
1541
|
|
|
1542
1542
|
// src/tools/memory/sync-lock.ts
|
|
1543
|
-
import { closeSync as
|
|
1543
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1544
1544
|
import { hostname } from "node:os";
|
|
1545
1545
|
import { join as join8 } from "node:path";
|
|
1546
1546
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1547
1547
|
function positiveOr(value, fallback) {
|
|
1548
1548
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1549
1549
|
}
|
|
1550
|
-
function createExclusive2(
|
|
1550
|
+
function createExclusive2(path4, contents) {
|
|
1551
1551
|
let fd;
|
|
1552
1552
|
try {
|
|
1553
|
-
fd = openSync3(
|
|
1553
|
+
fd = openSync3(path4, "wx");
|
|
1554
1554
|
} catch (err) {
|
|
1555
1555
|
const code = err.code;
|
|
1556
1556
|
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -1558,37 +1558,37 @@ function createExclusive2(path3, contents) {
|
|
|
1558
1558
|
try {
|
|
1559
1559
|
writeFileSync4(fd, contents, "utf8");
|
|
1560
1560
|
} catch (err) {
|
|
1561
|
-
|
|
1561
|
+
closeSync3(fd);
|
|
1562
1562
|
try {
|
|
1563
|
-
unlinkSync2(
|
|
1563
|
+
unlinkSync2(path4);
|
|
1564
1564
|
} catch {
|
|
1565
1565
|
}
|
|
1566
1566
|
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
1567
1567
|
}
|
|
1568
|
-
|
|
1568
|
+
closeSync3(fd);
|
|
1569
1569
|
return { ok: true };
|
|
1570
1570
|
}
|
|
1571
|
-
function removeAbandoned(
|
|
1571
|
+
function removeAbandoned(path4, expectedRaw) {
|
|
1572
1572
|
let current;
|
|
1573
1573
|
try {
|
|
1574
|
-
current = readFileSync9(
|
|
1574
|
+
current = readFileSync9(path4, "utf8");
|
|
1575
1575
|
} catch {
|
|
1576
1576
|
return;
|
|
1577
1577
|
}
|
|
1578
1578
|
if (current !== expectedRaw) return;
|
|
1579
1579
|
try {
|
|
1580
|
-
unlinkSync2(
|
|
1580
|
+
unlinkSync2(path4);
|
|
1581
1581
|
} catch {
|
|
1582
1582
|
}
|
|
1583
1583
|
}
|
|
1584
|
-
function makeRelease(
|
|
1584
|
+
function makeRelease(path4, token) {
|
|
1585
1585
|
let released = false;
|
|
1586
1586
|
return () => {
|
|
1587
1587
|
if (released) return;
|
|
1588
1588
|
released = true;
|
|
1589
1589
|
let raw;
|
|
1590
1590
|
try {
|
|
1591
|
-
raw = readFileSync9(
|
|
1591
|
+
raw = readFileSync9(path4, "utf8");
|
|
1592
1592
|
} catch {
|
|
1593
1593
|
return;
|
|
1594
1594
|
}
|
|
@@ -1600,7 +1600,7 @@ function makeRelease(path3, token) {
|
|
|
1600
1600
|
}
|
|
1601
1601
|
if (!stillOurs) return;
|
|
1602
1602
|
try {
|
|
1603
|
-
unlinkSync2(
|
|
1603
|
+
unlinkSync2(path4);
|
|
1604
1604
|
} catch {
|
|
1605
1605
|
}
|
|
1606
1606
|
};
|
|
@@ -1619,7 +1619,7 @@ async function acquireMemorySyncLock(options) {
|
|
|
1619
1619
|
}));
|
|
1620
1620
|
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
1621
1621
|
const thisHost = hostname();
|
|
1622
|
-
const
|
|
1622
|
+
const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
1623
1623
|
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
1624
1624
|
const deadline = now() + waitMs;
|
|
1625
1625
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
@@ -1635,30 +1635,30 @@ async function acquireMemorySyncLock(options) {
|
|
|
1635
1635
|
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
1636
1636
|
acquiredAtMs
|
|
1637
1637
|
};
|
|
1638
|
-
const created = createExclusive2(
|
|
1638
|
+
const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
|
|
1639
1639
|
`);
|
|
1640
1640
|
if (created.ok) {
|
|
1641
|
-
return { path:
|
|
1641
|
+
return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
|
|
1642
1642
|
}
|
|
1643
1643
|
if (!created.exists) {
|
|
1644
1644
|
throw new Error(
|
|
1645
|
-
`memory sync lock ${
|
|
1645
|
+
`memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
1646
1646
|
);
|
|
1647
1647
|
}
|
|
1648
|
-
const record = readLockRecord(
|
|
1648
|
+
const record = readLockRecord(path4);
|
|
1649
1649
|
let reclaimed = false;
|
|
1650
1650
|
if (record) {
|
|
1651
1651
|
holderDescription = describeHolder(record);
|
|
1652
|
-
const age = lockAgeMs(record,
|
|
1652
|
+
const age = lockAgeMs(record, path4, now());
|
|
1653
1653
|
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
1654
1654
|
tookOverFrom = record.payload;
|
|
1655
|
-
removeAbandoned(
|
|
1655
|
+
removeAbandoned(path4, record.raw);
|
|
1656
1656
|
reclaimed = true;
|
|
1657
1657
|
}
|
|
1658
1658
|
}
|
|
1659
1659
|
if (now() >= deadline) {
|
|
1660
1660
|
throw new Error(
|
|
1661
|
-
`memory sync lock ${
|
|
1661
|
+
`memory sync lock ${path4} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
|
|
1662
1662
|
);
|
|
1663
1663
|
}
|
|
1664
1664
|
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
@@ -1922,7 +1922,7 @@ var init_memory_push_cache = __esm({
|
|
|
1922
1922
|
});
|
|
1923
1923
|
|
|
1924
1924
|
// src/tools/memory/memory-sync-http.ts
|
|
1925
|
-
import { existsSync as
|
|
1925
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
1926
1926
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
1927
1927
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1928
1928
|
const response = await withRequestTimeout(
|
|
@@ -2007,7 +2007,7 @@ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadl
|
|
|
2007
2007
|
}
|
|
2008
2008
|
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
2009
2009
|
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
2010
|
-
if (!
|
|
2010
|
+
if (!existsSync6(memoryDir)) {
|
|
2011
2011
|
return empty;
|
|
2012
2012
|
}
|
|
2013
2013
|
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
@@ -2101,7 +2101,7 @@ var init_memory_sync_http = __esm({
|
|
|
2101
2101
|
});
|
|
2102
2102
|
|
|
2103
2103
|
// src/tools/memory/sync-kill-switch.ts
|
|
2104
|
-
import { existsSync as
|
|
2104
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
|
|
2105
2105
|
import { homedir as homedir6 } from "node:os";
|
|
2106
2106
|
import { join as join10 } from "node:path";
|
|
2107
2107
|
function memorySyncSentinelPath(home) {
|
|
@@ -2120,7 +2120,7 @@ function clip(raw) {
|
|
|
2120
2120
|
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
2121
2121
|
const env = deps.env ?? process.env;
|
|
2122
2122
|
const home = deps.home ?? homedir6();
|
|
2123
|
-
const fileExists = deps.fileExists ??
|
|
2123
|
+
const fileExists = deps.fileExists ?? existsSync7;
|
|
2124
2124
|
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
2125
2125
|
const fired = [];
|
|
2126
2126
|
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
@@ -2166,7 +2166,7 @@ __export(memory_knowledge_bridge_exports, {
|
|
|
2166
2166
|
extractMemoryTitle: () => extractMemoryTitle,
|
|
2167
2167
|
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
2168
2168
|
});
|
|
2169
|
-
import { existsSync as
|
|
2169
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
2170
2170
|
function extractMemoryTitle(fileName, content) {
|
|
2171
2171
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2172
2172
|
if (frontmatter) {
|
|
@@ -2181,7 +2181,7 @@ async function upsertMemoryFilesAsKnowledge(options) {
|
|
|
2181
2181
|
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
2182
2182
|
let files;
|
|
2183
2183
|
try {
|
|
2184
|
-
if (!
|
|
2184
|
+
if (!existsSync8(memoryDir)) {
|
|
2185
2185
|
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
2186
2186
|
}
|
|
2187
2187
|
files = readdirSync5(memoryDir).filter(
|
|
@@ -2293,7 +2293,7 @@ __export(sync_config_exports, {
|
|
|
2293
2293
|
isNoopSyncReason: () => isNoopSyncReason,
|
|
2294
2294
|
runMemorySync: () => runMemorySync
|
|
2295
2295
|
});
|
|
2296
|
-
import { existsSync as
|
|
2296
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
2297
2297
|
import { homedir as homedir7 } from "node:os";
|
|
2298
2298
|
import { join as join11 } from "node:path";
|
|
2299
2299
|
function isToolInput22(v) {
|
|
@@ -2335,7 +2335,7 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch,
|
|
|
2335
2335
|
}
|
|
2336
2336
|
const memoryDir = getMemoryDir(cwd);
|
|
2337
2337
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
2338
|
-
if (action === "push" && !
|
|
2338
|
+
if (action === "push" && !existsSync9(memoryDir)) {
|
|
2339
2339
|
return {
|
|
2340
2340
|
synced: true,
|
|
2341
2341
|
action: "push",
|
|
@@ -5014,10 +5014,10 @@ init_common();
|
|
|
5014
5014
|
init_auth_token_source();
|
|
5015
5015
|
init_credential_store();
|
|
5016
5016
|
var AdminCallableError = class extends Error {
|
|
5017
|
-
constructor(status,
|
|
5017
|
+
constructor(status, path4, message) {
|
|
5018
5018
|
super(message);
|
|
5019
5019
|
this.status = status;
|
|
5020
|
-
this.path =
|
|
5020
|
+
this.path = path4;
|
|
5021
5021
|
this.name = "AdminCallableError";
|
|
5022
5022
|
}
|
|
5023
5023
|
status;
|
|
@@ -5044,21 +5044,21 @@ var HttpAdminCallableClient = class {
|
|
|
5044
5044
|
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
5045
5045
|
this.readOnly = config.readOnly ?? false;
|
|
5046
5046
|
}
|
|
5047
|
-
async invoke(
|
|
5048
|
-
if (!
|
|
5047
|
+
async invoke(path4, body, opts) {
|
|
5048
|
+
if (!path4.startsWith("/")) {
|
|
5049
5049
|
throw new Error(
|
|
5050
|
-
`HttpAdminCallableClient.invoke: path must start with '/', got '${
|
|
5050
|
+
`HttpAdminCallableClient.invoke: path must start with '/', got '${path4}'`
|
|
5051
5051
|
);
|
|
5052
5052
|
}
|
|
5053
5053
|
const token = await this.tokenSource.getToken();
|
|
5054
5054
|
if (!token) {
|
|
5055
5055
|
throw new AdminCallableError(
|
|
5056
5056
|
401,
|
|
5057
|
-
|
|
5058
|
-
`admin proxy ${
|
|
5057
|
+
path4,
|
|
5058
|
+
`admin proxy ${path4}: no auth token available (check VO_USER_REFRESH_TOKEN / VO_USER_ID_TOKEN / VO_CONTROL_PLANE_ADMIN_TOKEN)`
|
|
5059
5059
|
);
|
|
5060
5060
|
}
|
|
5061
|
-
const url = `${this.baseUrl}${
|
|
5061
|
+
const url = `${this.baseUrl}${path4}`;
|
|
5062
5062
|
const response = await this.fetchFn(url, {
|
|
5063
5063
|
method: "POST",
|
|
5064
5064
|
headers: {
|
|
@@ -5071,8 +5071,8 @@ var HttpAdminCallableClient = class {
|
|
|
5071
5071
|
if (response.status < 200 || response.status >= 300) {
|
|
5072
5072
|
throw new AdminCallableError(
|
|
5073
5073
|
response.status,
|
|
5074
|
-
|
|
5075
|
-
`admin proxy ${
|
|
5074
|
+
path4,
|
|
5075
|
+
`admin proxy ${path4} returned HTTP ${response.status}: ${text.slice(0, 200)}`
|
|
5076
5076
|
);
|
|
5077
5077
|
}
|
|
5078
5078
|
let parsed;
|
|
@@ -5081,23 +5081,23 @@ var HttpAdminCallableClient = class {
|
|
|
5081
5081
|
} catch {
|
|
5082
5082
|
throw new AdminCallableError(
|
|
5083
5083
|
response.status,
|
|
5084
|
-
|
|
5085
|
-
`admin proxy ${
|
|
5084
|
+
path4,
|
|
5085
|
+
`admin proxy ${path4} returned non-JSON body`
|
|
5086
5086
|
);
|
|
5087
5087
|
}
|
|
5088
5088
|
if (typeof parsed !== "object" || parsed === null) {
|
|
5089
5089
|
throw new AdminCallableError(
|
|
5090
5090
|
response.status,
|
|
5091
|
-
|
|
5092
|
-
`admin proxy ${
|
|
5091
|
+
path4,
|
|
5092
|
+
`admin proxy ${path4} response not an object`
|
|
5093
5093
|
);
|
|
5094
5094
|
}
|
|
5095
5095
|
const obj = parsed;
|
|
5096
5096
|
if (obj["ok"] !== true) {
|
|
5097
5097
|
throw new AdminCallableError(
|
|
5098
5098
|
response.status,
|
|
5099
|
-
|
|
5100
|
-
`admin proxy ${
|
|
5099
|
+
path4,
|
|
5100
|
+
`admin proxy ${path4} returned ok=false: ${JSON.stringify(obj).slice(0, 200)}`
|
|
5101
5101
|
);
|
|
5102
5102
|
}
|
|
5103
5103
|
if (opts?.rawEnvelope) {
|
|
@@ -5110,8 +5110,8 @@ var HttpAdminCallableClient = class {
|
|
|
5110
5110
|
if (!("result" in obj)) {
|
|
5111
5111
|
throw new AdminCallableError(
|
|
5112
5112
|
response.status,
|
|
5113
|
-
|
|
5114
|
-
`admin proxy ${
|
|
5113
|
+
path4,
|
|
5114
|
+
`admin proxy ${path4} response missing .result field`
|
|
5115
5115
|
);
|
|
5116
5116
|
}
|
|
5117
5117
|
return obj["result"];
|
|
@@ -6145,7 +6145,7 @@ init_common();
|
|
|
6145
6145
|
import { spawn } from "node:child_process";
|
|
6146
6146
|
import { homedir as homedir5 } from "node:os";
|
|
6147
6147
|
import { join as join7 } from "node:path";
|
|
6148
|
-
import { existsSync as
|
|
6148
|
+
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";
|
|
6149
6149
|
|
|
6150
6150
|
// src/swarm/tier-binding.ts
|
|
6151
6151
|
var SWARM_TIERS = Object.freeze([
|
|
@@ -6351,10 +6351,10 @@ function sanitizeSwarmId(raw) {
|
|
|
6351
6351
|
return id;
|
|
6352
6352
|
}
|
|
6353
6353
|
var CEILING_FILE = "ceiling.json";
|
|
6354
|
-
function createExclusive(
|
|
6354
|
+
function createExclusive(path4, contents) {
|
|
6355
6355
|
let fd;
|
|
6356
6356
|
try {
|
|
6357
|
-
fd = openSync(
|
|
6357
|
+
fd = openSync(path4, "wx");
|
|
6358
6358
|
} catch {
|
|
6359
6359
|
return false;
|
|
6360
6360
|
}
|
|
@@ -6369,18 +6369,18 @@ function capToCents(cap) {
|
|
|
6369
6369
|
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
6370
6370
|
}
|
|
6371
6371
|
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
6372
|
-
const
|
|
6372
|
+
const path4 = join6(swarmDir, CEILING_FILE);
|
|
6373
6373
|
const head = JSON.stringify({
|
|
6374
6374
|
ceiling: proposedCeiling,
|
|
6375
6375
|
cap_cents: proposedCapCents,
|
|
6376
6376
|
recorded_at: nowIso
|
|
6377
6377
|
});
|
|
6378
|
-
if (createExclusive(
|
|
6378
|
+
if (createExclusive(path4, head)) {
|
|
6379
6379
|
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
6380
6380
|
}
|
|
6381
6381
|
let parsed;
|
|
6382
6382
|
try {
|
|
6383
|
-
parsed = JSON.parse(readFileSync6(
|
|
6383
|
+
parsed = JSON.parse(readFileSync6(path4, "utf8"));
|
|
6384
6384
|
} catch {
|
|
6385
6385
|
return null;
|
|
6386
6386
|
}
|
|
@@ -6460,6 +6460,288 @@ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso })
|
|
|
6460
6460
|
};
|
|
6461
6461
|
};
|
|
6462
6462
|
|
|
6463
|
+
// src/swarm/spawn-plan.ts
|
|
6464
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
6465
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
6466
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
6467
|
+
if (!hasBinding) {
|
|
6468
|
+
const explicit = input.agent?.trim();
|
|
6469
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit || "claude", maxTurns: input.max_turns, platform });
|
|
6470
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
6471
|
+
return {
|
|
6472
|
+
ok: true,
|
|
6473
|
+
bin: resolved2.bin,
|
|
6474
|
+
args: resolved2.args,
|
|
6475
|
+
agent: resolved2.agent,
|
|
6476
|
+
tier: "unbound",
|
|
6477
|
+
bound: false,
|
|
6478
|
+
env: {},
|
|
6479
|
+
slot: null,
|
|
6480
|
+
capUsd: null,
|
|
6481
|
+
capRemainingUsd: null
|
|
6482
|
+
};
|
|
6483
|
+
}
|
|
6484
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
6485
|
+
const admission = admitSubagentSpawn(binding);
|
|
6486
|
+
if (!admission.allowed) {
|
|
6487
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
6488
|
+
}
|
|
6489
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
6490
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
6491
|
+
const resolved = resolveSuccessorLaunch({
|
|
6492
|
+
agent: binding.agent,
|
|
6493
|
+
maxTurns: input.max_turns,
|
|
6494
|
+
platform
|
|
6495
|
+
});
|
|
6496
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
6497
|
+
const slot = claim({
|
|
6498
|
+
swarmId: binding.swarm_id,
|
|
6499
|
+
proposedCeiling: binding.subagent_budget,
|
|
6500
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
6501
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
6502
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
6503
|
+
dir: resolveLedgerDir(env),
|
|
6504
|
+
nowIso
|
|
6505
|
+
});
|
|
6506
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
6507
|
+
return {
|
|
6508
|
+
ok: true,
|
|
6509
|
+
bin: resolved.bin,
|
|
6510
|
+
args: resolved.args,
|
|
6511
|
+
agent: resolved.agent,
|
|
6512
|
+
tier: binding.tier,
|
|
6513
|
+
bound: true,
|
|
6514
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
6515
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
6516
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
6517
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
6518
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
6519
|
+
// untouched $50 and walked away with $75 between them.
|
|
6520
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
6521
|
+
slot: slot.slot,
|
|
6522
|
+
capUsd: slot.capUsd,
|
|
6523
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
6524
|
+
};
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
// ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
|
|
6528
|
+
import { existsSync as existsSync4, realpathSync } from "node:fs";
|
|
6529
|
+
import { win32 as path3 } from "node:path";
|
|
6530
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6531
|
+
var NATIVE_CLAUDE_PARTS = [
|
|
6532
|
+
"node_modules",
|
|
6533
|
+
"@anthropic-ai",
|
|
6534
|
+
"claude-code",
|
|
6535
|
+
"bin",
|
|
6536
|
+
"claude.exe"
|
|
6537
|
+
];
|
|
6538
|
+
function pathValue(env) {
|
|
6539
|
+
for (const key of ["Path", "PATH", "path"]) {
|
|
6540
|
+
if (typeof env?.[key] === "string") return env[key];
|
|
6541
|
+
}
|
|
6542
|
+
return "";
|
|
6543
|
+
}
|
|
6544
|
+
function cleanPathSegment(value) {
|
|
6545
|
+
const trimmed = String(value || "").trim();
|
|
6546
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
6547
|
+
}
|
|
6548
|
+
function envValue(env, name) {
|
|
6549
|
+
const exact = env?.[name];
|
|
6550
|
+
if (typeof exact === "string") return exact.trim();
|
|
6551
|
+
const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
6552
|
+
return typeof env?.[key] === "string" ? env[key].trim() : "";
|
|
6553
|
+
}
|
|
6554
|
+
function userClaudeCandidates(bin, env) {
|
|
6555
|
+
if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
|
|
6556
|
+
const userProfile = envValue(env, "USERPROFILE");
|
|
6557
|
+
const appData = envValue(env, "APPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Roaming") : "");
|
|
6558
|
+
const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Local") : "");
|
|
6559
|
+
const candidates = [];
|
|
6560
|
+
if (appData) {
|
|
6561
|
+
const npmBin = path3.join(appData, "npm");
|
|
6562
|
+
candidates.push(
|
|
6563
|
+
path3.join(npmBin, "claude.exe"),
|
|
6564
|
+
path3.join(npmBin, "claude.cmd"),
|
|
6565
|
+
path3.join(npmBin, "claude.ps1"),
|
|
6566
|
+
path3.join(npmBin, "claude"),
|
|
6567
|
+
path3.join(npmBin, ...NATIVE_CLAUDE_PARTS)
|
|
6568
|
+
);
|
|
6569
|
+
}
|
|
6570
|
+
if (userProfile) candidates.push(path3.join(userProfile, ".local", "bin", "claude.exe"));
|
|
6571
|
+
if (localAppData) {
|
|
6572
|
+
candidates.push(
|
|
6573
|
+
path3.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
6574
|
+
path3.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
|
|
6575
|
+
);
|
|
6576
|
+
}
|
|
6577
|
+
return candidates;
|
|
6578
|
+
}
|
|
6579
|
+
function pathCandidates(bin, env) {
|
|
6580
|
+
if (path3.isAbsolute(bin) || /[\\/]/u.test(bin)) {
|
|
6581
|
+
return [path3.resolve(bin)];
|
|
6582
|
+
}
|
|
6583
|
+
const extension = path3.extname(bin);
|
|
6584
|
+
const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path3.join(directory, bin)] : [
|
|
6585
|
+
path3.join(directory, `${bin}.exe`),
|
|
6586
|
+
path3.join(directory, `${bin}.cmd`),
|
|
6587
|
+
path3.join(directory, `${bin}.ps1`),
|
|
6588
|
+
path3.join(directory, bin)
|
|
6589
|
+
]);
|
|
6590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6591
|
+
return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
|
|
6592
|
+
const key = candidate.toLowerCase();
|
|
6593
|
+
if (seen.has(key)) return false;
|
|
6594
|
+
seen.add(key);
|
|
6595
|
+
return true;
|
|
6596
|
+
});
|
|
6597
|
+
}
|
|
6598
|
+
function canonicalExistingPath(candidate, exists, canonicalize2) {
|
|
6599
|
+
if (!exists(candidate)) return null;
|
|
6600
|
+
try {
|
|
6601
|
+
return canonicalize2(candidate);
|
|
6602
|
+
} catch {
|
|
6603
|
+
return null;
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
function resolveWindowsClaudeExecutable({
|
|
6607
|
+
bin = "claude",
|
|
6608
|
+
env = process.env,
|
|
6609
|
+
exists = existsSync4,
|
|
6610
|
+
canonicalize: canonicalize2 = realpathSync
|
|
6611
|
+
} = {}) {
|
|
6612
|
+
const requested = String(bin || "").trim();
|
|
6613
|
+
if (!requested || requested.includes("\0")) {
|
|
6614
|
+
throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
|
|
6615
|
+
}
|
|
6616
|
+
for (const candidate of pathCandidates(requested, env)) {
|
|
6617
|
+
const found = canonicalExistingPath(candidate, exists, canonicalize2);
|
|
6618
|
+
if (!found) continue;
|
|
6619
|
+
if (path3.extname(found).toLowerCase() === ".exe") return found;
|
|
6620
|
+
const native = path3.join(path3.dirname(found), ...NATIVE_CLAUDE_PARTS);
|
|
6621
|
+
const resolvedNative = canonicalExistingPath(native, exists, canonicalize2);
|
|
6622
|
+
if (resolvedNative) return resolvedNative;
|
|
6623
|
+
}
|
|
6624
|
+
const error = new Error(
|
|
6625
|
+
`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.`
|
|
6626
|
+
);
|
|
6627
|
+
error.code = "ENOENT";
|
|
6628
|
+
throw error;
|
|
6629
|
+
}
|
|
6630
|
+
|
|
6631
|
+
// src/swarm/successor-windows-exe.ts
|
|
6632
|
+
var resolveWindowsClaudeExe = (bin, env) => resolveWindowsClaudeExecutable({ bin, env });
|
|
6633
|
+
function resolveNativeWindowsExecutable(bin, env = process.env, resolve3 = resolveWindowsClaudeExe) {
|
|
6634
|
+
try {
|
|
6635
|
+
return { ok: true, bin: resolve3(bin, env) };
|
|
6636
|
+
} catch (error) {
|
|
6637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6638
|
+
return {
|
|
6639
|
+
ok: false,
|
|
6640
|
+
reason: `could not resolve a native Windows executable for '${bin}': ${message}`
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
// src/swarm/successor-liveness.ts
|
|
6646
|
+
import { statSync as statSync3 } from "node:fs";
|
|
6647
|
+
var DEFAULT_EARLY_EXIT_SEC = 10;
|
|
6648
|
+
var DEFAULT_NO_OUTPUT_SEC = 0;
|
|
6649
|
+
var DEFAULT_POLL_MS = 200;
|
|
6650
|
+
var KILL_ESCALATION_MS = 2e3;
|
|
6651
|
+
function positiveSeconds(raw, fallback) {
|
|
6652
|
+
if (raw === void 0) return fallback;
|
|
6653
|
+
const n = Number(raw.trim());
|
|
6654
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
6655
|
+
}
|
|
6656
|
+
function resolveLivenessConfigFromEnv(env = process.env, explicitOverride = {}) {
|
|
6657
|
+
const earlyExitMs = explicitOverride.earlyExitMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_EXIT_CHECK_SEC"], DEFAULT_EARLY_EXIT_SEC) * 1e3;
|
|
6658
|
+
const noOutputMs = explicitOverride.noOutputMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_OUTPUT_CHECK_SEC"], DEFAULT_NO_OUTPUT_SEC) * 1e3;
|
|
6659
|
+
return { earlyExitMs, noOutputMs };
|
|
6660
|
+
}
|
|
6661
|
+
function defaultStatLogBytes(path4) {
|
|
6662
|
+
try {
|
|
6663
|
+
return statSync3(path4).size;
|
|
6664
|
+
} catch {
|
|
6665
|
+
return 0;
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
function defaultKillChild(child) {
|
|
6669
|
+
try {
|
|
6670
|
+
child.kill("SIGTERM");
|
|
6671
|
+
} catch {
|
|
6672
|
+
}
|
|
6673
|
+
const escalation = setTimeout(() => {
|
|
6674
|
+
try {
|
|
6675
|
+
child.kill("SIGKILL");
|
|
6676
|
+
} catch {
|
|
6677
|
+
}
|
|
6678
|
+
}, KILL_ESCALATION_MS);
|
|
6679
|
+
escalation.unref();
|
|
6680
|
+
}
|
|
6681
|
+
function checkSuccessorLiveness(child, logPath, config, deps = {}) {
|
|
6682
|
+
const statLogBytes = deps.statLogBytes ?? defaultStatLogBytes;
|
|
6683
|
+
const now = deps.now ?? Date.now;
|
|
6684
|
+
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
6685
|
+
const killChild = deps.killChild ?? defaultKillChild;
|
|
6686
|
+
const outputGateEnabled = Number.isFinite(config.noOutputMs) && config.noOutputMs > 0;
|
|
6687
|
+
const startedAt = now();
|
|
6688
|
+
return new Promise((resolve3) => {
|
|
6689
|
+
let settled = false;
|
|
6690
|
+
let pollTimer = null;
|
|
6691
|
+
const onExit = (code, signal) => {
|
|
6692
|
+
const elapsedMs = now() - startedAt;
|
|
6693
|
+
finish({
|
|
6694
|
+
ok: false,
|
|
6695
|
+
reason: `child_exited_early: exit code ${code ?? "null"} signal ${signal ?? "none"} after ${elapsedMs}ms`,
|
|
6696
|
+
detail: { exitCode: code, signal, elapsedMs }
|
|
6697
|
+
});
|
|
6698
|
+
};
|
|
6699
|
+
const cleanup = () => {
|
|
6700
|
+
if (pollTimer !== null) clearInterval(pollTimer);
|
|
6701
|
+
child.off?.("exit", onExit);
|
|
6702
|
+
};
|
|
6703
|
+
const finish = (result) => {
|
|
6704
|
+
if (settled) return;
|
|
6705
|
+
settled = true;
|
|
6706
|
+
cleanup();
|
|
6707
|
+
if (!result.ok) {
|
|
6708
|
+
try {
|
|
6709
|
+
killChild(child);
|
|
6710
|
+
} catch {
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
resolve3(result);
|
|
6714
|
+
};
|
|
6715
|
+
if (typeof child.exitCode === "number" || typeof child.signalCode === "string" && child.signalCode.length > 0) {
|
|
6716
|
+
finish({
|
|
6717
|
+
ok: false,
|
|
6718
|
+
reason: `child_exited_early: exit code ${child.exitCode ?? "null"} signal ${child.signalCode ?? "none"} before the liveness watch attached`,
|
|
6719
|
+
detail: { exitCode: child.exitCode ?? null, signal: child.signalCode ?? null, elapsedMs: 0 }
|
|
6720
|
+
});
|
|
6721
|
+
return;
|
|
6722
|
+
}
|
|
6723
|
+
child.on("exit", onExit);
|
|
6724
|
+
const tick = () => {
|
|
6725
|
+
if (settled) return;
|
|
6726
|
+
const elapsedMs = now() - startedAt;
|
|
6727
|
+
const outputSeen = outputGateEnabled ? statLogBytes(logPath) > 0 : true;
|
|
6728
|
+
if (outputSeen && elapsedMs >= config.earlyExitMs) {
|
|
6729
|
+
finish({ ok: true });
|
|
6730
|
+
return;
|
|
6731
|
+
}
|
|
6732
|
+
if (outputGateEnabled && !outputSeen && elapsedMs >= config.noOutputMs) {
|
|
6733
|
+
finish({
|
|
6734
|
+
ok: false,
|
|
6735
|
+
reason: `no_output: log stayed empty for ${elapsedMs}ms (limit ${config.noOutputMs}ms)`,
|
|
6736
|
+
detail: { elapsedMs, logPath }
|
|
6737
|
+
});
|
|
6738
|
+
}
|
|
6739
|
+
};
|
|
6740
|
+
pollTimer = setInterval(tick, pollIntervalMs);
|
|
6741
|
+
tick();
|
|
6742
|
+
});
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6463
6745
|
// src/tools/session/spawn-successor.ts
|
|
6464
6746
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
6465
6747
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
@@ -6491,7 +6773,7 @@ var inputSchema20 = {
|
|
|
6491
6773
|
additionalProperties: false
|
|
6492
6774
|
};
|
|
6493
6775
|
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
6494
|
-
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.";
|
|
6776
|
+
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.";
|
|
6495
6777
|
function isToolInput20(v) {
|
|
6496
6778
|
if (typeof v !== "object" || v === null) return false;
|
|
6497
6779
|
const o = v;
|
|
@@ -6509,7 +6791,7 @@ function retiredCounterRefusal(v) {
|
|
|
6509
6791
|
}
|
|
6510
6792
|
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
6511
6793
|
try {
|
|
6512
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m:
|
|
6794
|
+
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);
|
|
6513
6795
|
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
6514
6796
|
} catch {
|
|
6515
6797
|
return null;
|
|
@@ -6547,97 +6829,14 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
|
6547
6829
|
if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
|
|
6548
6830
|
return lines.join("\n");
|
|
6549
6831
|
}
|
|
6550
|
-
function
|
|
6551
|
-
const args = ["-p", "--permission-mode", "acceptEdits"];
|
|
6552
|
-
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
6553
|
-
args.push("--max-turns", String(maxTurns));
|
|
6554
|
-
}
|
|
6555
|
-
return args;
|
|
6556
|
-
}
|
|
6557
|
-
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
6558
|
-
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
6559
|
-
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
6560
|
-
if (!hasBinding) {
|
|
6561
|
-
const explicit = input.agent?.trim();
|
|
6562
|
-
if (explicit) {
|
|
6563
|
-
const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
|
|
6564
|
-
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
6565
|
-
return {
|
|
6566
|
-
ok: true,
|
|
6567
|
-
bin: resolved2.bin,
|
|
6568
|
-
args: resolved2.args,
|
|
6569
|
-
agent: resolved2.agent,
|
|
6570
|
-
tier: "unbound",
|
|
6571
|
-
bound: false,
|
|
6572
|
-
env: {},
|
|
6573
|
-
slot: null,
|
|
6574
|
-
capUsd: null,
|
|
6575
|
-
capRemainingUsd: null
|
|
6576
|
-
};
|
|
6577
|
-
}
|
|
6578
|
-
return {
|
|
6579
|
-
ok: true,
|
|
6580
|
-
bin: "claude",
|
|
6581
|
-
args: buildSuccessorArgs(input.max_turns),
|
|
6582
|
-
agent: "claude",
|
|
6583
|
-
tier: "unbound",
|
|
6584
|
-
bound: false,
|
|
6585
|
-
env: {},
|
|
6586
|
-
slot: null,
|
|
6587
|
-
capUsd: null,
|
|
6588
|
-
capRemainingUsd: null
|
|
6589
|
-
};
|
|
6590
|
-
}
|
|
6591
|
-
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
6592
|
-
const admission = admitSubagentSpawn(binding);
|
|
6593
|
-
if (!admission.allowed) {
|
|
6594
|
-
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
6595
|
-
}
|
|
6596
|
-
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
6597
|
-
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
6598
|
-
const resolved = resolveSuccessorLaunch({
|
|
6599
|
-
agent: binding.agent,
|
|
6600
|
-
maxTurns: input.max_turns,
|
|
6601
|
-
platform
|
|
6602
|
-
});
|
|
6603
|
-
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
6604
|
-
const slot = claim({
|
|
6605
|
-
swarmId: binding.swarm_id,
|
|
6606
|
-
proposedCeiling: binding.subagent_budget,
|
|
6607
|
-
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
6608
|
-
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
6609
|
-
proposedCapUsd: binding.spend_cap_usd,
|
|
6610
|
-
dir: resolveLedgerDir(env),
|
|
6611
|
-
nowIso
|
|
6612
|
-
});
|
|
6613
|
-
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
6614
|
-
return {
|
|
6615
|
-
ok: true,
|
|
6616
|
-
bin: resolved.bin,
|
|
6617
|
-
args: resolved.args,
|
|
6618
|
-
agent: resolved.agent,
|
|
6619
|
-
tier: binding.tier,
|
|
6620
|
-
bound: true,
|
|
6621
|
-
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
6622
|
-
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
6623
|
-
// #9312) meant the child re-read the full budget and every generation
|
|
6624
|
-
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
6625
|
-
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
6626
|
-
// untouched $50 and walked away with $75 between them.
|
|
6627
|
-
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
6628
|
-
slot: slot.slot,
|
|
6629
|
-
capUsd: slot.capUsd,
|
|
6630
|
-
capRemainingUsd: slot.capRemainingUsd
|
|
6631
|
-
};
|
|
6632
|
-
}
|
|
6633
|
-
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
6832
|
+
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
|
|
6634
6833
|
const retired = retiredCounterRefusal(rawInput);
|
|
6635
6834
|
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
6636
6835
|
if (!isToolInput20(rawInput)) {
|
|
6637
6836
|
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
6638
6837
|
}
|
|
6639
6838
|
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
6640
|
-
if (!handoffPath || !
|
|
6839
|
+
if (!handoffPath || !existsSync5(handoffPath)) {
|
|
6641
6840
|
return jsonContent({
|
|
6642
6841
|
tool: TOOL_NAME20,
|
|
6643
6842
|
schema_version: 1,
|
|
@@ -6662,27 +6861,52 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
6662
6861
|
}
|
|
6663
6862
|
});
|
|
6664
6863
|
}
|
|
6864
|
+
const platform = overrides.platform ?? process.platform;
|
|
6865
|
+
let resolvedBin = plan.bin;
|
|
6866
|
+
if (platform === "win32") {
|
|
6867
|
+
const resolution = resolveNativeWindowsExecutable(plan.bin, process.env, overrides.resolveWindowsExecutable);
|
|
6868
|
+
if (!resolution.ok) {
|
|
6869
|
+
return jsonContent({
|
|
6870
|
+
tool: TOOL_NAME20,
|
|
6871
|
+
schema_version: 1,
|
|
6872
|
+
payload: {
|
|
6873
|
+
spawned: false,
|
|
6874
|
+
reason: resolution.reason,
|
|
6875
|
+
agent: plan.agent,
|
|
6876
|
+
tier: plan.tier,
|
|
6877
|
+
handoff_path: handoffPath
|
|
6878
|
+
}
|
|
6879
|
+
});
|
|
6880
|
+
}
|
|
6881
|
+
resolvedBin = resolution.bin;
|
|
6882
|
+
}
|
|
6665
6883
|
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
6666
6884
|
mkdirSync4(logDir, { recursive: true });
|
|
6667
6885
|
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
6668
6886
|
const logFd = openSync2(logPath, "a");
|
|
6669
|
-
const child = spawnImpl(
|
|
6887
|
+
const child = spawnImpl(resolvedBin, [...plan.args], {
|
|
6670
6888
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
6671
6889
|
detached: true,
|
|
6672
6890
|
stdio: ["pipe", logFd, logFd],
|
|
6673
|
-
//
|
|
6674
|
-
//
|
|
6675
|
-
|
|
6891
|
+
// Never a shell: `resolvedBin` is either the bare platform-neutral name
|
|
6892
|
+
// (POSIX, resolved by the OS via PATH + shebang) or the native win32 exe
|
|
6893
|
+
// resolved above — routing either through cmd.exe/sh is the extra layer
|
|
6894
|
+
// a detached, unref'd child can lose silently (2026-08-16 incident).
|
|
6895
|
+
shell: false,
|
|
6676
6896
|
windowsHide: true,
|
|
6897
|
+
windowsVerbatimArguments: false,
|
|
6677
6898
|
// Carry the SAME binding to the child. Without this the successor inherits
|
|
6678
6899
|
// no tier and re-resolves its own — which is the split-payer defect one
|
|
6679
6900
|
// generation down.
|
|
6680
6901
|
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
6681
6902
|
});
|
|
6903
|
+
closeSync2(logFd);
|
|
6682
6904
|
let spawnError = null;
|
|
6683
6905
|
child.on("error", (e) => {
|
|
6684
6906
|
spawnError = e.message;
|
|
6685
6907
|
});
|
|
6908
|
+
child.stdin.on?.("error", () => {
|
|
6909
|
+
});
|
|
6686
6910
|
try {
|
|
6687
6911
|
child.stdin.write(prompt);
|
|
6688
6912
|
child.stdin.end();
|
|
@@ -6690,10 +6914,40 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
6690
6914
|
}
|
|
6691
6915
|
child.unref();
|
|
6692
6916
|
await new Promise((r) => setTimeout(r, 150));
|
|
6917
|
+
if (spawnError) {
|
|
6918
|
+
return jsonContent({
|
|
6919
|
+
tool: TOOL_NAME20,
|
|
6920
|
+
schema_version: 1,
|
|
6921
|
+
payload: {
|
|
6922
|
+
spawned: false,
|
|
6923
|
+
reason: `spawn failed: ${spawnError}`,
|
|
6924
|
+
agent: plan.agent,
|
|
6925
|
+
tier: plan.tier,
|
|
6926
|
+
handoff_path: handoffPath
|
|
6927
|
+
}
|
|
6928
|
+
});
|
|
6929
|
+
}
|
|
6930
|
+
const checkLiveness = overrides.checkLiveness ?? ((c, p) => checkSuccessorLiveness(c, p, resolveLivenessConfigFromEnv(process.env, overrides.livenessConfig), overrides.livenessDeps));
|
|
6931
|
+
const liveness = await checkLiveness(child, logPath);
|
|
6932
|
+
if (!liveness.ok) {
|
|
6933
|
+
return jsonContent({
|
|
6934
|
+
tool: TOOL_NAME20,
|
|
6935
|
+
schema_version: 1,
|
|
6936
|
+
payload: {
|
|
6937
|
+
spawned: false,
|
|
6938
|
+
reason: liveness.reason,
|
|
6939
|
+
pid: child.pid ?? null,
|
|
6940
|
+
log_path: logPath,
|
|
6941
|
+
agent: plan.agent,
|
|
6942
|
+
tier: plan.tier,
|
|
6943
|
+
handoff_path: handoffPath
|
|
6944
|
+
}
|
|
6945
|
+
});
|
|
6946
|
+
}
|
|
6693
6947
|
return jsonContent({
|
|
6694
6948
|
tool: TOOL_NAME20,
|
|
6695
6949
|
schema_version: 1,
|
|
6696
|
-
payload:
|
|
6950
|
+
payload: {
|
|
6697
6951
|
spawned: true,
|
|
6698
6952
|
pid: child.pid ?? null,
|
|
6699
6953
|
log_path: logPath,
|
|
@@ -6705,7 +6959,10 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
6705
6959
|
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
6706
6960
|
// against the pool without reading the ledger directory by hand.
|
|
6707
6961
|
ledger_cap_usd: plan.capUsd,
|
|
6708
|
-
ledger_cap_remaining_usd: plan.capRemainingUsd
|
|
6962
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd,
|
|
6963
|
+
// Additive (2026-08-17): true only once the child survived the
|
|
6964
|
+
// early-exit window (and the output window, when that gate is enabled).
|
|
6965
|
+
verified_alive: true
|
|
6709
6966
|
}
|
|
6710
6967
|
});
|
|
6711
6968
|
}
|
|
@@ -6889,10 +7146,10 @@ async function getCloudAuth(fetchFn) {
|
|
|
6889
7146
|
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
6890
7147
|
return { ok: true, controlPlaneUrl, token };
|
|
6891
7148
|
}
|
|
6892
|
-
async function callPrivateKnowledge(
|
|
7149
|
+
async function callPrivateKnowledge(path4, body, fetchFn) {
|
|
6893
7150
|
const auth = await getCloudAuth(fetchFn);
|
|
6894
7151
|
if (!auth.ok) return { ok: false, reason: auth.reason };
|
|
6895
|
-
const response = await fetchFn(`${auth.controlPlaneUrl}${
|
|
7152
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
|
|
6896
7153
|
method: "POST",
|
|
6897
7154
|
headers: {
|
|
6898
7155
|
authorization: `Bearer ${auth.token}`,
|
|
@@ -7110,11 +7367,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
7110
7367
|
}
|
|
7111
7368
|
|
|
7112
7369
|
// src/tools/skills/skill-corpus.ts
|
|
7113
|
-
import { existsSync as
|
|
7370
|
+
import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
|
|
7114
7371
|
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
7115
7372
|
|
|
7116
7373
|
// ../skill-registry/src/loader.ts
|
|
7117
|
-
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as
|
|
7374
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
|
|
7118
7375
|
import { join as join12 } from "node:path";
|
|
7119
7376
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
7120
7377
|
constructor(skillFile, reason) {
|
|
@@ -7171,7 +7428,7 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
7171
7428
|
const entryPath = join12(skillsDir, entry);
|
|
7172
7429
|
let stat;
|
|
7173
7430
|
try {
|
|
7174
|
-
stat =
|
|
7431
|
+
stat = statSync6(entryPath);
|
|
7175
7432
|
} catch {
|
|
7176
7433
|
continue;
|
|
7177
7434
|
}
|
|
@@ -7221,12 +7478,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
7221
7478
|
const override = env.VO_SKILLS_DIR;
|
|
7222
7479
|
if (typeof override === "string" && override.length > 0) {
|
|
7223
7480
|
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
7224
|
-
return
|
|
7481
|
+
return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
|
|
7225
7482
|
}
|
|
7226
7483
|
let dir = resolve2(startDir);
|
|
7227
7484
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
7228
7485
|
const candidate = join13(dir, ".claude", "skills");
|
|
7229
|
-
if (
|
|
7486
|
+
if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
|
|
7230
7487
|
const parent = dirname5(dir);
|
|
7231
7488
|
if (parent === dir) break;
|
|
7232
7489
|
dir = parent;
|
|
@@ -8435,7 +8692,7 @@ function createConsensusFallbackClient(primary, fallback, options = {}) {
|
|
|
8435
8692
|
}
|
|
8436
8693
|
|
|
8437
8694
|
// src/consensus/shadow-client.ts
|
|
8438
|
-
import { appendFileSync as appendFileSync2, chmodSync as chmodSync4, mkdirSync as mkdirSync8, renameSync, statSync as
|
|
8695
|
+
import { appendFileSync as appendFileSync2, chmodSync as chmodSync4, mkdirSync as mkdirSync8, renameSync, statSync as statSync8 } from "node:fs";
|
|
8439
8696
|
import { homedir as homedir8 } from "node:os";
|
|
8440
8697
|
import { dirname as dirname7, join as join14 } from "node:path";
|
|
8441
8698
|
var DEFAULT_SHADOW_TIMEOUT_MS = 2e4;
|
|
@@ -8444,17 +8701,17 @@ function defaultShadowReceiptPath(env = process.env) {
|
|
|
8444
8701
|
const p = (env["VO_MCP_MOAT_SHADOW_PATH"] ?? "").trim();
|
|
8445
8702
|
return p || join14(homedir8(), ".claude", "vo-mcp-moat-shadow.jsonl");
|
|
8446
8703
|
}
|
|
8447
|
-
function appendShadowReceipt(receipt,
|
|
8704
|
+
function appendShadowReceipt(receipt, path4 = defaultShadowReceiptPath()) {
|
|
8448
8705
|
try {
|
|
8449
|
-
mkdirSync8(dirname7(
|
|
8706
|
+
mkdirSync8(dirname7(path4), { recursive: true, mode: 448 });
|
|
8450
8707
|
try {
|
|
8451
|
-
if (
|
|
8708
|
+
if (statSync8(path4).size > SHADOW_RECEIPT_MAX_BYTES) renameSync(path4, `${path4}.1`);
|
|
8452
8709
|
} catch {
|
|
8453
8710
|
}
|
|
8454
|
-
appendFileSync2(
|
|
8711
|
+
appendFileSync2(path4, `${JSON.stringify(receipt)}
|
|
8455
8712
|
`, "utf8");
|
|
8456
8713
|
try {
|
|
8457
|
-
chmodSync4(
|
|
8714
|
+
chmodSync4(path4, 384);
|
|
8458
8715
|
} catch {
|
|
8459
8716
|
}
|
|
8460
8717
|
} catch {
|
|
@@ -8607,11 +8864,11 @@ function processCapture(rawBody, expectedState, store) {
|
|
|
8607
8864
|
return { ok: false, httpStatus: 400, error: "login response missing refresh_token / api_key" };
|
|
8608
8865
|
}
|
|
8609
8866
|
const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : void 0;
|
|
8610
|
-
const
|
|
8867
|
+
const path4 = store({ refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} });
|
|
8611
8868
|
return {
|
|
8612
8869
|
ok: true,
|
|
8613
8870
|
httpStatus: 200,
|
|
8614
|
-
result: { ...email ? { email } : {}, credentialPath:
|
|
8871
|
+
result: { ...email ? { email } : {}, credentialPath: path4 },
|
|
8615
8872
|
captured: { refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} }
|
|
8616
8873
|
};
|
|
8617
8874
|
}
|
|
@@ -8699,8 +8956,8 @@ async function runLogin(opts = {}) {
|
|
|
8699
8956
|
}
|
|
8700
8957
|
} catch {
|
|
8701
8958
|
}
|
|
8702
|
-
const
|
|
8703
|
-
result = { ...capt.email ? { email: capt.email } : {}, credentialPath:
|
|
8959
|
+
const path4 = writeNow(cred);
|
|
8960
|
+
result = { ...capt.email ? { email: capt.email } : {}, credentialPath: path4 };
|
|
8704
8961
|
}
|
|
8705
8962
|
res.writeHead(outcome.httpStatus, { "content-type": "text/html; charset=utf-8" });
|
|
8706
8963
|
res.end(outcome.ok ? "<h2>AlgoHQ login complete \u2014 you can close this tab.</h2>" : `<h2>Login failed: ${outcome.error}</h2>`);
|