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