@algosuite/vo-mcp 0.2.0-beta.0 → 0.2.0-beta.2
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/bin/vo-mcp +6 -1
- package/dist/pair-cli.js +214 -0
- package/dist/pair-cli.js.map +7 -0
- package/dist/runner-cli.js +584 -52
- package/dist/runner-cli.js.map +4 -4
- package/dist/set-key-cli.js +170 -0
- package/dist/set-key-cli.js.map +7 -0
- package/package.json +1 -1
package/dist/runner-cli.js
CHANGED
|
@@ -155,12 +155,65 @@ import fs from "node:fs";
|
|
|
155
155
|
function repoRoot() {
|
|
156
156
|
return process.env.VO_CODE_RUNNER_REPO || process.cwd();
|
|
157
157
|
}
|
|
158
|
+
function clonesRoot() {
|
|
159
|
+
return process.env.VO_CODE_RUNNER_CLONES_ROOT || "";
|
|
160
|
+
}
|
|
161
|
+
var VALID_REPO_SLUG = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
162
|
+
var TRACKED_WORKTREES = /* @__PURE__ */ new Map();
|
|
158
163
|
function sanitize(value, fallback) {
|
|
159
164
|
const cleaned = String(value || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
160
165
|
return cleaned || fallback;
|
|
161
166
|
}
|
|
167
|
+
function cloneDirForSlug(repoSlug, clonesRootDir) {
|
|
168
|
+
if (!clonesRootDir || !repoSlug || !VALID_REPO_SLUG.test(String(repoSlug))) return null;
|
|
169
|
+
const [owner, name] = String(repoSlug).split("/");
|
|
170
|
+
if (owner === "." || owner === ".." || name === "." || name === "..") return null;
|
|
171
|
+
if (owner.startsWith("-") || name.startsWith("-")) return null;
|
|
172
|
+
return path.join(clonesRootDir, `${sanitize(owner, "owner")}__${sanitize(name, "repo")}`);
|
|
173
|
+
}
|
|
174
|
+
function resolveTaskRoot(repoSlug) {
|
|
175
|
+
const root = clonesRoot();
|
|
176
|
+
if (root && !path.isAbsolute(root)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`[vo-mcp runner] VO_CODE_RUNNER_CLONES_ROOT must be an absolute path (got '${root}')`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const dir = cloneDirForSlug(repoSlug, root);
|
|
182
|
+
if (!dir) return { root: repoRoot(), multiRepo: false };
|
|
183
|
+
if (!fs.existsSync(path.join(dir, ".git"))) {
|
|
184
|
+
fs.mkdirSync(clonesRoot(), { recursive: true });
|
|
185
|
+
const [owner, name] = String(repoSlug).split("/");
|
|
186
|
+
const tmpDir = `${dir}.tmp-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
187
|
+
const cl = spawnSync(
|
|
188
|
+
"git",
|
|
189
|
+
["clone", "--no-tags", `https://github.com/${owner}/${name}.git`, tmpDir],
|
|
190
|
+
{ encoding: "utf8", timeout: 6e5 }
|
|
191
|
+
);
|
|
192
|
+
if (cl.status !== 0 || !fs.existsSync(path.join(tmpDir, ".git"))) {
|
|
193
|
+
try {
|
|
194
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
throw new Error(
|
|
198
|
+
`[vo-mcp runner] clone failed for ${repoSlug}: ${String(cl.stderr || cl.error || "").slice(0, 200)}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
fs.renameSync(tmpDir, dir);
|
|
203
|
+
} catch {
|
|
204
|
+
try {
|
|
205
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
if (!fs.existsSync(path.join(dir, ".git"))) {
|
|
209
|
+
throw new Error(`[vo-mcp runner] clone race left no usable clone for ${repoSlug}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { root: dir, multiRepo: true };
|
|
214
|
+
}
|
|
162
215
|
function createFixWorktree(kind, error = {}) {
|
|
163
|
-
const root =
|
|
216
|
+
const { root, multiRepo } = resolveTaskRoot(error.repo);
|
|
164
217
|
const safeKind = sanitize(kind, "task");
|
|
165
218
|
const safeTarget = sanitize(error.source || error.tester || "run", "run").slice(0, 24);
|
|
166
219
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -174,18 +227,23 @@ function createFixWorktree(kind, error = {}) {
|
|
|
174
227
|
{ cwd: root, encoding: "utf8", timeout: 12e4 }
|
|
175
228
|
);
|
|
176
229
|
if (add.status === 0 && fs.existsSync(worktreeDir)) {
|
|
230
|
+
TRACKED_WORKTREES.set(worktreeName, { worktreeDir, root });
|
|
177
231
|
return { worktreeDir, worktreeName };
|
|
178
232
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
233
|
+
const detail = String(add.stderr || add.error || "").slice(0, 200);
|
|
234
|
+
if (multiRepo) {
|
|
235
|
+
throw new Error(`[vo-mcp runner] worktree create failed for ${error.repo}: ${detail}`);
|
|
236
|
+
}
|
|
237
|
+
console.error(`[vo-mcp runner] worktree isolation unavailable: ${detail}`);
|
|
182
238
|
return { worktreeDir: root, worktreeName: "" };
|
|
183
239
|
}
|
|
184
240
|
function cleanupFixWorktree(worktreeName) {
|
|
185
241
|
if (!worktreeName) return;
|
|
186
|
-
const
|
|
187
|
-
const
|
|
242
|
+
const tracked = TRACKED_WORKTREES.get(worktreeName);
|
|
243
|
+
const root = tracked ? tracked.root : repoRoot();
|
|
244
|
+
const worktreeDir = tracked ? tracked.worktreeDir : path.join(root, ".agent-worktrees", worktreeName);
|
|
188
245
|
spawnSync("git", ["worktree", "remove", "--force", worktreeDir], { cwd: root, timeout: 12e4 });
|
|
246
|
+
TRACKED_WORKTREES.delete(worktreeName);
|
|
189
247
|
}
|
|
190
248
|
|
|
191
249
|
// src/runner/spend-cap-shim.mjs
|
|
@@ -293,20 +351,30 @@ function createControlPlaneClient({
|
|
|
293
351
|
},
|
|
294
352
|
/**
|
|
295
353
|
* Report this machine's rolling-7-day Claude Code token usage (the real
|
|
296
|
-
* weekly-capacity gauge)
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
354
|
+
* weekly-capacity gauge) PLUS the operator's real Claude weekly % (when
|
|
355
|
+
* available). The daemon authenticates as admin, so the target `operatorId`
|
|
356
|
+
* is named explicitly. Best-effort; throws on a non-2xx so the caller can
|
|
357
|
+
* log + move on.
|
|
358
|
+
*
|
|
359
|
+
* `tokens` = { input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens }.
|
|
360
|
+
* Optional: `claudeWeeklyPct` (number) + `claudeWeeklyResetsAt` (ISO string | null).
|
|
300
361
|
*/
|
|
301
|
-
async postWeeklyTokens({ operatorId, runnerId, tokens }) {
|
|
302
|
-
const
|
|
362
|
+
async postWeeklyTokens({ operatorId, runnerId, tokens, claudeWeeklyPct, claudeWeeklyResetsAt }) {
|
|
363
|
+
const body = {
|
|
303
364
|
operator_id: operatorId,
|
|
304
365
|
runner_id: runnerId,
|
|
305
366
|
input_tokens: tokens.input_tokens,
|
|
306
367
|
output_tokens: tokens.output_tokens,
|
|
307
368
|
cache_creation_tokens: tokens.cache_creation_tokens,
|
|
308
369
|
cache_read_tokens: tokens.cache_read_tokens
|
|
309
|
-
}
|
|
370
|
+
};
|
|
371
|
+
if (typeof claudeWeeklyPct === "number") {
|
|
372
|
+
body.claude_weekly_pct = claudeWeeklyPct;
|
|
373
|
+
}
|
|
374
|
+
if (claudeWeeklyResetsAt !== void 0) {
|
|
375
|
+
body.claude_weekly_resets_at = claudeWeeklyResetsAt;
|
|
376
|
+
}
|
|
377
|
+
const res = await req("POST", "/api/v1/weekly-tokens", body);
|
|
310
378
|
if (res.status === 401) {
|
|
311
379
|
cachedFirebaseToken = null;
|
|
312
380
|
throw new Error("weekly-tokens unauthorized (401)");
|
|
@@ -314,6 +382,52 @@ function createControlPlaneClient({
|
|
|
314
382
|
if (!res.ok) throw new Error(`weekly-tokens failed: HTTP ${res.status}`);
|
|
315
383
|
return true;
|
|
316
384
|
},
|
|
385
|
+
/**
|
|
386
|
+
* Send a liveness heartbeat (M2). The control-plane upserts it under the
|
|
387
|
+
* authenticated operator so the web shows a TRUE "runner online" signal.
|
|
388
|
+
* Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
|
|
389
|
+
*/
|
|
390
|
+
async postHeartbeat({ runnerId, uptimeSec, activeTasks, version }) {
|
|
391
|
+
const body = { runner_id: runnerId };
|
|
392
|
+
if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
|
|
393
|
+
if (typeof activeTasks === "number") body.active_tasks = activeTasks;
|
|
394
|
+
if (version) body.version = version;
|
|
395
|
+
const res = await req("POST", "/api/v1/runner/heartbeat", body);
|
|
396
|
+
if (res.status === 401) {
|
|
397
|
+
cachedFirebaseToken = null;
|
|
398
|
+
throw new Error("heartbeat unauthorized (401)");
|
|
399
|
+
}
|
|
400
|
+
if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
|
|
401
|
+
return true;
|
|
402
|
+
},
|
|
403
|
+
/**
|
|
404
|
+
* Mint a short-lived (~1h), repo-scoped GitHub App installation token for
|
|
405
|
+
* THIS runner's operator (M3). The control-plane keys the mint on the
|
|
406
|
+
* authenticated operator (ctx.operator_id), so the token covers only that
|
|
407
|
+
* operator's installation.
|
|
408
|
+
*
|
|
409
|
+
* Returns { token, expiresAt } on success, or `null` on ANY non-success —
|
|
410
|
+
* 503 (App not configured / dormant), 404 (operator has no installation),
|
|
411
|
+
* other non-2xx, or a network error. `null` is the signal to the caller to
|
|
412
|
+
* fall back to the runner's own ambient `gh` auth, so a dormant or
|
|
413
|
+
* unmapped App NEVER blocks a PR the runner could open itself. Never throws.
|
|
414
|
+
*
|
|
415
|
+
* The minted token is only usable for push + PR if the GitHub App grants
|
|
416
|
+
* BOTH `Contents: write` (git push) AND `Pull requests: write` (gh pr
|
|
417
|
+
* create) — see docs/vo/github-app-setup-2026-06-18.md. A token missing
|
|
418
|
+
* either scope fails at push (→ ambient fallback) or at `gh pr create`.
|
|
419
|
+
*/
|
|
420
|
+
async getInstallationToken() {
|
|
421
|
+
try {
|
|
422
|
+
const res = await req("POST", "/api/v1/github/installation-token", {});
|
|
423
|
+
if (!res.ok) return null;
|
|
424
|
+
const json = await res.json();
|
|
425
|
+
if (!json || !json.token) return null;
|
|
426
|
+
return { token: json.token, expiresAt: json.expires_at || null };
|
|
427
|
+
} catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
317
431
|
/**
|
|
318
432
|
* Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
|
|
319
433
|
* Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'ultracode'),
|
|
@@ -335,6 +449,39 @@ function createControlPlaneClient({
|
|
|
335
449
|
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
336
450
|
import { spawn } from "node:child_process";
|
|
337
451
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
452
|
+
|
|
453
|
+
// ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
|
|
454
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
455
|
+
var require2 = createRequire2(import.meta.url);
|
|
456
|
+
var KEY_SERVICE = "algosuite-vo";
|
|
457
|
+
var KEY_ACCOUNT = "anthropic-api-key";
|
|
458
|
+
var _entryCtor;
|
|
459
|
+
var _loadTried = false;
|
|
460
|
+
function defaultEntryCtor() {
|
|
461
|
+
if (_loadTried) return _entryCtor;
|
|
462
|
+
_loadTried = true;
|
|
463
|
+
try {
|
|
464
|
+
_entryCtor = require2("@napi-rs/keyring").Entry;
|
|
465
|
+
} catch {
|
|
466
|
+
_entryCtor = null;
|
|
467
|
+
}
|
|
468
|
+
return _entryCtor;
|
|
469
|
+
}
|
|
470
|
+
function getAnthropicKey({ EntryCtor = defaultEntryCtor() } = {}) {
|
|
471
|
+
if (!EntryCtor) return null;
|
|
472
|
+
try {
|
|
473
|
+
return new EntryCtor(KEY_SERVICE, KEY_ACCOUNT).getPassword() || null;
|
|
474
|
+
} catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey } = {}) {
|
|
479
|
+
if (baseEnv.ANTHROPIC_API_KEY) return { ...baseEnv };
|
|
480
|
+
const key = getKey();
|
|
481
|
+
return key ? { ...baseEnv, ANTHROPIC_API_KEY: key } : { ...baseEnv };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
338
485
|
var DEFAULT_PERMISSION_MODE = "acceptEdits";
|
|
339
486
|
function extractText(content) {
|
|
340
487
|
if (typeof content === "string") return content.trim();
|
|
@@ -386,10 +533,11 @@ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, m
|
|
|
386
533
|
}
|
|
387
534
|
return args;
|
|
388
535
|
}
|
|
389
|
-
function
|
|
536
|
+
function runAgentTask({
|
|
537
|
+
runner,
|
|
390
538
|
prompt,
|
|
391
539
|
cwd,
|
|
392
|
-
|
|
540
|
+
bin = runner.binary,
|
|
393
541
|
permissionMode,
|
|
394
542
|
maxTurns,
|
|
395
543
|
model,
|
|
@@ -402,16 +550,20 @@ function runClaudeTask({
|
|
|
402
550
|
spawnImpl = spawn
|
|
403
551
|
}) {
|
|
404
552
|
return new Promise((resolve) => {
|
|
405
|
-
const args =
|
|
406
|
-
const child = spawnImpl(
|
|
553
|
+
const args = runner.buildArgs({ permissionMode, maxTurns, model, prompt });
|
|
554
|
+
const child = spawnImpl(bin, args, {
|
|
407
555
|
cwd,
|
|
408
|
-
|
|
556
|
+
// BYO auth: each runner fills its provider's credential env var(s) from the
|
|
557
|
+
// OS keychain when not already set (Claude→ANTHROPIC_API_KEY via
|
|
558
|
+
// anthropic-key-store, Codex→OPENAI_API_KEY, …). Explicit env wins; no key
|
|
559
|
+
// stored → unchanged (the CLI's own login as before).
|
|
560
|
+
env: typeof runner.applyAuthEnv === "function" ? runner.applyAuthEnv(env2) : env2,
|
|
409
561
|
stdio: ["pipe", "pipe", "pipe"],
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
shell
|
|
414
|
-
|
|
562
|
+
// getSpawnOptions carries the per-runner shell/windowsHide posture. On
|
|
563
|
+
// Windows the CLI is a `.cmd` shim → spawn needs shell:true to resolve it
|
|
564
|
+
// (without it: ENOENT). Safe because the prompt goes via stdin below,
|
|
565
|
+
// never argv, so the shell never sees untrusted input.
|
|
566
|
+
...runner.getSpawnOptions()
|
|
415
567
|
});
|
|
416
568
|
try {
|
|
417
569
|
child.stdin.write(String(prompt));
|
|
@@ -446,7 +598,7 @@ function runClaudeTask({
|
|
|
446
598
|
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
447
599
|
const line = buffer.slice(0, nl);
|
|
448
600
|
buffer = buffer.slice(nl + 1);
|
|
449
|
-
const evt =
|
|
601
|
+
const evt = runner.parseEvent(line);
|
|
450
602
|
if (!evt) continue;
|
|
451
603
|
if (evt.kind === "progress") {
|
|
452
604
|
try {
|
|
@@ -499,7 +651,7 @@ function runClaudeTask({
|
|
|
499
651
|
return;
|
|
500
652
|
}
|
|
501
653
|
if (!result.summary && code !== 0) {
|
|
502
|
-
result.summary = stderrTail.slice(-500) ||
|
|
654
|
+
result.summary = stderrTail.slice(-500) || `${bin} exited ${code}`;
|
|
503
655
|
}
|
|
504
656
|
resolve({ ...result, ok: result.ok && code === 0 });
|
|
505
657
|
});
|
|
@@ -521,6 +673,14 @@ var ClaudeRunner = class {
|
|
|
521
673
|
windowsHide: true
|
|
522
674
|
};
|
|
523
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Fill ANTHROPIC_API_KEY from the OS keychain when not already set (M4 BYO),
|
|
678
|
+
* so a friend who ran `vo-mcp set-key` authenticates without an env var.
|
|
679
|
+
* Explicit env wins; no key stored → unchanged (Claude Code login as before).
|
|
680
|
+
*/
|
|
681
|
+
applyAuthEnv(env2 = process.env) {
|
|
682
|
+
return withAnthropicKey(env2);
|
|
683
|
+
}
|
|
524
684
|
/**
|
|
525
685
|
* Best-effort auth check: is `claude` on PATH and can we verify login?
|
|
526
686
|
* Never throws. If we can't cheaply detect auth, we return installed:true
|
|
@@ -564,6 +724,310 @@ var ClaudeRunner = class {
|
|
|
564
724
|
};
|
|
565
725
|
var claudeRunner = new ClaudeRunner();
|
|
566
726
|
|
|
727
|
+
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
728
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
729
|
+
|
|
730
|
+
// ../../scripts/virtual-office/code-runner/agent-key-store.mjs
|
|
731
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
732
|
+
var require3 = createRequire3(import.meta.url);
|
|
733
|
+
var KEY_SERVICE2 = "algosuite-vo";
|
|
734
|
+
var PROVIDER_ENV = {
|
|
735
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
736
|
+
openai: ["OPENAI_API_KEY", "CODEX_API_KEY"],
|
|
737
|
+
cursor: ["CURSOR_API_KEY"]
|
|
738
|
+
};
|
|
739
|
+
var PROVIDER_ALIAS = {
|
|
740
|
+
claude: "anthropic",
|
|
741
|
+
anthropic: "anthropic",
|
|
742
|
+
codex: "openai",
|
|
743
|
+
openai: "openai",
|
|
744
|
+
cursor: "cursor"
|
|
745
|
+
};
|
|
746
|
+
function resolveProvider(name) {
|
|
747
|
+
const key = String(name || "").trim().toLowerCase();
|
|
748
|
+
return PROVIDER_ALIAS[key] || null;
|
|
749
|
+
}
|
|
750
|
+
function accountFor(provider) {
|
|
751
|
+
return `${provider}-api-key`;
|
|
752
|
+
}
|
|
753
|
+
var _entryCtor2;
|
|
754
|
+
var _loadTried2 = false;
|
|
755
|
+
function defaultEntryCtor2() {
|
|
756
|
+
if (_loadTried2) return _entryCtor2;
|
|
757
|
+
_loadTried2 = true;
|
|
758
|
+
try {
|
|
759
|
+
_entryCtor2 = require3("@napi-rs/keyring").Entry;
|
|
760
|
+
} catch {
|
|
761
|
+
_entryCtor2 = null;
|
|
762
|
+
}
|
|
763
|
+
return _entryCtor2;
|
|
764
|
+
}
|
|
765
|
+
function getAgentKey(provider, { EntryCtor = defaultEntryCtor2() } = {}) {
|
|
766
|
+
const p = resolveProvider(provider);
|
|
767
|
+
if (!p || !EntryCtor) return null;
|
|
768
|
+
try {
|
|
769
|
+
return new EntryCtor(KEY_SERVICE2, accountFor(p)).getPassword() || null;
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function withAgentKey(provider, baseEnv = {}, { getKey = getAgentKey } = {}) {
|
|
775
|
+
const p = resolveProvider(provider);
|
|
776
|
+
const vars = p && PROVIDER_ENV[p] || [];
|
|
777
|
+
const out = { ...baseEnv };
|
|
778
|
+
if (!p || vars.length === 0) return out;
|
|
779
|
+
if (vars.some((v) => out[v])) return out;
|
|
780
|
+
const key = getKey(p);
|
|
781
|
+
if (!key) return out;
|
|
782
|
+
for (const v of vars) out[v] = key;
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
787
|
+
function buildCodexArgs({ model } = {}) {
|
|
788
|
+
const args = ["exec", "--json", "--full-auto"];
|
|
789
|
+
if (model) {
|
|
790
|
+
args.push("--model", String(model));
|
|
791
|
+
}
|
|
792
|
+
args.push("-");
|
|
793
|
+
return args;
|
|
794
|
+
}
|
|
795
|
+
function itemText(item) {
|
|
796
|
+
if (!item) return "";
|
|
797
|
+
if (typeof item.text === "string") return item.text;
|
|
798
|
+
if (typeof item.message === "string") return item.message;
|
|
799
|
+
if (Array.isArray(item.content)) {
|
|
800
|
+
return item.content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
801
|
+
}
|
|
802
|
+
return "";
|
|
803
|
+
}
|
|
804
|
+
function parseCodexEvent(line) {
|
|
805
|
+
const trimmed = String(line || "").trim();
|
|
806
|
+
if (!trimmed) return null;
|
|
807
|
+
let evt;
|
|
808
|
+
try {
|
|
809
|
+
evt = JSON.parse(trimmed);
|
|
810
|
+
} catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
if (!evt || typeof evt !== "object") return null;
|
|
814
|
+
const type = evt.type;
|
|
815
|
+
if (type === "item.completed" && evt.item) {
|
|
816
|
+
const it = evt.item.type;
|
|
817
|
+
if (it === "agent_message" || it === "assistant_message") {
|
|
818
|
+
const text = itemText(evt.item).trim();
|
|
819
|
+
return text ? { kind: "progress", text } : null;
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
if (type === "turn.completed") {
|
|
824
|
+
return { kind: "result", isError: false, costUsd: null, summary: "completed", numTurns: null };
|
|
825
|
+
}
|
|
826
|
+
if (type === "turn.failed" || type === "error") {
|
|
827
|
+
const msg = evt.error && (evt.error.message || evt.error) || evt.message || "codex run failed";
|
|
828
|
+
return { kind: "result", isError: true, costUsd: null, summary: String(msg), numTurns: null };
|
|
829
|
+
}
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
var CodexRunner = class {
|
|
833
|
+
get binary() {
|
|
834
|
+
return "codex";
|
|
835
|
+
}
|
|
836
|
+
buildArgs(opts = {}) {
|
|
837
|
+
return buildCodexArgs(opts);
|
|
838
|
+
}
|
|
839
|
+
parseEvent(line) {
|
|
840
|
+
return parseCodexEvent(line);
|
|
841
|
+
}
|
|
842
|
+
getSpawnOptions() {
|
|
843
|
+
return {
|
|
844
|
+
shell: process.platform === "win32",
|
|
845
|
+
windowsHide: true
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Fill the OpenAI credential env var(s) (OPENAI_API_KEY / CODEX_API_KEY) from
|
|
850
|
+
* the OS keychain when not already set, so a BYO friend who ran
|
|
851
|
+
* `vo-mcp set-key --provider codex` authenticates without an env var. Explicit
|
|
852
|
+
* env wins; no key stored → unchanged (a prior `codex login` still works).
|
|
853
|
+
*/
|
|
854
|
+
applyAuthEnv(env2 = process.env) {
|
|
855
|
+
return withAgentKey("openai", env2);
|
|
856
|
+
}
|
|
857
|
+
/** Best-effort: is `codex` on PATH? Never throws. */
|
|
858
|
+
async checkAuth() {
|
|
859
|
+
try {
|
|
860
|
+
const { status, error } = spawnSync3("codex", ["--version"], {
|
|
861
|
+
shell: process.platform === "win32",
|
|
862
|
+
windowsHide: true,
|
|
863
|
+
timeout: 3e3,
|
|
864
|
+
stdio: "ignore"
|
|
865
|
+
});
|
|
866
|
+
if (error) {
|
|
867
|
+
return { installed: false, authenticated: false, message: `codex not found on PATH: ${error.message}` };
|
|
868
|
+
}
|
|
869
|
+
if (status !== 0) {
|
|
870
|
+
return { installed: true, authenticated: false, message: "codex exists but --version failed (auth unclear)" };
|
|
871
|
+
}
|
|
872
|
+
return { installed: true, authenticated: true, message: "codex binary found (auth check is best-effort)" };
|
|
873
|
+
} catch (err) {
|
|
874
|
+
return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
var codexRunner = new CodexRunner();
|
|
879
|
+
|
|
880
|
+
// ../../scripts/virtual-office/code-runner/cursor-runner.mjs
|
|
881
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
882
|
+
function buildCursorArgs({ model, prompt } = {}) {
|
|
883
|
+
const args = ["-p", "--output-format", "stream-json", "--force"];
|
|
884
|
+
if (model) {
|
|
885
|
+
args.push("--model", String(model));
|
|
886
|
+
}
|
|
887
|
+
const p = String(prompt ?? "");
|
|
888
|
+
if (p.length > 0) {
|
|
889
|
+
args.push(p);
|
|
890
|
+
}
|
|
891
|
+
return args;
|
|
892
|
+
}
|
|
893
|
+
function messageText(message) {
|
|
894
|
+
if (!message) return "";
|
|
895
|
+
const content = message.content;
|
|
896
|
+
if (typeof content === "string") return content;
|
|
897
|
+
if (Array.isArray(content)) {
|
|
898
|
+
return content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
899
|
+
}
|
|
900
|
+
return "";
|
|
901
|
+
}
|
|
902
|
+
function parseCursorEvent(line) {
|
|
903
|
+
const trimmed = String(line || "").trim();
|
|
904
|
+
if (!trimmed) return null;
|
|
905
|
+
let evt;
|
|
906
|
+
try {
|
|
907
|
+
evt = JSON.parse(trimmed);
|
|
908
|
+
} catch {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
if (!evt || typeof evt !== "object") return null;
|
|
912
|
+
if (evt.type === "assistant") {
|
|
913
|
+
const text = messageText(evt.message).trim();
|
|
914
|
+
return text ? { kind: "progress", text } : null;
|
|
915
|
+
}
|
|
916
|
+
if (evt.type === "result") {
|
|
917
|
+
const isError = Boolean(evt.is_error) || evt.subtype === "error";
|
|
918
|
+
return {
|
|
919
|
+
kind: "result",
|
|
920
|
+
isError,
|
|
921
|
+
costUsd: null,
|
|
922
|
+
summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
|
|
923
|
+
numTurns: null
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
var CursorRunner = class {
|
|
929
|
+
get binary() {
|
|
930
|
+
return "cursor-agent";
|
|
931
|
+
}
|
|
932
|
+
buildArgs(opts = {}) {
|
|
933
|
+
return buildCursorArgs(opts);
|
|
934
|
+
}
|
|
935
|
+
parseEvent(line) {
|
|
936
|
+
return parseCursorEvent(line);
|
|
937
|
+
}
|
|
938
|
+
getSpawnOptions() {
|
|
939
|
+
return {
|
|
940
|
+
shell: process.platform === "win32",
|
|
941
|
+
windowsHide: true
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Fill CURSOR_API_KEY from the OS keychain when not already set, so a BYO
|
|
946
|
+
* friend who ran `vo-mcp set-key --provider cursor` authenticates without an
|
|
947
|
+
* env var. Explicit env wins; no key stored → a prior `cursor-agent login`.
|
|
948
|
+
*/
|
|
949
|
+
applyAuthEnv(env2 = process.env) {
|
|
950
|
+
return withAgentKey("cursor", env2);
|
|
951
|
+
}
|
|
952
|
+
/** Best-effort: is `cursor-agent` on PATH? Never throws. */
|
|
953
|
+
async checkAuth() {
|
|
954
|
+
try {
|
|
955
|
+
const { status, error } = spawnSync4("cursor-agent", ["--version"], {
|
|
956
|
+
shell: process.platform === "win32",
|
|
957
|
+
windowsHide: true,
|
|
958
|
+
timeout: 3e3,
|
|
959
|
+
stdio: "ignore"
|
|
960
|
+
});
|
|
961
|
+
if (error) {
|
|
962
|
+
return { installed: false, authenticated: false, message: `cursor-agent not found on PATH: ${error.message}` };
|
|
963
|
+
}
|
|
964
|
+
if (status !== 0) {
|
|
965
|
+
return { installed: true, authenticated: false, message: "cursor-agent exists but --version failed (auth unclear)" };
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
installed: true,
|
|
969
|
+
authenticated: true,
|
|
970
|
+
message: "cursor-agent found (EXPERIMENTAL: headless mode may need a TTY; auth check is best-effort)"
|
|
971
|
+
};
|
|
972
|
+
} catch (err) {
|
|
973
|
+
return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
var cursorRunner = new CursorRunner();
|
|
978
|
+
|
|
979
|
+
// ../../scripts/virtual-office/code-runner/agent-runner-interface.mjs
|
|
980
|
+
function validateAgentRunner(runner) {
|
|
981
|
+
if (!runner || typeof runner !== "object") {
|
|
982
|
+
throw new TypeError("AgentRunner must be an object");
|
|
983
|
+
}
|
|
984
|
+
if (typeof runner.binary !== "string" || runner.binary.length === 0) {
|
|
985
|
+
throw new TypeError("AgentRunner.binary must be a non-empty string");
|
|
986
|
+
}
|
|
987
|
+
if (typeof runner.buildArgs !== "function") {
|
|
988
|
+
throw new TypeError("AgentRunner.buildArgs must be a function");
|
|
989
|
+
}
|
|
990
|
+
if (typeof runner.parseEvent !== "function") {
|
|
991
|
+
throw new TypeError("AgentRunner.parseEvent must be a function");
|
|
992
|
+
}
|
|
993
|
+
if (typeof runner.getSpawnOptions !== "function") {
|
|
994
|
+
throw new TypeError("AgentRunner.getSpawnOptions must be a function");
|
|
995
|
+
}
|
|
996
|
+
if (typeof runner.checkAuth !== "function") {
|
|
997
|
+
throw new TypeError("AgentRunner.checkAuth must be a function");
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ../../scripts/virtual-office/code-runner/resolve-runner.mjs
|
|
1002
|
+
var DEFAULT_AGENT = "claude";
|
|
1003
|
+
var RUNNERS = {
|
|
1004
|
+
claude: claudeRunner,
|
|
1005
|
+
codex: codexRunner,
|
|
1006
|
+
cursor: cursorRunner
|
|
1007
|
+
};
|
|
1008
|
+
function listAgents() {
|
|
1009
|
+
return Object.keys(RUNNERS);
|
|
1010
|
+
}
|
|
1011
|
+
function resolveRunner(env2 = process.env, { warn = () => {
|
|
1012
|
+
} } = {}) {
|
|
1013
|
+
const raw = String(env2.VO_CODE_RUNNER_AGENT || env2.VO_AGENT || DEFAULT_AGENT).trim().toLowerCase();
|
|
1014
|
+
let agent = raw;
|
|
1015
|
+
let fellBack = false;
|
|
1016
|
+
let runner = RUNNERS[agent];
|
|
1017
|
+
if (!runner) {
|
|
1018
|
+
try {
|
|
1019
|
+
warn(`unknown VO_CODE_RUNNER_AGENT "${raw}"; falling back to "${DEFAULT_AGENT}" (known: ${listAgents().join(", ")})`);
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
agent = DEFAULT_AGENT;
|
|
1023
|
+
runner = RUNNERS[DEFAULT_AGENT];
|
|
1024
|
+
fellBack = true;
|
|
1025
|
+
}
|
|
1026
|
+
validateAgentRunner(runner);
|
|
1027
|
+
const runnerBin = env2.VO_CODE_RUNNER_BIN || (agent === "claude" ? env2.VO_CODE_RUNNER_CLAUDE_BIN : "") || runner.binary;
|
|
1028
|
+
return { agent, runner, runnerBin, fellBack };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
567
1031
|
// ../../scripts/virtual-office/code-runner/rate-limit-resume.mjs
|
|
568
1032
|
import { appendFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
569
1033
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -664,9 +1128,9 @@ function classifyFailureForResume({
|
|
|
664
1128
|
}
|
|
665
1129
|
|
|
666
1130
|
// ../../scripts/virtual-office/code-runner/publish.mjs
|
|
667
|
-
import { spawnSync as
|
|
668
|
-
function run(cmd, args, cwd, { timeout = 18e4, raw = false } = {}) {
|
|
669
|
-
const r =
|
|
1131
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1132
|
+
function run(cmd, args, cwd, { timeout = 18e4, raw = false, env: env2 } = {}) {
|
|
1133
|
+
const r = spawnSync5(cmd, args, { cwd, encoding: "utf8", timeout, ...env2 ? { env: env2 } : {} });
|
|
670
1134
|
if (r.error) throw r.error;
|
|
671
1135
|
if (r.status !== 0) {
|
|
672
1136
|
throw new Error(`${cmd} ${args[0]} failed (exit ${r.status}): ${(r.stderr || "").slice(-300)}`);
|
|
@@ -725,6 +1189,44 @@ function listCommittedFiles(cwd, base = "origin/main") {
|
|
|
725
1189
|
function compactTitle(s, max = 100) {
|
|
726
1190
|
return String(s || "").replace(/\s+/g, " ").trim().slice(0, max) || "code-task";
|
|
727
1191
|
}
|
|
1192
|
+
function installationTokenEnv(githubToken, baseEnv = process.env) {
|
|
1193
|
+
if (!githubToken) return void 0;
|
|
1194
|
+
return { ...baseEnv, GH_TOKEN: githubToken, GITHUB_TOKEN: githubToken };
|
|
1195
|
+
}
|
|
1196
|
+
function pushArgs(branch, { withToken = false } = {}) {
|
|
1197
|
+
if (withToken) {
|
|
1198
|
+
return [
|
|
1199
|
+
"-c",
|
|
1200
|
+
"credential.helper=",
|
|
1201
|
+
"-c",
|
|
1202
|
+
"credential.helper=!gh auth git-credential",
|
|
1203
|
+
"push",
|
|
1204
|
+
"origin",
|
|
1205
|
+
branch
|
|
1206
|
+
];
|
|
1207
|
+
}
|
|
1208
|
+
return ["push", "origin", branch];
|
|
1209
|
+
}
|
|
1210
|
+
function pushPlan(branch, githubToken) {
|
|
1211
|
+
if (githubToken) {
|
|
1212
|
+
return {
|
|
1213
|
+
primary: { args: pushArgs(branch, { withToken: true }), env: installationTokenEnv(githubToken), tokenUsed: true },
|
|
1214
|
+
fallback: { args: pushArgs(branch), env: void 0, tokenUsed: false }
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
return { primary: { args: pushArgs(branch), env: void 0, tokenUsed: false }, fallback: null };
|
|
1218
|
+
}
|
|
1219
|
+
function pushBranch(worktreeDir, branch, githubToken, runFn = run) {
|
|
1220
|
+
const { primary, fallback } = pushPlan(branch, githubToken);
|
|
1221
|
+
try {
|
|
1222
|
+
runFn("git", primary.args, worktreeDir, { env: primary.env });
|
|
1223
|
+
return primary.tokenUsed;
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
if (!fallback) throw err;
|
|
1226
|
+
runFn("git", fallback.args, worktreeDir, { env: fallback.env });
|
|
1227
|
+
return fallback.tokenUsed;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
728
1230
|
function openCodeTaskPr(worktreeDir, files, {
|
|
729
1231
|
title,
|
|
730
1232
|
body,
|
|
@@ -734,7 +1236,12 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
734
1236
|
maxFiles = 200,
|
|
735
1237
|
// Safety net: the agent already COMMITTED its work to a branch (despite the
|
|
736
1238
|
// preamble). Skip add+commit; just push the existing branch and open the PR.
|
|
737
|
-
alreadyCommitted = false
|
|
1239
|
+
alreadyCommitted = false,
|
|
1240
|
+
// Optional GitHub App installation token (M3). When present, the push + PR
|
|
1241
|
+
// authenticate as the App (the runner needs no repo write access of its own);
|
|
1242
|
+
// a push failure transparently falls back to the runner's ambient gh auth.
|
|
1243
|
+
// Absent → today's behavior (ambient gh) exactly.
|
|
1244
|
+
githubToken = null
|
|
738
1245
|
} = {}) {
|
|
739
1246
|
if (!Array.isArray(files) || files.length === 0) {
|
|
740
1247
|
throw new Error("openCodeTaskPr: no files to commit");
|
|
@@ -751,13 +1258,14 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
751
1258
|
branch = run("git", ["branch", "--show-current"], worktreeDir, { timeout: 3e4 });
|
|
752
1259
|
} catch {
|
|
753
1260
|
}
|
|
1261
|
+
let tokenUsed;
|
|
754
1262
|
if (alreadyCommitted) {
|
|
755
1263
|
if (!branch || branch === "main" || branch === "HEAD") {
|
|
756
1264
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
757
1265
|
branch = `${branchPrefix}-${stamp}`;
|
|
758
1266
|
run("git", ["checkout", "-b", branch], worktreeDir);
|
|
759
1267
|
}
|
|
760
|
-
|
|
1268
|
+
tokenUsed = pushBranch(worktreeDir, branch, githubToken);
|
|
761
1269
|
} else {
|
|
762
1270
|
if (!branch || branch === "main" || branch === "HEAD") {
|
|
763
1271
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -770,7 +1278,7 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
770
1278
|
}
|
|
771
1279
|
const commitMsg = compactTitle(title, 180);
|
|
772
1280
|
run("git", ["commit", "--no-verify", "-m", commitMsg], worktreeDir);
|
|
773
|
-
|
|
1281
|
+
tokenUsed = pushBranch(worktreeDir, branch, githubToken);
|
|
774
1282
|
}
|
|
775
1283
|
const out = run(
|
|
776
1284
|
"gh",
|
|
@@ -786,7 +1294,8 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
786
1294
|
"--body",
|
|
787
1295
|
String(body || "")
|
|
788
1296
|
],
|
|
789
|
-
worktreeDir
|
|
1297
|
+
worktreeDir,
|
|
1298
|
+
{ env: tokenUsed ? installationTokenEnv(githubToken) : void 0 }
|
|
790
1299
|
);
|
|
791
1300
|
const m = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
|
|
792
1301
|
if (!m) throw new Error("gh pr create returned no parseable PR URL");
|
|
@@ -973,14 +1482,37 @@ async function forwardSessionSpool(deps) {
|
|
|
973
1482
|
return { forwarded, pruned };
|
|
974
1483
|
}
|
|
975
1484
|
|
|
1485
|
+
// ../../scripts/virtual-office/code-runner/loop-ticks.mjs
|
|
1486
|
+
var HEARTBEAT_MS = 6e4;
|
|
1487
|
+
function makeLoopTicks({ client, cfg, env: env2, log: log2, getActive }) {
|
|
1488
|
+
let lastSessionForward = 0;
|
|
1489
|
+
let lastHeartbeat = 0;
|
|
1490
|
+
return function tick() {
|
|
1491
|
+
const now = Date.now();
|
|
1492
|
+
if (cfg.sessionForwardSec > 0 && now - lastSessionForward >= cfg.sessionForwardSec * 1e3) {
|
|
1493
|
+
lastSessionForward = now;
|
|
1494
|
+
forwardSessionSpool({
|
|
1495
|
+
baseUrl: String(env2.VO_CONTROL_PLANE_URL || "").replace(/\/$/, ""),
|
|
1496
|
+
token: env2.VO_CONTROL_PLANE_ADMIN_TOKEN || "",
|
|
1497
|
+
operatorSeed: cfg.operatorSeed
|
|
1498
|
+
}).catch(() => {
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
if (now - lastHeartbeat >= HEARTBEAT_MS) {
|
|
1502
|
+
lastHeartbeat = now;
|
|
1503
|
+
client.postHeartbeat({ runnerId: cfg.runnerId, uptimeSec: Math.floor(process.uptime()), activeTasks: getActive() }).catch((e) => log2(`heartbeat failed: ${e.message}`));
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
976
1508
|
// ../../scripts/virtual-office/code-runner/pr-watcher.mjs
|
|
977
1509
|
import { homedir as homedir4 } from "node:os";
|
|
978
1510
|
import { join as join4 } from "node:path";
|
|
979
1511
|
import { readFile as readFile2, writeFile as writeFile2, mkdir } from "node:fs/promises";
|
|
980
|
-
import { spawnSync as
|
|
1512
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
981
1513
|
var CI_FIX_MARKER = "[VO-CI-FIX]";
|
|
982
1514
|
function ghViewPr(prNumber, repo) {
|
|
983
|
-
const r =
|
|
1515
|
+
const r = spawnSync6(
|
|
984
1516
|
"gh",
|
|
985
1517
|
["pr", "view", String(prNumber), "-R", repo, "--json", "state,statusCheckRollup,headRefName"],
|
|
986
1518
|
{ encoding: "utf8", timeout: 3e4 }
|
|
@@ -1618,7 +2150,8 @@ var parseList = (s) => String(s || "").split(/[\s,]+/).map((x) => x.trim()).filt
|
|
|
1618
2150
|
function loadConfig(env2 = process.env) {
|
|
1619
2151
|
return {
|
|
1620
2152
|
runnerId: env2.VO_CODE_RUNNER_ID || `vo-code-runner-${os.hostname()}`,
|
|
1621
|
-
|
|
2153
|
+
// BYO multi-agent: {agent, runner, runnerBin} — VO_CODE_RUNNER_AGENT (claude|codex).
|
|
2154
|
+
...resolveRunner(env2, { warn: (m) => log(`agent-select: ${m}`) }),
|
|
1622
2155
|
permissionMode: env2.VO_CODE_RUNNER_PERMISSION_MODE || "acceptEdits",
|
|
1623
2156
|
maxConcurrency: Math.max(1, Number(env2.VO_CODE_TASK_MAX_CONCURRENCY || 2) || 2),
|
|
1624
2157
|
pollSec: Math.max(1, Number(env2.VO_CODE_RUNNER_POLL_SEC || 5) || 5),
|
|
@@ -1689,15 +2222,16 @@ async function processOneTask(client, task, cfg) {
|
|
|
1689
2222
|
const id = task.code_task_id;
|
|
1690
2223
|
let worktreeName = "";
|
|
1691
2224
|
try {
|
|
1692
|
-
const wt = createFixWorktree("code-task", { source: id.slice(0, 8) });
|
|
2225
|
+
const wt = createFixWorktree("code-task", { source: id.slice(0, 8), repo: task.repo });
|
|
1693
2226
|
worktreeName = wt.worktreeName;
|
|
1694
2227
|
const { dispatchMode, tier, model, permissionMode: effectivePermissionMode, maxTurns: effectiveMaxTurns, prompt: effortPrompt } = await resolveEffortDispatch({ client, task, env: process.env, basePrompt: composeDispatchPrompt(task.prompt, { repo: task.repo }) });
|
|
1695
|
-
await safeProgress(client, id, { message: `${cfg.runnerId} spawning ${model} (${tier}, effort ${dispatchMode})` });
|
|
2228
|
+
await safeProgress(client, id, { message: `${cfg.runnerId} spawning ${cfg.agent}:${model} (${tier}, effort ${dispatchMode})` });
|
|
1696
2229
|
const cap = typeof task.max_budget_usd === "number" ? task.max_budget_usd : resolveSpendCapUsd();
|
|
1697
|
-
const run2 = await
|
|
2230
|
+
const run2 = await runAgentTask({
|
|
2231
|
+
runner: cfg.runner,
|
|
2232
|
+
bin: cfg.runnerBin,
|
|
1698
2233
|
prompt: effortPrompt,
|
|
1699
2234
|
cwd: wt.worktreeDir,
|
|
1700
|
-
claudeBin: cfg.claudeBin,
|
|
1701
2235
|
permissionMode: effectivePermissionMode,
|
|
1702
2236
|
maxTurns: effectiveMaxTurns,
|
|
1703
2237
|
model,
|
|
@@ -1769,10 +2303,12 @@ async function processOneTask(client, task, cfg) {
|
|
|
1769
2303
|
return;
|
|
1770
2304
|
}
|
|
1771
2305
|
await safeProgress(client, id, { message: `opening PR for ${files.length} changed file(s)` });
|
|
2306
|
+
const githubToken = (await client.getInstallationToken())?.token ?? null;
|
|
1772
2307
|
const pr = openCodeTaskPr(wt.worktreeDir, files, {
|
|
1773
2308
|
title: `code-task: ${task.prompt}`,
|
|
1774
2309
|
body: buildPrBody(task, run2, files),
|
|
1775
|
-
alreadyCommitted
|
|
2310
|
+
alreadyCommitted,
|
|
2311
|
+
githubToken
|
|
1776
2312
|
});
|
|
1777
2313
|
await safeProgress(client, id, {
|
|
1778
2314
|
status: "pr_opened",
|
|
@@ -1826,25 +2362,17 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
1826
2362
|
log
|
|
1827
2363
|
});
|
|
1828
2364
|
log(
|
|
1829
|
-
`up as ${cfg.runnerId} \u2192 ${env2.VO_CONTROL_PLANE_URL} (concurrency ${cfg.maxConcurrency}, poll ${cfg.pollSec}s, once=${once2})`
|
|
2365
|
+
`up as ${cfg.runnerId} \u2192 ${env2.VO_CONTROL_PLANE_URL} (agent ${cfg.agent} [${cfg.runnerBin}], concurrency ${cfg.maxConcurrency}, poll ${cfg.pollSec}s, once=${once2})`
|
|
1830
2366
|
);
|
|
1831
2367
|
for (const line of describeClaimScoping(cfg, env2)) log(line);
|
|
1832
2368
|
log(
|
|
1833
2369
|
cfg.watchEnabled ? `PR watcher ON \u2014 auto-fix ${cfg.watchMaxFix}/PR on CI failure, never auto-merges, every ${cfg.watchIntervalSec}s (VO_CODE_RUNNER_WATCH=0 to disable)` : "PR watcher OFF (VO_CODE_RUNNER_WATCH=0)"
|
|
1834
2370
|
);
|
|
1835
|
-
let lastSessionForward = 0;
|
|
1836
2371
|
let lastWatchCycle = 0;
|
|
1837
2372
|
const runWatch = makeWatchRunner({ client, log, maxFixAttempts: cfg.watchMaxFix });
|
|
2373
|
+
const loopTick = makeLoopTicks({ client, cfg, env: env2, log, getActive: () => active });
|
|
1838
2374
|
while (!stopping) {
|
|
1839
|
-
|
|
1840
|
-
lastSessionForward = Date.now();
|
|
1841
|
-
forwardSessionSpool({
|
|
1842
|
-
baseUrl: String(env2.VO_CONTROL_PLANE_URL || "").replace(/\/$/, ""),
|
|
1843
|
-
token: env2.VO_CONTROL_PLANE_ADMIN_TOKEN || "",
|
|
1844
|
-
operatorSeed: cfg.operatorSeed
|
|
1845
|
-
}).catch(() => {
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
2375
|
+
loopTick();
|
|
1848
2376
|
if (cfg.watchEnabled && Date.now() - lastWatchCycle >= cfg.watchIntervalSec * 1e3) {
|
|
1849
2377
|
lastWatchCycle = Date.now();
|
|
1850
2378
|
runWatch().then((r) => {
|
|
@@ -1888,7 +2416,11 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
1888
2416
|
if (controlServer) controlServer.close();
|
|
1889
2417
|
log("stopped");
|
|
1890
2418
|
}
|
|
1891
|
-
var invokedDirectly = process.argv[1] && fileURLToPath2(import.meta.url) === process.argv[1]
|
|
2419
|
+
var invokedDirectly = process.argv[1] && fileURLToPath2(import.meta.url) === process.argv[1] && // Bundle-safe: only self-start when THIS file is the real entry, not when the
|
|
2420
|
+
// module is inlined into a bundle (e.g. @algosuite/vo-mcp's dist/runner-cli.js,
|
|
2421
|
+
// which calls main() itself — without this, `node dist/runner-cli.js` would
|
|
2422
|
+
// start a SECOND daemon loop and double-claim tasks).
|
|
2423
|
+
import.meta.url.endsWith("code-runner-daemon.mjs");
|
|
1892
2424
|
if (invokedDirectly) {
|
|
1893
2425
|
const once2 = process.argv.includes("--once");
|
|
1894
2426
|
main({ once: once2 }).catch((err) => {
|