@algosuite/vo-mcp 0.2.0-beta.1 → 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 +562 -44
- 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
|
|
@@ -324,6 +382,52 @@ function createControlPlaneClient({
|
|
|
324
382
|
if (!res.ok) throw new Error(`weekly-tokens failed: HTTP ${res.status}`);
|
|
325
383
|
return true;
|
|
326
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
|
+
},
|
|
327
431
|
/**
|
|
328
432
|
* Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
|
|
329
433
|
* Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'ultracode'),
|
|
@@ -345,6 +449,39 @@ function createControlPlaneClient({
|
|
|
345
449
|
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
346
450
|
import { spawn } from "node:child_process";
|
|
347
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
|
|
348
485
|
var DEFAULT_PERMISSION_MODE = "acceptEdits";
|
|
349
486
|
function extractText(content) {
|
|
350
487
|
if (typeof content === "string") return content.trim();
|
|
@@ -396,10 +533,11 @@ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, m
|
|
|
396
533
|
}
|
|
397
534
|
return args;
|
|
398
535
|
}
|
|
399
|
-
function
|
|
536
|
+
function runAgentTask({
|
|
537
|
+
runner,
|
|
400
538
|
prompt,
|
|
401
539
|
cwd,
|
|
402
|
-
|
|
540
|
+
bin = runner.binary,
|
|
403
541
|
permissionMode,
|
|
404
542
|
maxTurns,
|
|
405
543
|
model,
|
|
@@ -412,16 +550,20 @@ function runClaudeTask({
|
|
|
412
550
|
spawnImpl = spawn
|
|
413
551
|
}) {
|
|
414
552
|
return new Promise((resolve) => {
|
|
415
|
-
const args =
|
|
416
|
-
const child = spawnImpl(
|
|
553
|
+
const args = runner.buildArgs({ permissionMode, maxTurns, model, prompt });
|
|
554
|
+
const child = spawnImpl(bin, args, {
|
|
417
555
|
cwd,
|
|
418
|
-
|
|
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,
|
|
419
561
|
stdio: ["pipe", "pipe", "pipe"],
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
shell
|
|
424
|
-
|
|
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()
|
|
425
567
|
});
|
|
426
568
|
try {
|
|
427
569
|
child.stdin.write(String(prompt));
|
|
@@ -456,7 +598,7 @@ function runClaudeTask({
|
|
|
456
598
|
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
457
599
|
const line = buffer.slice(0, nl);
|
|
458
600
|
buffer = buffer.slice(nl + 1);
|
|
459
|
-
const evt =
|
|
601
|
+
const evt = runner.parseEvent(line);
|
|
460
602
|
if (!evt) continue;
|
|
461
603
|
if (evt.kind === "progress") {
|
|
462
604
|
try {
|
|
@@ -509,7 +651,7 @@ function runClaudeTask({
|
|
|
509
651
|
return;
|
|
510
652
|
}
|
|
511
653
|
if (!result.summary && code !== 0) {
|
|
512
|
-
result.summary = stderrTail.slice(-500) ||
|
|
654
|
+
result.summary = stderrTail.slice(-500) || `${bin} exited ${code}`;
|
|
513
655
|
}
|
|
514
656
|
resolve({ ...result, ok: result.ok && code === 0 });
|
|
515
657
|
});
|
|
@@ -531,6 +673,14 @@ var ClaudeRunner = class {
|
|
|
531
673
|
windowsHide: true
|
|
532
674
|
};
|
|
533
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
|
+
}
|
|
534
684
|
/**
|
|
535
685
|
* Best-effort auth check: is `claude` on PATH and can we verify login?
|
|
536
686
|
* Never throws. If we can't cheaply detect auth, we return installed:true
|
|
@@ -574,6 +724,310 @@ var ClaudeRunner = class {
|
|
|
574
724
|
};
|
|
575
725
|
var claudeRunner = new ClaudeRunner();
|
|
576
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
|
+
|
|
577
1031
|
// ../../scripts/virtual-office/code-runner/rate-limit-resume.mjs
|
|
578
1032
|
import { appendFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
579
1033
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -674,9 +1128,9 @@ function classifyFailureForResume({
|
|
|
674
1128
|
}
|
|
675
1129
|
|
|
676
1130
|
// ../../scripts/virtual-office/code-runner/publish.mjs
|
|
677
|
-
import { spawnSync as
|
|
678
|
-
function run(cmd, args, cwd, { timeout = 18e4, raw = false } = {}) {
|
|
679
|
-
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 } : {} });
|
|
680
1134
|
if (r.error) throw r.error;
|
|
681
1135
|
if (r.status !== 0) {
|
|
682
1136
|
throw new Error(`${cmd} ${args[0]} failed (exit ${r.status}): ${(r.stderr || "").slice(-300)}`);
|
|
@@ -735,6 +1189,44 @@ function listCommittedFiles(cwd, base = "origin/main") {
|
|
|
735
1189
|
function compactTitle(s, max = 100) {
|
|
736
1190
|
return String(s || "").replace(/\s+/g, " ").trim().slice(0, max) || "code-task";
|
|
737
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
|
+
}
|
|
738
1230
|
function openCodeTaskPr(worktreeDir, files, {
|
|
739
1231
|
title,
|
|
740
1232
|
body,
|
|
@@ -744,7 +1236,12 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
744
1236
|
maxFiles = 200,
|
|
745
1237
|
// Safety net: the agent already COMMITTED its work to a branch (despite the
|
|
746
1238
|
// preamble). Skip add+commit; just push the existing branch and open the PR.
|
|
747
|
-
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
|
|
748
1245
|
} = {}) {
|
|
749
1246
|
if (!Array.isArray(files) || files.length === 0) {
|
|
750
1247
|
throw new Error("openCodeTaskPr: no files to commit");
|
|
@@ -761,13 +1258,14 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
761
1258
|
branch = run("git", ["branch", "--show-current"], worktreeDir, { timeout: 3e4 });
|
|
762
1259
|
} catch {
|
|
763
1260
|
}
|
|
1261
|
+
let tokenUsed;
|
|
764
1262
|
if (alreadyCommitted) {
|
|
765
1263
|
if (!branch || branch === "main" || branch === "HEAD") {
|
|
766
1264
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
767
1265
|
branch = `${branchPrefix}-${stamp}`;
|
|
768
1266
|
run("git", ["checkout", "-b", branch], worktreeDir);
|
|
769
1267
|
}
|
|
770
|
-
|
|
1268
|
+
tokenUsed = pushBranch(worktreeDir, branch, githubToken);
|
|
771
1269
|
} else {
|
|
772
1270
|
if (!branch || branch === "main" || branch === "HEAD") {
|
|
773
1271
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -780,7 +1278,7 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
780
1278
|
}
|
|
781
1279
|
const commitMsg = compactTitle(title, 180);
|
|
782
1280
|
run("git", ["commit", "--no-verify", "-m", commitMsg], worktreeDir);
|
|
783
|
-
|
|
1281
|
+
tokenUsed = pushBranch(worktreeDir, branch, githubToken);
|
|
784
1282
|
}
|
|
785
1283
|
const out = run(
|
|
786
1284
|
"gh",
|
|
@@ -796,7 +1294,8 @@ function openCodeTaskPr(worktreeDir, files, {
|
|
|
796
1294
|
"--body",
|
|
797
1295
|
String(body || "")
|
|
798
1296
|
],
|
|
799
|
-
worktreeDir
|
|
1297
|
+
worktreeDir,
|
|
1298
|
+
{ env: tokenUsed ? installationTokenEnv(githubToken) : void 0 }
|
|
800
1299
|
);
|
|
801
1300
|
const m = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
|
|
802
1301
|
if (!m) throw new Error("gh pr create returned no parseable PR URL");
|
|
@@ -983,14 +1482,37 @@ async function forwardSessionSpool(deps) {
|
|
|
983
1482
|
return { forwarded, pruned };
|
|
984
1483
|
}
|
|
985
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
|
+
|
|
986
1508
|
// ../../scripts/virtual-office/code-runner/pr-watcher.mjs
|
|
987
1509
|
import { homedir as homedir4 } from "node:os";
|
|
988
1510
|
import { join as join4 } from "node:path";
|
|
989
1511
|
import { readFile as readFile2, writeFile as writeFile2, mkdir } from "node:fs/promises";
|
|
990
|
-
import { spawnSync as
|
|
1512
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
991
1513
|
var CI_FIX_MARKER = "[VO-CI-FIX]";
|
|
992
1514
|
function ghViewPr(prNumber, repo) {
|
|
993
|
-
const r =
|
|
1515
|
+
const r = spawnSync6(
|
|
994
1516
|
"gh",
|
|
995
1517
|
["pr", "view", String(prNumber), "-R", repo, "--json", "state,statusCheckRollup,headRefName"],
|
|
996
1518
|
{ encoding: "utf8", timeout: 3e4 }
|
|
@@ -1628,7 +2150,8 @@ var parseList = (s) => String(s || "").split(/[\s,]+/).map((x) => x.trim()).filt
|
|
|
1628
2150
|
function loadConfig(env2 = process.env) {
|
|
1629
2151
|
return {
|
|
1630
2152
|
runnerId: env2.VO_CODE_RUNNER_ID || `vo-code-runner-${os.hostname()}`,
|
|
1631
|
-
|
|
2153
|
+
// BYO multi-agent: {agent, runner, runnerBin} — VO_CODE_RUNNER_AGENT (claude|codex).
|
|
2154
|
+
...resolveRunner(env2, { warn: (m) => log(`agent-select: ${m}`) }),
|
|
1632
2155
|
permissionMode: env2.VO_CODE_RUNNER_PERMISSION_MODE || "acceptEdits",
|
|
1633
2156
|
maxConcurrency: Math.max(1, Number(env2.VO_CODE_TASK_MAX_CONCURRENCY || 2) || 2),
|
|
1634
2157
|
pollSec: Math.max(1, Number(env2.VO_CODE_RUNNER_POLL_SEC || 5) || 5),
|
|
@@ -1699,15 +2222,16 @@ async function processOneTask(client, task, cfg) {
|
|
|
1699
2222
|
const id = task.code_task_id;
|
|
1700
2223
|
let worktreeName = "";
|
|
1701
2224
|
try {
|
|
1702
|
-
const wt = createFixWorktree("code-task", { source: id.slice(0, 8) });
|
|
2225
|
+
const wt = createFixWorktree("code-task", { source: id.slice(0, 8), repo: task.repo });
|
|
1703
2226
|
worktreeName = wt.worktreeName;
|
|
1704
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 }) });
|
|
1705
|
-
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})` });
|
|
1706
2229
|
const cap = typeof task.max_budget_usd === "number" ? task.max_budget_usd : resolveSpendCapUsd();
|
|
1707
|
-
const run2 = await
|
|
2230
|
+
const run2 = await runAgentTask({
|
|
2231
|
+
runner: cfg.runner,
|
|
2232
|
+
bin: cfg.runnerBin,
|
|
1708
2233
|
prompt: effortPrompt,
|
|
1709
2234
|
cwd: wt.worktreeDir,
|
|
1710
|
-
claudeBin: cfg.claudeBin,
|
|
1711
2235
|
permissionMode: effectivePermissionMode,
|
|
1712
2236
|
maxTurns: effectiveMaxTurns,
|
|
1713
2237
|
model,
|
|
@@ -1779,10 +2303,12 @@ async function processOneTask(client, task, cfg) {
|
|
|
1779
2303
|
return;
|
|
1780
2304
|
}
|
|
1781
2305
|
await safeProgress(client, id, { message: `opening PR for ${files.length} changed file(s)` });
|
|
2306
|
+
const githubToken = (await client.getInstallationToken())?.token ?? null;
|
|
1782
2307
|
const pr = openCodeTaskPr(wt.worktreeDir, files, {
|
|
1783
2308
|
title: `code-task: ${task.prompt}`,
|
|
1784
2309
|
body: buildPrBody(task, run2, files),
|
|
1785
|
-
alreadyCommitted
|
|
2310
|
+
alreadyCommitted,
|
|
2311
|
+
githubToken
|
|
1786
2312
|
});
|
|
1787
2313
|
await safeProgress(client, id, {
|
|
1788
2314
|
status: "pr_opened",
|
|
@@ -1836,25 +2362,17 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
1836
2362
|
log
|
|
1837
2363
|
});
|
|
1838
2364
|
log(
|
|
1839
|
-
`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})`
|
|
1840
2366
|
);
|
|
1841
2367
|
for (const line of describeClaimScoping(cfg, env2)) log(line);
|
|
1842
2368
|
log(
|
|
1843
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)"
|
|
1844
2370
|
);
|
|
1845
|
-
let lastSessionForward = 0;
|
|
1846
2371
|
let lastWatchCycle = 0;
|
|
1847
2372
|
const runWatch = makeWatchRunner({ client, log, maxFixAttempts: cfg.watchMaxFix });
|
|
2373
|
+
const loopTick = makeLoopTicks({ client, cfg, env: env2, log, getActive: () => active });
|
|
1848
2374
|
while (!stopping) {
|
|
1849
|
-
|
|
1850
|
-
lastSessionForward = Date.now();
|
|
1851
|
-
forwardSessionSpool({
|
|
1852
|
-
baseUrl: String(env2.VO_CONTROL_PLANE_URL || "").replace(/\/$/, ""),
|
|
1853
|
-
token: env2.VO_CONTROL_PLANE_ADMIN_TOKEN || "",
|
|
1854
|
-
operatorSeed: cfg.operatorSeed
|
|
1855
|
-
}).catch(() => {
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
2375
|
+
loopTick();
|
|
1858
2376
|
if (cfg.watchEnabled && Date.now() - lastWatchCycle >= cfg.watchIntervalSec * 1e3) {
|
|
1859
2377
|
lastWatchCycle = Date.now();
|
|
1860
2378
|
runWatch().then((r) => {
|