@algosuite/vo-mcp 0.1.0 → 0.2.0-beta.1

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.
@@ -0,0 +1,1940 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __esm = (fn, res, err) => function __init() {
6
+ if (err) throw err[0];
7
+ try {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ } catch (e) {
10
+ throw err = [e], e;
11
+ }
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+
18
+ // src/runner/control-plane-auth-stub.mjs
19
+ var control_plane_auth_stub_exports = {};
20
+ __export(control_plane_auth_stub_exports, {
21
+ getFirebaseAuth: () => getFirebaseAuth
22
+ });
23
+ async function getFirebaseAuth() {
24
+ throw new Error(
25
+ "vo-mcp runner: no control-plane credential. Run `vo-mcp login` first \u2014 the runner authenticates with your stored vo_credential (or set VO_CONTROL_PLANE_ADMIN_TOKEN)."
26
+ );
27
+ }
28
+ var init_control_plane_auth_stub = __esm({
29
+ "src/runner/control-plane-auth-stub.mjs"() {
30
+ }
31
+ });
32
+
33
+ // src/cloud/credential-store.ts
34
+ import { homedir } from "node:os";
35
+ import { join, dirname } from "node:path";
36
+ import {
37
+ existsSync,
38
+ mkdirSync,
39
+ readFileSync,
40
+ writeFileSync,
41
+ chmodSync,
42
+ rmSync
43
+ } from "node:fs";
44
+
45
+ // src/cloud/keychain.ts
46
+ import { createRequire } from "node:module";
47
+ var SERVICE = "vo-mcp";
48
+ var ACCOUNT = "refresh-credential";
49
+ var cached;
50
+ function loadKeyring() {
51
+ if (cached !== void 0) return cached;
52
+ try {
53
+ const req = createRequire(import.meta.url);
54
+ const mod = req("@napi-rs/keyring");
55
+ cached = mod && typeof mod.Entry === "function" ? mod : null;
56
+ } catch {
57
+ cached = null;
58
+ }
59
+ return cached;
60
+ }
61
+ function keychainAvailable() {
62
+ return loadKeyring() !== null;
63
+ }
64
+ function keychainGet() {
65
+ const k = loadKeyring();
66
+ if (!k) return null;
67
+ try {
68
+ return new k.Entry(SERVICE, ACCOUNT).getPassword();
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function keychainSet(secret) {
74
+ const k = loadKeyring();
75
+ if (!k) return false;
76
+ try {
77
+ new k.Entry(SERVICE, ACCOUNT).setPassword(secret);
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ function keychainDelete() {
84
+ const k = loadKeyring();
85
+ if (!k) return false;
86
+ try {
87
+ return new k.Entry(SERVICE, ACCOUNT).deletePassword();
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ // src/cloud/credential-store.ts
94
+ var realKeychain = {
95
+ available: keychainAvailable,
96
+ get: keychainGet,
97
+ set: keychainSet,
98
+ delete: keychainDelete
99
+ };
100
+ function credentialPath(env2 = process.env) {
101
+ const override = env2["VO_MCP_CREDENTIALS_PATH"]?.trim();
102
+ if (override) return override;
103
+ return join(homedir(), ".config", "vo-mcp", "credentials.json");
104
+ }
105
+ function keychainEnabled(env2, keychain) {
106
+ const disabled = (env2["VO_MCP_DISABLE_KEYCHAIN"] ?? "").trim().toLowerCase();
107
+ if (disabled === "1" || disabled === "true" || disabled === "yes") return false;
108
+ return keychain.available();
109
+ }
110
+ function deserialize(raw) {
111
+ try {
112
+ const parsed = JSON.parse(raw);
113
+ const refresh = typeof parsed.refresh_token === "string" ? parsed.refresh_token.trim() : "";
114
+ const apiKey = typeof parsed.api_key === "string" ? parsed.api_key.trim() : "";
115
+ const voCred = typeof parsed.vo_credential === "string" ? parsed.vo_credential.trim() : "";
116
+ if (!voCred && (!refresh || !apiKey)) return null;
117
+ return {
118
+ ...refresh ? { refresh_token: refresh } : {},
119
+ ...apiKey ? { api_key: apiKey } : {},
120
+ ...voCred ? { vo_credential: voCred } : {},
121
+ ...typeof parsed.vo_credential_expires_at === "string" ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {},
122
+ ...typeof parsed.email === "string" ? { email: parsed.email } : {},
123
+ ...typeof parsed.stored_at === "string" ? { stored_at: parsed.stored_at } : {}
124
+ };
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ function readFromFile(env2) {
130
+ try {
131
+ const p = credentialPath(env2);
132
+ if (!existsSync(p)) return null;
133
+ return deserialize(readFileSync(p, "utf8"));
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ function readStoredCredential(env2 = process.env, keychain = realKeychain) {
139
+ if (keychainEnabled(env2, keychain)) {
140
+ const raw = keychain.get();
141
+ const fromKeychain = raw ? deserialize(raw) : null;
142
+ if (fromKeychain) return fromKeychain;
143
+ }
144
+ return readFromFile(env2);
145
+ }
146
+
147
+ // ../../scripts/virtual-office/code-runner-daemon.mjs
148
+ import os from "node:os";
149
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
150
+
151
+ // src/runner/worktree-helper.mjs
152
+ import { spawnSync } from "node:child_process";
153
+ import path from "node:path";
154
+ import fs from "node:fs";
155
+ function repoRoot() {
156
+ return process.env.VO_CODE_RUNNER_REPO || process.cwd();
157
+ }
158
+ function sanitize(value, fallback) {
159
+ const cleaned = String(value || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
160
+ return cleaned || fallback;
161
+ }
162
+ function createFixWorktree(kind, error = {}) {
163
+ const root = repoRoot();
164
+ const safeKind = sanitize(kind, "task");
165
+ const safeTarget = sanitize(error.source || error.tester || "run", "run").slice(0, 24);
166
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
167
+ const worktreeName = `${safeKind}-${safeTarget}-${stamp}`;
168
+ const branchName = `vo/${worktreeName}`;
169
+ const worktreeDir = path.join(root, ".agent-worktrees", worktreeName);
170
+ spawnSync("git", ["fetch", "origin", "main"], { cwd: root, timeout: 12e4 });
171
+ const add = spawnSync(
172
+ "git",
173
+ ["worktree", "add", "-B", branchName, worktreeDir, "origin/main"],
174
+ { cwd: root, encoding: "utf8", timeout: 12e4 }
175
+ );
176
+ if (add.status === 0 && fs.existsSync(worktreeDir)) {
177
+ return { worktreeDir, worktreeName };
178
+ }
179
+ console.error(
180
+ `[vo-mcp runner] worktree isolation unavailable: ${String(add.stderr || add.error || "").slice(0, 200)}`
181
+ );
182
+ return { worktreeDir: root, worktreeName: "" };
183
+ }
184
+ function cleanupFixWorktree(worktreeName) {
185
+ if (!worktreeName) return;
186
+ const root = repoRoot();
187
+ const worktreeDir = path.join(root, ".agent-worktrees", worktreeName);
188
+ spawnSync("git", ["worktree", "remove", "--force", worktreeDir], { cwd: root, timeout: 12e4 });
189
+ }
190
+
191
+ // src/runner/spend-cap-shim.mjs
192
+ function resolveSpendCapUsd(value = process.env.VO_SPEND_CAP_USD ?? process.env.VO_CODE_DISPATCH_CAP_USD) {
193
+ const parsed = Number.parseFloat(String(value ?? ""));
194
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
195
+ return parsed;
196
+ }
197
+
198
+ // ../../scripts/virtual-office/code-runner/control-plane-client.mjs
199
+ var cachedFirebaseToken = null;
200
+ async function resolveBearer(env2) {
201
+ const adminToken = env2.VO_CONTROL_PLANE_ADMIN_TOKEN;
202
+ if (adminToken) return adminToken;
203
+ if (cachedFirebaseToken) return cachedFirebaseToken;
204
+ const { getFirebaseAuth: getFirebaseAuth2 } = await Promise.resolve().then(() => (init_control_plane_auth_stub(), control_plane_auth_stub_exports));
205
+ const auth = await getFirebaseAuth2({ env: env2 });
206
+ if (!auth || !auth.idToken) {
207
+ throw new Error(
208
+ "no control-plane credential: set VO_CONTROL_PLANE_ADMIN_TOKEN, or SMOKE_EMAIL/SMOKE_PASSWORD/SMOKE_API_KEY"
209
+ );
210
+ }
211
+ cachedFirebaseToken = auth.idToken;
212
+ return cachedFirebaseToken;
213
+ }
214
+ function createControlPlaneClient({
215
+ baseUrl = process.env.VO_CONTROL_PLANE_URL || "",
216
+ env: env2 = process.env,
217
+ fetchImpl = fetch
218
+ } = {}) {
219
+ if (!baseUrl) {
220
+ throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
221
+ }
222
+ const root = baseUrl.replace(/\/+$/, "");
223
+ async function req(method, path3, body) {
224
+ const bearer = await resolveBearer(env2);
225
+ return fetchImpl(`${root}${path3}`, {
226
+ method,
227
+ headers: {
228
+ "content-type": "application/json",
229
+ authorization: `Bearer ${bearer}`
230
+ },
231
+ body: body === void 0 ? void 0 : JSON.stringify(body)
232
+ });
233
+ }
234
+ return {
235
+ /**
236
+ * Claim the next pending task. Returns the task or null (empty queue).
237
+ * `repos` (optional `owner/name` list) and `operatorIds` (optional
238
+ * `operator_id` list) scope the claim so this daemon only picks up tasks it
239
+ * serves — the control-plane filters by both (logical AND), so another
240
+ * operator's task never lands on (or bills) this machine.
241
+ */
242
+ async claim(runnerId, repos, operatorIds) {
243
+ const body = { runner_id: runnerId };
244
+ if (Array.isArray(repos) && repos.length > 0) body.repos = repos;
245
+ if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
246
+ const res = await req("POST", "/api/v1/code-task/claim", body);
247
+ if (res.status === 401) {
248
+ cachedFirebaseToken = null;
249
+ throw new Error("claim unauthorized (401)");
250
+ }
251
+ if (!res.ok) throw new Error(`claim failed: HTTP ${res.status}`);
252
+ const json = await res.json();
253
+ return json && json.task ? json.task : null;
254
+ },
255
+ /**
256
+ * Enqueue a new code-task (used by the PR watcher to auto-dispatch a CI fix).
257
+ * Server derives operator/tenant from the daemon's authenticated principal.
258
+ * Returns the created task, or throws on a non-2xx response.
259
+ */
260
+ async enqueueCodeTask({ repo, prompt, max_budget_usd, max_turns }) {
261
+ const body = { repo, prompt };
262
+ if (typeof max_budget_usd === "number") body.max_budget_usd = max_budget_usd;
263
+ if (typeof max_turns === "number") body.max_turns = max_turns;
264
+ const res = await req("POST", "/api/v1/code-task", body);
265
+ if (res.status === 401) {
266
+ cachedFirebaseToken = null;
267
+ throw new Error("enqueue unauthorized (401)");
268
+ }
269
+ if (!res.ok) throw new Error(`enqueue failed: HTTP ${res.status}`);
270
+ const json = await res.json();
271
+ return json && json.task ? json.task : null;
272
+ },
273
+ /**
274
+ * Append progress / set terminal status. Returns
275
+ * { task } — applied
276
+ * { terminal: true } — task already terminal (operator cancelled): STOP
277
+ */
278
+ async postProgress(taskId, patch) {
279
+ const res = await req("PATCH", `/api/v1/code-task/${taskId}/progress`, patch);
280
+ if (res.status === 409) return { terminal: true };
281
+ if (res.status === 404) return { terminal: true, missing: true };
282
+ if (!res.ok) throw new Error(`progress failed: HTTP ${res.status}`);
283
+ const json = await res.json();
284
+ return { task: json && json.task };
285
+ },
286
+ /** Read the current task (cancel detection). Null on 404. */
287
+ async getTask(taskId) {
288
+ const res = await req("GET", `/api/v1/code-task/${taskId}`);
289
+ if (res.status === 404) return null;
290
+ if (!res.ok) throw new Error(`getTask failed: HTTP ${res.status}`);
291
+ const json = await res.json();
292
+ return json ? json.task : null;
293
+ },
294
+ /**
295
+ * Report this machine's rolling-7-day Claude Code token usage (the real
296
+ * weekly-capacity gauge) PLUS the operator's real Claude weekly % (when
297
+ * available). The daemon authenticates as admin, so the target `operatorId`
298
+ * is named explicitly. Best-effort; throws on a non-2xx so the caller can
299
+ * log + move on.
300
+ *
301
+ * `tokens` = { input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens }.
302
+ * Optional: `claudeWeeklyPct` (number) + `claudeWeeklyResetsAt` (ISO string | null).
303
+ */
304
+ async postWeeklyTokens({ operatorId, runnerId, tokens, claudeWeeklyPct, claudeWeeklyResetsAt }) {
305
+ const body = {
306
+ operator_id: operatorId,
307
+ runner_id: runnerId,
308
+ input_tokens: tokens.input_tokens,
309
+ output_tokens: tokens.output_tokens,
310
+ cache_creation_tokens: tokens.cache_creation_tokens,
311
+ cache_read_tokens: tokens.cache_read_tokens
312
+ };
313
+ if (typeof claudeWeeklyPct === "number") {
314
+ body.claude_weekly_pct = claudeWeeklyPct;
315
+ }
316
+ if (claudeWeeklyResetsAt !== void 0) {
317
+ body.claude_weekly_resets_at = claudeWeeklyResetsAt;
318
+ }
319
+ const res = await req("POST", "/api/v1/weekly-tokens", body);
320
+ if (res.status === 401) {
321
+ cachedFirebaseToken = null;
322
+ throw new Error("weekly-tokens unauthorized (401)");
323
+ }
324
+ if (!res.ok) throw new Error(`weekly-tokens failed: HTTP ${res.status}`);
325
+ return true;
326
+ },
327
+ /**
328
+ * Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
329
+ * Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'ultracode'),
330
+ * defaulting to 'standard' on any error. Never throws — best-effort.
331
+ */
332
+ async getDispatchMode() {
333
+ try {
334
+ const res = await req("GET", "/api/v1/dispatch-mode-config");
335
+ if (!res.ok) return "standard";
336
+ const json = await res.json();
337
+ return json?.dispatchMode || "standard";
338
+ } catch {
339
+ return "standard";
340
+ }
341
+ }
342
+ };
343
+ }
344
+
345
+ // ../../scripts/virtual-office/code-runner/claude-runner.mjs
346
+ import { spawn } from "node:child_process";
347
+ import { spawnSync as spawnSync2 } from "node:child_process";
348
+ var DEFAULT_PERMISSION_MODE = "acceptEdits";
349
+ function extractText(content) {
350
+ if (typeof content === "string") return content.trim();
351
+ if (Array.isArray(content)) {
352
+ return content.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim();
353
+ }
354
+ return "";
355
+ }
356
+ function parseStreamEvent(line) {
357
+ const trimmed = String(line || "").trim();
358
+ if (!trimmed) return null;
359
+ let evt;
360
+ try {
361
+ evt = JSON.parse(trimmed);
362
+ } catch {
363
+ return null;
364
+ }
365
+ if (!evt || typeof evt !== "object") return null;
366
+ if (evt.type === "assistant" && evt.message && evt.message.content) {
367
+ const text = extractText(evt.message.content);
368
+ return text ? { kind: "progress", text } : null;
369
+ }
370
+ if (evt.type === "result") {
371
+ const isError = Boolean(evt.is_error) || evt.subtype === "error_max_turns" || evt.subtype === "error_during_execution";
372
+ return {
373
+ kind: "result",
374
+ isError,
375
+ costUsd: typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : null,
376
+ summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
377
+ numTurns: typeof evt.num_turns === "number" ? evt.num_turns : null
378
+ };
379
+ }
380
+ return null;
381
+ }
382
+ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model } = {}) {
383
+ const args = [
384
+ "-p",
385
+ "--output-format",
386
+ "stream-json",
387
+ "--verbose",
388
+ "--permission-mode",
389
+ String(permissionMode || DEFAULT_PERMISSION_MODE)
390
+ ];
391
+ if (Number.isInteger(maxTurns) && maxTurns > 0) {
392
+ args.push("--max-turns", String(maxTurns));
393
+ }
394
+ if (model) {
395
+ args.push("--model", String(model));
396
+ }
397
+ return args;
398
+ }
399
+ function runClaudeTask({
400
+ prompt,
401
+ cwd,
402
+ claudeBin = "claude",
403
+ permissionMode,
404
+ maxTurns,
405
+ model,
406
+ env: env2 = process.env,
407
+ onProgress = () => {
408
+ },
409
+ shouldCancel = async () => false,
410
+ cancelPollMs = 5e3,
411
+ maxWallClockMs = 0,
412
+ spawnImpl = spawn
413
+ }) {
414
+ return new Promise((resolve) => {
415
+ const args = buildClaudeArgs({ permissionMode, maxTurns, model });
416
+ const child = spawnImpl(claudeBin, args, {
417
+ cwd,
418
+ env: { ...env2 },
419
+ stdio: ["pipe", "pipe", "pipe"],
420
+ // Windows: `claude` is a `.cmd` shim → spawn needs a shell to resolve it
421
+ // (without it: spawn ENOENT). Safe because the prompt goes via stdin
422
+ // below, never argv, so the shell never sees untrusted input.
423
+ shell: process.platform === "win32",
424
+ windowsHide: true
425
+ });
426
+ try {
427
+ child.stdin.write(String(prompt));
428
+ child.stdin.end();
429
+ } catch {
430
+ }
431
+ let buffer = "";
432
+ let result = { ok: false, costUsd: null, summary: "", numTurns: null, killed: false };
433
+ let killed = false;
434
+ let timedOut = false;
435
+ let stderrTail = "";
436
+ const hardKill = () => {
437
+ try {
438
+ child.kill("SIGTERM");
439
+ } catch {
440
+ }
441
+ setTimeout(() => {
442
+ try {
443
+ child.kill("SIGKILL");
444
+ } catch {
445
+ }
446
+ }, 5e3);
447
+ };
448
+ const wallTimer = maxWallClockMs > 0 ? setTimeout(() => {
449
+ timedOut = true;
450
+ clearInterval(poll);
451
+ hardKill();
452
+ }, maxWallClockMs) : null;
453
+ child.stdout.on("data", (chunk) => {
454
+ buffer += chunk.toString();
455
+ let nl;
456
+ while ((nl = buffer.indexOf("\n")) >= 0) {
457
+ const line = buffer.slice(0, nl);
458
+ buffer = buffer.slice(nl + 1);
459
+ const evt = parseStreamEvent(line);
460
+ if (!evt) continue;
461
+ if (evt.kind === "progress") {
462
+ try {
463
+ onProgress(evt.text.slice(0, 1500));
464
+ } catch {
465
+ }
466
+ } else if (evt.kind === "result") {
467
+ result = {
468
+ ...result,
469
+ ok: !evt.isError,
470
+ costUsd: evt.costUsd,
471
+ summary: evt.summary,
472
+ numTurns: evt.numTurns
473
+ };
474
+ }
475
+ }
476
+ });
477
+ child.stderr.on("data", (c) => {
478
+ stderrTail = (stderrTail + c.toString()).slice(-4e3);
479
+ });
480
+ child.on("error", (err) => {
481
+ clearInterval(poll);
482
+ if (wallTimer) clearTimeout(wallTimer);
483
+ resolve({ ...result, ok: false, summary: `spawn error: ${err.message}` });
484
+ });
485
+ const poll = setInterval(() => {
486
+ Promise.resolve().then(() => shouldCancel()).then((cancel) => {
487
+ if (cancel && !killed) {
488
+ killed = true;
489
+ clearInterval(poll);
490
+ hardKill();
491
+ }
492
+ }).catch(() => {
493
+ });
494
+ }, cancelPollMs);
495
+ child.on("close", (code) => {
496
+ clearInterval(poll);
497
+ if (wallTimer) clearTimeout(wallTimer);
498
+ if (timedOut) {
499
+ resolve({
500
+ ...result,
501
+ ok: false,
502
+ timedOut: true,
503
+ summary: `wall-clock timeout (${maxWallClockMs}ms)`
504
+ });
505
+ return;
506
+ }
507
+ if (killed) {
508
+ resolve({ ...result, ok: false, killed: true, summary: "cancelled by operator" });
509
+ return;
510
+ }
511
+ if (!result.summary && code !== 0) {
512
+ result.summary = stderrTail.slice(-500) || `claude exited ${code}`;
513
+ }
514
+ resolve({ ...result, ok: result.ok && code === 0 });
515
+ });
516
+ });
517
+ }
518
+ var ClaudeRunner = class {
519
+ get binary() {
520
+ return "claude";
521
+ }
522
+ buildArgs({ permissionMode, maxTurns, model } = {}) {
523
+ return buildClaudeArgs({ permissionMode, maxTurns, model });
524
+ }
525
+ parseEvent(line) {
526
+ return parseStreamEvent(line);
527
+ }
528
+ getSpawnOptions() {
529
+ return {
530
+ shell: process.platform === "win32",
531
+ windowsHide: true
532
+ };
533
+ }
534
+ /**
535
+ * Best-effort auth check: is `claude` on PATH and can we verify login?
536
+ * Never throws. If we can't cheaply detect auth, we return installed:true
537
+ * and let the real spawn fail with a clearer error from the CLI itself.
538
+ */
539
+ async checkAuth() {
540
+ try {
541
+ const { status, error } = spawnSync2("claude", ["--version"], {
542
+ shell: process.platform === "win32",
543
+ windowsHide: true,
544
+ timeout: 3e3,
545
+ stdio: "ignore"
546
+ });
547
+ if (error) {
548
+ return {
549
+ installed: false,
550
+ authenticated: false,
551
+ message: `claude not found on PATH: ${error.message}`
552
+ };
553
+ }
554
+ if (status !== 0) {
555
+ return {
556
+ installed: true,
557
+ authenticated: false,
558
+ message: "claude binary exists but --version failed (auth unclear)"
559
+ };
560
+ }
561
+ return {
562
+ installed: true,
563
+ authenticated: true,
564
+ message: "claude binary found (auth check is best-effort)"
565
+ };
566
+ } catch (err) {
567
+ return {
568
+ installed: false,
569
+ authenticated: false,
570
+ message: `checkAuth probe failed: ${err.message}`
571
+ };
572
+ }
573
+ }
574
+ };
575
+ var claudeRunner = new ClaudeRunner();
576
+
577
+ // ../../scripts/virtual-office/code-runner/rate-limit-resume.mjs
578
+ import { appendFileSync, mkdirSync as mkdirSync2 } from "node:fs";
579
+ import { homedir as homedir2 } from "node:os";
580
+ import { join as join2, dirname as dirname2 } from "node:path";
581
+
582
+ // ../../scripts/ci/rate-limit-detector-core.mjs
583
+ var RATE_LIMIT_RE = /\b(?:usage limit reached|usage limit|rate[ _-]?limit(?:ed|_error)?|too many requests|\b429\b|limit (?:will )?reset)/i;
584
+ function extractResumeAfter(text, { now = null } = {}) {
585
+ const s = String(text || "");
586
+ const iso = s.match(/\b(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\b/);
587
+ if (iso) {
588
+ const t = Date.parse(iso[1].replace(" ", "T"));
589
+ if (Number.isFinite(t)) return new Date(t).toISOString();
590
+ }
591
+ const epoch = s.match(/(?:reset|resets|retry[- ]?after|available)[^0-9]{0,20}(\d{10,13})/i);
592
+ if (epoch) {
593
+ let n = Number(epoch[1]);
594
+ if (n < 1e12) n *= 1e3;
595
+ if (Number.isFinite(n)) return new Date(n).toISOString();
596
+ }
597
+ const after = s.match(/retry[- ]?after[^0-9]{0,8}(\d{1,6})\s*(?:s|sec|seconds)?\b/i);
598
+ if (after && now != null) {
599
+ const t = new Date(now).getTime() + Number(after[1]) * 1e3;
600
+ if (Number.isFinite(t)) return new Date(t).toISOString();
601
+ }
602
+ return null;
603
+ }
604
+ function detectRateLimit(text, { now = null } = {}) {
605
+ const s = String(text || "");
606
+ const rateLimited = RATE_LIMIT_RE.test(s);
607
+ return {
608
+ rateLimited,
609
+ resumeAfter: rateLimited ? extractResumeAfter(s, { now }) : null
610
+ };
611
+ }
612
+
613
+ // ../../scripts/virtual-office/code-runner/rate-limit-resume.mjs
614
+ function resumeQueuePath() {
615
+ return join2(homedir2(), ".claude", "resume-queue.jsonl");
616
+ }
617
+ function buildResumeEntry({ task = {}, resumeAfter = null, summary = "", at } = {}) {
618
+ return {
619
+ kind: "rate_limited_code_task",
620
+ at,
621
+ code_task_id: task.code_task_id || null,
622
+ repo: task.repo || null,
623
+ operator_id: task.operator_id || null,
624
+ prompt: task.prompt || "",
625
+ resume_after: resumeAfter,
626
+ // ISO string, or null (scheduler backs off when null)
627
+ attempts: Number(task._resume_attempts || 0) + 1,
628
+ summary: String(summary).slice(0, 500)
629
+ };
630
+ }
631
+ function recordRateLimited({ task = {}, resumeAfter = null, summary = "", queuePath = resumeQueuePath(), at = (/* @__PURE__ */ new Date()).toISOString() } = {}) {
632
+ const entry = buildResumeEntry({ task, resumeAfter, summary, at });
633
+ try {
634
+ mkdirSync2(dirname2(queuePath), { recursive: true });
635
+ appendFileSync(queuePath, `${JSON.stringify(entry)}
636
+ `, "utf-8");
637
+ return { ok: true, entry };
638
+ } catch (e) {
639
+ return { ok: false, error: e && e.message ? e.message : "write failed", entry };
640
+ }
641
+ }
642
+ function classifyFailureForResume({
643
+ enabled = false,
644
+ run: run2 = {},
645
+ task = {},
646
+ now = (/* @__PURE__ */ new Date()).toISOString(),
647
+ detect = detectRateLimit,
648
+ record = recordRateLimited
649
+ } = {}) {
650
+ if (enabled) {
651
+ const rl = detect(run2.summary, { now });
652
+ if (rl.rateLimited) {
653
+ const rec = record({ task, resumeAfter: rl.resumeAfter, summary: run2.summary });
654
+ return {
655
+ rateLimited: true,
656
+ resumeAfter: rl.resumeAfter,
657
+ recorded: !!(rec && rec.ok),
658
+ progress: {
659
+ status: "failed",
660
+ message: `rate-limited (resumable): ${run2.summary}`.slice(0, 1500),
661
+ result: "rate_limited"
662
+ }
663
+ };
664
+ }
665
+ }
666
+ return {
667
+ rateLimited: false,
668
+ progress: {
669
+ status: "failed",
670
+ message: `agent failed: ${run2.summary}`.slice(0, 1500),
671
+ result: String(run2.summary).slice(0, 2e3)
672
+ }
673
+ };
674
+ }
675
+
676
+ // ../../scripts/virtual-office/code-runner/publish.mjs
677
+ import { spawnSync as spawnSync3 } from "node:child_process";
678
+ function run(cmd, args, cwd, { timeout = 18e4, raw = false } = {}) {
679
+ const r = spawnSync3(cmd, args, { cwd, encoding: "utf8", timeout });
680
+ if (r.error) throw r.error;
681
+ if (r.status !== 0) {
682
+ throw new Error(`${cmd} ${args[0]} failed (exit ${r.status}): ${(r.stderr || "").slice(-300)}`);
683
+ }
684
+ const out = r.stdout || "";
685
+ return raw ? out : out.trim();
686
+ }
687
+ function parsePorcelainZ(out) {
688
+ const tokens = String(out).split("\0");
689
+ const files = [];
690
+ for (let i = 0; i < tokens.length; i += 1) {
691
+ const tok = tokens[i];
692
+ if (!tok) continue;
693
+ const path3 = tok.slice(3);
694
+ if (path3) files.push(path3);
695
+ if (tok[0] === "R" || tok[0] === "C") i += 1;
696
+ }
697
+ return files;
698
+ }
699
+ var SCRATCH_PATTERNS = [
700
+ /(^|\/)\.tmp-/i,
701
+ // .tmp-pr-body.md and other dot-temp scratch
702
+ /(^|\/)tmp\/pr[-_]?(body|description)/i,
703
+ // tmp/pr-body.md, tmp/pr_description...
704
+ /(^|\/)pr[-_]?(body|description)(\.(md|txt))?$/i
705
+ // pr-body.md, PR_DESCRIPTION.txt
706
+ ];
707
+ function isAgentScratch(path3) {
708
+ const p = String(path3 || "");
709
+ return SCRATCH_PATTERNS.some((re) => re.test(p));
710
+ }
711
+ function listChangedFiles(cwd) {
712
+ const out = run("git", ["-c", "core.quotepath=false", "status", "--porcelain", "-z"], cwd, {
713
+ timeout: 6e4,
714
+ raw: true
715
+ });
716
+ return parsePorcelainZ(out);
717
+ }
718
+ function listCommittedFiles(cwd, base = "origin/main") {
719
+ try {
720
+ run("git", ["fetch", "origin", "main"], cwd, { timeout: 6e4 });
721
+ } catch {
722
+ }
723
+ try {
724
+ const out = run(
725
+ "git",
726
+ ["-c", "core.quotepath=false", "diff", "--name-only", "-z", `${base}...HEAD`],
727
+ cwd,
728
+ { timeout: 6e4, raw: true }
729
+ );
730
+ return String(out).split("\0").map((s) => s.trim()).filter(Boolean);
731
+ } catch {
732
+ return [];
733
+ }
734
+ }
735
+ function compactTitle(s, max = 100) {
736
+ return String(s || "").replace(/\s+/g, " ").trim().slice(0, max) || "code-task";
737
+ }
738
+ function openCodeTaskPr(worktreeDir, files, {
739
+ title,
740
+ body,
741
+ branchPrefix = "vo/code-task",
742
+ botName = "vo-code-runner",
743
+ botEmail = "vo-code-runner@algosuite.ai",
744
+ maxFiles = 200,
745
+ // Safety net: the agent already COMMITTED its work to a branch (despite the
746
+ // preamble). Skip add+commit; just push the existing branch and open the PR.
747
+ alreadyCommitted = false
748
+ } = {}) {
749
+ if (!Array.isArray(files) || files.length === 0) {
750
+ throw new Error("openCodeTaskPr: no files to commit");
751
+ }
752
+ const cleaned = files.filter((f) => !isAgentScratch(f));
753
+ if (!alreadyCommitted && cleaned.length === 0) {
754
+ throw new Error("openCodeTaskPr: only scratch files, nothing to commit");
755
+ }
756
+ const toAdd = cleaned.slice(0, maxFiles);
757
+ run("git", ["config", "user.name", botName], worktreeDir);
758
+ run("git", ["config", "user.email", botEmail], worktreeDir);
759
+ let branch = "";
760
+ try {
761
+ branch = run("git", ["branch", "--show-current"], worktreeDir, { timeout: 3e4 });
762
+ } catch {
763
+ }
764
+ if (alreadyCommitted) {
765
+ if (!branch || branch === "main" || branch === "HEAD") {
766
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
767
+ branch = `${branchPrefix}-${stamp}`;
768
+ run("git", ["checkout", "-b", branch], worktreeDir);
769
+ }
770
+ run("git", ["push", "origin", branch], worktreeDir);
771
+ } else {
772
+ if (!branch || branch === "main" || branch === "HEAD") {
773
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
774
+ branch = `${branchPrefix}-${stamp}`;
775
+ run("git", ["fetch", "origin", "main"], worktreeDir, { timeout: 6e4 });
776
+ run("git", ["checkout", "-b", branch, "origin/main"], worktreeDir);
777
+ }
778
+ for (const f of toAdd) {
779
+ run("git", ["add", "--", f], worktreeDir, { timeout: 6e4 });
780
+ }
781
+ const commitMsg = compactTitle(title, 180);
782
+ run("git", ["commit", "--no-verify", "-m", commitMsg], worktreeDir);
783
+ run("git", ["push", "origin", branch], worktreeDir);
784
+ }
785
+ const out = run(
786
+ "gh",
787
+ [
788
+ "pr",
789
+ "create",
790
+ "--base",
791
+ "main",
792
+ "--head",
793
+ branch,
794
+ "--title",
795
+ compactTitle(title),
796
+ "--body",
797
+ String(body || "")
798
+ ],
799
+ worktreeDir
800
+ );
801
+ const m = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
802
+ if (!m) throw new Error("gh pr create returned no parseable PR URL");
803
+ return { prUrl: m[0], prNumber: Number(m[1]), branch, truncated: cleaned.length > maxFiles };
804
+ }
805
+
806
+ // ../../scripts/virtual-office/code-runner/dispatch-onboarding.mjs
807
+ var MANDATORY_READS = [
808
+ "CLAUDE.md (repo root \u2014 Claude-specific rules; auto-loaded, but READ it)",
809
+ 'AGENTS.md (repo root \u2014 cross-vendor rules + "Onboarding for a lane"; NOT auto-loaded)',
810
+ "README.md (repo root \u2014 product context)",
811
+ "docs/current/virtual-office-agent-charter.md",
812
+ "docs/current/virtual-office-operating-model.md",
813
+ "docs/current/virtual-office-test-architect.md",
814
+ "docs/current/evidence-grounded-consensus-testing.md",
815
+ "docs/vo/ADR-001-verification-oracle-not-orchestrator-2026-05-29.md (VO verifies + signs; human approves merges; NO autonomous bot-merge / headless triggers)",
816
+ "docs/vo/vo-adr-002-two-plane-moat.md (fat secret server / thin dumb client)",
817
+ "docs/vo/vo-roadmap-2026-05-26.md (the live roadmap \u2014 read its Change log tail for current state)",
818
+ "the nearest scoped CLAUDE.md for any directory you edit",
819
+ "for AlgoTax work: docs/current/algotax-progressive-return-roadmap.md + docs/current/algotax-coverage-roadmap.md",
820
+ "docs/current/pr-live-stewardship-doctrine.md (own EVERY PR to LIVE-VERIFIED; never let the operator discover a red PR or a backed-up deploy)"
821
+ ];
822
+ var NON_NEGOTIABLES = [
823
+ "MULTI-MODEL CONSENSUS VERIFICATION IS THE CORE of every Algosuite product \u2014 never ship single-model judgment as the product; route verifiable decisions through the consensus/verify path.",
824
+ 'TEST HONESTY (enforced): a test passes ONLY when it proves the product returned the VERIFIED CORRECT answer. No broad catch-alls; INVALID_ARGUMENT / null / PERMISSION_DENIED / empty / "no data" / SKIP are NOT passes. Fake green is a blocking bug.',
825
+ "VERIFY BEFORE ACT, human approves the merge (ADR-001). Never arm an autonomous bot-merge loop; never add a headless/automatic agent trigger.",
826
+ "NEVER trigger a full / all-codebase functions deploy, and NEVER edit functions-shared/src without an explicit plan \u2014 a full functions deploy is ~24h and catastrophic (RED LINE).",
827
+ "Gen2 Cloud Functions ONLY (firebase-functions/v2/*). Gen1 is CI-blocked.",
828
+ "Work on your OWN branch in a worktree; never `git add -A` / `git add .` (add files by name); respect file-size caps (components \u2264300, functions/services/utils \u2264400).",
829
+ "A handoff or roadmap line is a CLAIM, not evidence \u2014 verify shipped state against `git show origin/main:<path>`, never the stale local main tree.",
830
+ `MANDATORY FOR EVERY VO PR (cloud-run/vo-*, packages/vo-mcp, packages/consensus-engine, packages/vo-ratchets, packages/vo-arch-defaults, scripts/virtual-office, vo-claude-plugin): record a dated Change-log entry IN THE SAME PR via EITHER appending to the "\xA7 10 Change log" of docs/vo/vo-roadmap-2026-05-26.md OR (PREFERRED) creating docs/vo/roadmap-log/<YYYY-MM-DD>-<short-slug>.md (fragments avoid conflicts when PRs ship concurrently) and flip any status the work shipped. CI enforces this (check-vo-roadmap-discipline.mjs); bypass ONLY via "VO-ROADMAP-ALLOW: <reason>" in the PR body. The roadmap is the single source of truth \u2014 if you didn't update it, you didn't ship. Finish line = MERGED + DEPLOYED + LIVE-VERIFIED.`,
831
+ "Every UI change ships against docs/current/ui-trust-standard.md and adds VO QA tester coverage; verify in a real browser, not selector-presence.",
832
+ 'PR \u2192 LIVE is YOUR job end-to-end \u2014 the operator must NEVER be the one to discover a red PR or a backed-up deploy. Own every PR from branch \u2192 CI \u2192 merge \u2192 functions deploy \u2192 LIVE-VERIFIED. "Done" = the functions you changed are actually SERVING in prod in every region; prove it with `node scripts/ci/prove-pr-live.mjs --pr <N>` \u2014 a merge / green deploy checkmark / homepage 200 is NOT proof. If a function staled, re-deploy ONLY the affected functions (targeted), never a full deploy. If you hit a usage/rate limit, STOP cleanly with the PR obligation OPEN \u2014 the watchdog auto-resumes when it resets; do not abandon it. See docs/current/pr-live-stewardship-doctrine.md.',
833
+ `CONTEXT DEPTH IS NOT A REASON TO STOP. "I'm deep in context / fresh context would be better / I'll checkpoint" is the SAME premature-stop failure as doing 20 minutes of work instead of 6 hours \u2014 there is no quality cliff before compaction and the harness carries work forward. Keep BUILDING until the task is genuinely DONE; delicate or fleet-governing work means be CAREFUL, not stop. The ONLY valid pauses are real blockers: an operator decision is required, a dependency is not merged, or a hard external wait.`
834
+ ];
835
+ function buildDispatchOnboarding({ repo = "Algosuite-ai/Nexus" } = {}) {
836
+ const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
837
+ const rules = NON_NEGOTIABLES.map((r) => ` - ${r}`).join("\n");
838
+ return [
839
+ `You are a Virtual Office (VO) dispatched coding agent working in a fresh worktree of ${repo}.`,
840
+ "You were dispatched by the operator (greylor, a non-coder founder) to do the TASK at the end of this message.",
841
+ "Before writing ANY code, you MUST read the onboarding docs below \u2014 they are mandatory, not optional. Your worktree auto-loads CLAUDE.md, but the rest are NOT auto-loaded; open and read them.",
842
+ "",
843
+ "MANDATORY READS (read these FIRST, in order):",
844
+ reads,
845
+ "",
846
+ "NON-NEGOTIABLE RULES (these bind you even before you finish reading):",
847
+ rules,
848
+ "",
849
+ "HOW THE RUNNER PUBLISHES YOUR WORK (critical \u2014 read carefully):",
850
+ " - Leave your changes as UNCOMMITTED edits in this worktree. The VO runner commits them, pushes a branch, and opens the PR FOR you \u2014 that is its job, not yours.",
851
+ " - Do NOT run git (no commit, no branch, no checkout) and do NOT run `gh` / open a PR yourself. You are sandboxed to file edits; git/gh commands will be denied, and committing your work moves it where the runner cannot see it (your change would be silently discarded).",
852
+ " - When the task is done, simply STOP. Your final message should summarize what you changed; the runner detects your edited files and creates the PR.",
853
+ " - If a git or `gh` command is DENIED, that is EXPECTED and CORRECT \u2014 it means the runner will handle publishing. Do NOT retry it, do NOT try a different git/gh invocation, and do NOT wait for an approval that will not come. STOP immediately with your edits uncommitted. (Agents that retried a denied `gh pr create` burned ~25 minutes of usage and their finished fix was lost.)",
854
+ " - Do NOT create scratch files \u2014 no drafted PR body, no notes/TODO/plan files, nothing under tmp/ or named pr-body*/pr-description*. The runner writes the PR body itself; the worktree should contain ONLY the real file changes the task requires. (Stray scratch files have leaked into PRs.)",
855
+ "",
856
+ "Definition of done: the change is correct, tested to the standard above, type-checks + lints clean, and (for VO surfaces) updates the roadmap. Leave it as UNCOMMITTED edits and STOP \u2014 the runner opens the PR. If the task is ambiguous or would violate a rule, STOP and report rather than guessing.",
857
+ "",
858
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 YOUR TASK \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
859
+ ].join("\n");
860
+ }
861
+ function composeDispatchPrompt(taskPrompt, opts = {}) {
862
+ return `${buildDispatchOnboarding(opts)}
863
+ ${String(taskPrompt ?? "").trim()}
864
+ `;
865
+ }
866
+
867
+ // ../../scripts/virtual-office/code-runner/session-spool-forwarder.mjs
868
+ import { homedir as homedir3 } from "node:os";
869
+ import { join as join3 } from "node:path";
870
+ import { readdir, readFile, unlink, writeFile } from "node:fs/promises";
871
+ import { createHash } from "node:crypto";
872
+ var SPOOL_DIR = join3(homedir3(), ".vo", "session-spool");
873
+ var CLOUD_MAP_FILE = join3(homedir3(), ".vo", "session-cloud-map.json");
874
+ var STALE_MS = 60 * 60 * 1e3;
875
+ var ACTIVE_SILENCE_MS = 10 * 60 * 1e3;
876
+ function deriveUuid(seed) {
877
+ const h = createHash("sha256").update(seed).digest("hex");
878
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-5${h.slice(13, 16)}-${(parseInt(h.slice(16, 18), 16) & 63 | 128).toString(16)}${h.slice(18, 20)}-${h.slice(20, 32)}`;
879
+ }
880
+ function spoolToCloud(record, ids) {
881
+ const ended = record.status === "ended";
882
+ const silentMs = Date.now() - Date.parse(record.last_seen_at || 0);
883
+ const status = ended ? "handed_off" : silentMs > ACTIVE_SILENCE_MS ? "abandoned" : "active";
884
+ return {
885
+ session_id: deriveUuid(`vo-session:${record.session_key}`),
886
+ operator_id: ids.operator_id,
887
+ tenant_id: ids.tenant_id,
888
+ agent_type: record.agent_type === "claude-code" ? "claude-code" : "other",
889
+ current_goal: (record.current_goal || "Interactive Claude Code session").slice(0, 2e3),
890
+ status,
891
+ last_seen_at: record.last_seen_at
892
+ };
893
+ }
894
+ async function readSpool(spoolDir = SPOOL_DIR) {
895
+ let files = [];
896
+ try {
897
+ files = await readdir(spoolDir);
898
+ } catch {
899
+ return [];
900
+ }
901
+ const out = [];
902
+ for (const f of files) {
903
+ if (!f.endsWith(".json")) continue;
904
+ try {
905
+ const record = JSON.parse(await readFile(join3(spoolDir, f), "utf8"));
906
+ if (record && typeof record.session_key === "string") {
907
+ out.push({ full: join3(spoolDir, f), record });
908
+ }
909
+ } catch {
910
+ }
911
+ }
912
+ return out;
913
+ }
914
+ async function readCloudMap(path3) {
915
+ try {
916
+ return JSON.parse(await readFile(path3, "utf8"));
917
+ } catch {
918
+ return {};
919
+ }
920
+ }
921
+ async function forwardSessionSpool(deps) {
922
+ const fetchImpl = deps.fetchImpl ?? fetch;
923
+ const now = deps.now ? deps.now() : Date.now();
924
+ const mapPath = deps.cloudMapPath ?? CLOUD_MAP_FILE;
925
+ const ids = {
926
+ operator_id: deriveUuid(`vo-operator:${deps.operatorSeed}`),
927
+ tenant_id: deriveUuid(`vo-tenant:${deps.operatorSeed}`)
928
+ };
929
+ const entries = await readSpool(deps.spoolDir);
930
+ const cloudMap = await readCloudMap(mapPath);
931
+ let forwarded = 0;
932
+ let pruned = 0;
933
+ for (const { full, record } of entries) {
934
+ const key = record.session_key;
935
+ const lastSeen = Date.parse(record.last_seen_at || 0);
936
+ const isPrune = record.status === "ended" && now - lastSeen > ACTIVE_SILENCE_MS || now - lastSeen > STALE_MS;
937
+ const cloud = spoolToCloud(record, ids);
938
+ try {
939
+ let cloudSessionId = cloudMap[key];
940
+ if (!cloudSessionId) {
941
+ const res = await fetchImpl(`${deps.baseUrl}/api/v1/session`, {
942
+ method: "POST",
943
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${deps.token}` },
944
+ body: JSON.stringify({
945
+ operator_id: cloud.operator_id,
946
+ tenant_id: cloud.tenant_id,
947
+ agent_type: cloud.agent_type,
948
+ current_goal: cloud.current_goal
949
+ })
950
+ });
951
+ const body = await res.json().catch(() => null);
952
+ cloudSessionId = body?.session?.session_id ?? null;
953
+ if (cloudSessionId) cloudMap[key] = cloudSessionId;
954
+ }
955
+ if (cloudSessionId) {
956
+ await fetchImpl(`${deps.baseUrl}/api/v1/session/${cloudSessionId}/report-state`, {
957
+ method: "POST",
958
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${deps.token}` },
959
+ body: JSON.stringify({
960
+ context_used_pct: 0,
961
+ current_goal: cloud.current_goal,
962
+ status: cloud.status
963
+ })
964
+ }).catch(() => {
965
+ });
966
+ forwarded++;
967
+ }
968
+ } catch {
969
+ }
970
+ if (isPrune) {
971
+ try {
972
+ await unlink(full);
973
+ pruned++;
974
+ } catch {
975
+ }
976
+ delete cloudMap[key];
977
+ }
978
+ }
979
+ try {
980
+ await writeFile(mapPath, JSON.stringify(cloudMap), "utf8");
981
+ } catch {
982
+ }
983
+ return { forwarded, pruned };
984
+ }
985
+
986
+ // ../../scripts/virtual-office/code-runner/pr-watcher.mjs
987
+ import { homedir as homedir4 } from "node:os";
988
+ import { join as join4 } from "node:path";
989
+ import { readFile as readFile2, writeFile as writeFile2, mkdir } from "node:fs/promises";
990
+ import { spawnSync as spawnSync4 } from "node:child_process";
991
+ var CI_FIX_MARKER = "[VO-CI-FIX]";
992
+ function ghViewPr(prNumber, repo) {
993
+ const r = spawnSync4(
994
+ "gh",
995
+ ["pr", "view", String(prNumber), "-R", repo, "--json", "state,statusCheckRollup,headRefName"],
996
+ { encoding: "utf8", timeout: 3e4 }
997
+ );
998
+ if (r.status !== 0) throw new Error((r.stderr || "gh pr view failed").slice(-200));
999
+ return JSON.parse(r.stdout || "{}");
1000
+ }
1001
+ var DEFAULT_STATE_FILE = join4(homedir4(), ".vo", "dispatched-prs.json");
1002
+ var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set([
1003
+ "FAILURE",
1004
+ "TIMED_OUT",
1005
+ "CANCELLED",
1006
+ "ACTION_REQUIRED",
1007
+ "ERROR",
1008
+ "STARTUP_FAILURE",
1009
+ "STALE"
1010
+ ]);
1011
+ var STALE_MS2 = 24 * 60 * 60 * 1e3;
1012
+ var MAX_ENQUEUE_ERRORS = 3;
1013
+ function parsePrCiStatus(view) {
1014
+ const state = (view && typeof view.state === "string" ? view.state : "UNKNOWN").toUpperCase();
1015
+ const rollup = view && Array.isArray(view.statusCheckRollup) ? view.statusCheckRollup : [];
1016
+ const failedChecks = [];
1017
+ let pending = false;
1018
+ for (const c of rollup) {
1019
+ const name = c.name || c.context || "check";
1020
+ const conclusion = String(c.conclusion || "").toUpperCase();
1021
+ if (conclusion) {
1022
+ if (FAIL_CONCLUSIONS.has(conclusion)) failedChecks.push(name);
1023
+ } else if (c.status) {
1024
+ pending = true;
1025
+ } else {
1026
+ const st = String(c.state || "").toUpperCase();
1027
+ if (st === "FAILURE" || st === "ERROR") failedChecks.push(name);
1028
+ else if (st !== "SUCCESS") pending = true;
1029
+ }
1030
+ }
1031
+ const ci = failedChecks.length > 0 ? "failing" : pending ? "pending" : "passing";
1032
+ return { state, ci, failedChecks, branch: view && view.headRefName || null };
1033
+ }
1034
+ function decideWatchAction(pr, fixAttempts, maxFixAttempts) {
1035
+ if (pr.state !== "OPEN") return "untrack";
1036
+ if (pr.ci === "failing" && (fixAttempts || 0) < maxFixAttempts) return "fix";
1037
+ return "wait";
1038
+ }
1039
+ function buildCiFixPrompt({ prNumber, repo, branch, failedChecks }) {
1040
+ return [
1041
+ `${CI_FIX_MARKER} A VO-dispatched pull request has FAILING CI and needs a fix.`,
1042
+ "",
1043
+ `Repo: ${repo}`,
1044
+ `PR: #${prNumber} (head branch: ${branch || "unknown"})`,
1045
+ `Failing checks: ${failedChecks && failedChecks.length ? failedChecks.join(", ") : "unknown"}`,
1046
+ "",
1047
+ "Diagnose the failure from the failing check NAMES + the PR diff (you cannot run gh).",
1048
+ "Fix it. Follow the PR Freshness / safe-rebuild protocol: open a FRESH fix on a new",
1049
+ `branch off current main that SUPERSEDES PR #${prNumber} (note "Supersedes #${prNumber}"`,
1050
+ "in your summary). Leave UNCOMMITTED edits and STOP \u2014 the runner opens the PR. A denied",
1051
+ "git/gh is EXPECTED; do NOT retry it."
1052
+ ].join("\n");
1053
+ }
1054
+ async function readState(stateFile) {
1055
+ try {
1056
+ const parsed = JSON.parse(await readFile2(stateFile, "utf8"));
1057
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1058
+ } catch {
1059
+ return {};
1060
+ }
1061
+ }
1062
+ async function writeState(stateFile, state) {
1063
+ try {
1064
+ await mkdir(join4(stateFile, ".."), { recursive: true });
1065
+ await writeFile2(stateFile, JSON.stringify(state, null, 2), "utf8");
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ async function trackDispatchedPr({ prNumber, repo, branch, taskId }, { stateFile = DEFAULT_STATE_FILE, now = () => Date.now() } = {}) {
1070
+ if (!prNumber || !repo) return;
1071
+ const state = await readState(stateFile);
1072
+ state[String(prNumber)] = {
1073
+ repo,
1074
+ branch: branch || null,
1075
+ taskId: taskId || null,
1076
+ fixAttempts: 0,
1077
+ trackedAt: now()
1078
+ };
1079
+ await writeState(stateFile, state);
1080
+ }
1081
+ async function runWatchCycle({ viewPr, enqueueFix, log: log2 = () => {
1082
+ }, now = () => Date.now(), maxFixAttempts = 1, stateFile = DEFAULT_STATE_FILE }) {
1083
+ const state = await readState(stateFile);
1084
+ const prNumbers = Object.keys(state);
1085
+ let checked = 0;
1086
+ let fixed = 0;
1087
+ let untracked = 0;
1088
+ for (const prNumber of prNumbers) {
1089
+ const entry = state[prNumber];
1090
+ let view;
1091
+ try {
1092
+ view = await viewPr(prNumber, entry.repo);
1093
+ } catch (err) {
1094
+ log2(`watch: pr #${prNumber} view failed: ${err.message}`);
1095
+ continue;
1096
+ }
1097
+ checked += 1;
1098
+ const pr = parsePrCiStatus(view);
1099
+ entry.lastCi = pr.ci;
1100
+ const action = decideWatchAction(pr, entry.fixAttempts, maxFixAttempts);
1101
+ if (action === "untrack") {
1102
+ delete state[prNumber];
1103
+ untracked += 1;
1104
+ log2(`watch: pr #${prNumber} is ${pr.state} \u2014 untracked`);
1105
+ } else if (action === "fix") {
1106
+ entry.fixAttempts = (entry.fixAttempts || 0) + 1;
1107
+ entry.lastCheckedAt = now();
1108
+ try {
1109
+ await enqueueFix({ prNumber: Number(prNumber), repo: entry.repo, branch: pr.branch || entry.branch, failedChecks: pr.failedChecks });
1110
+ fixed += 1;
1111
+ log2(`watch: pr #${prNumber} CI failing (${pr.failedChecks.join(", ") || "unknown"}) \u2014 dispatched fix ${entry.fixAttempts}/${maxFixAttempts}`);
1112
+ } catch (err) {
1113
+ entry.enqueueErrors = (entry.enqueueErrors || 0) + 1;
1114
+ if (entry.enqueueErrors >= MAX_ENQUEUE_ERRORS) {
1115
+ log2(`watch: pr #${prNumber} fix enqueue failed ${entry.enqueueErrors}x \u2014 giving up: ${err.message}`);
1116
+ } else {
1117
+ entry.fixAttempts = Math.max(0, (entry.fixAttempts || 1) - 1);
1118
+ log2(`watch: pr #${prNumber} fix enqueue failed (${entry.enqueueErrors}/${MAX_ENQUEUE_ERRORS}): ${err.message}`);
1119
+ }
1120
+ }
1121
+ } else {
1122
+ entry.lastCheckedAt = now();
1123
+ }
1124
+ }
1125
+ for (const [n, e] of Object.entries(state)) {
1126
+ const cappedFailing = e.lastCi === "failing" && (e.fixAttempts || 0) >= maxFixAttempts;
1127
+ if (cappedFailing && e.trackedAt && now() - e.trackedAt > STALE_MS2) {
1128
+ delete state[n];
1129
+ untracked += 1;
1130
+ log2(`watch: pr #${n} capped + failing + tracked >24h ago \u2014 pruned from watch state`);
1131
+ }
1132
+ }
1133
+ await writeState(stateFile, state);
1134
+ return { checked, fixed, untracked };
1135
+ }
1136
+ function makeWatchRunner({ client, viewPr = ghViewPr, log: log2, maxFixAttempts }) {
1137
+ return () => runWatchCycle({
1138
+ viewPr,
1139
+ enqueueFix: ({ prNumber, repo, branch, failedChecks }) => client.enqueueCodeTask({ repo, prompt: buildCiFixPrompt({ prNumber, repo, branch, failedChecks }) }),
1140
+ log: log2,
1141
+ maxFixAttempts
1142
+ });
1143
+ }
1144
+
1145
+ // ../../scripts/virtual-office/code-runner/control-server.mjs
1146
+ import { createServer } from "node:http";
1147
+ var LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
1148
+ function resolveCorsOrigin(reqOrigin, allowedOrigin) {
1149
+ if (typeof reqOrigin === "string" && (reqOrigin === allowedOrigin || LOCALHOST_ORIGIN_RE.test(reqOrigin))) {
1150
+ return reqOrigin;
1151
+ }
1152
+ return allowedOrigin;
1153
+ }
1154
+ function isControlOriginAllowed(reqOrigin, allowedOrigin) {
1155
+ return !reqOrigin || resolveCorsOrigin(reqOrigin, allowedOrigin) === reqOrigin;
1156
+ }
1157
+ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
1158
+ return (req, res) => {
1159
+ res.setHeader("Access-Control-Allow-Origin", resolveCorsOrigin(req.headers.origin, allowedOrigin));
1160
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1161
+ res.setHeader("Access-Control-Allow-Headers", "content-type, x-vo-control");
1162
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
1163
+ res.setHeader("Vary", "Origin");
1164
+ res.setHeader("Cache-Control", "no-store");
1165
+ if (req.method === "OPTIONS") {
1166
+ res.statusCode = 204;
1167
+ res.end();
1168
+ return;
1169
+ }
1170
+ const path3 = String(req.url || "").split("?")[0];
1171
+ res.setHeader("content-type", "application/json");
1172
+ if (req.method === "GET" && path3 === "/status") {
1173
+ let status;
1174
+ try {
1175
+ status = getStatus();
1176
+ } catch {
1177
+ status = {};
1178
+ }
1179
+ res.statusCode = 200;
1180
+ res.end(JSON.stringify({ ok: true, ...status }));
1181
+ return;
1182
+ }
1183
+ if (req.method === "POST" && path3 === "/stop") {
1184
+ if (!isControlOriginAllowed(req.headers.origin, allowedOrigin) || !req.headers["x-vo-control"]) {
1185
+ res.statusCode = 403;
1186
+ res.end(JSON.stringify({ ok: false, error: "forbidden" }));
1187
+ return;
1188
+ }
1189
+ try {
1190
+ requestStop("web-control");
1191
+ } catch {
1192
+ }
1193
+ res.statusCode = 200;
1194
+ res.end(JSON.stringify({ ok: true, stopping: true }));
1195
+ return;
1196
+ }
1197
+ res.statusCode = 404;
1198
+ res.end(JSON.stringify({ ok: false, error: "not_found" }));
1199
+ };
1200
+ }
1201
+ function startControlServer({ port, getStatus, requestStop, allowedOrigin, log: log2 = () => {
1202
+ } }) {
1203
+ const server = createServer(buildControlHandler({ getStatus, requestStop, allowedOrigin }));
1204
+ server.on("error", (e) => log2(`control server error: ${e.message} (in-product runner control disabled)`));
1205
+ server.listen(port, "127.0.0.1", () => log2(`control server on http://127.0.0.1:${port} (allow ${allowedOrigin})`));
1206
+ server.unref?.();
1207
+ return server;
1208
+ }
1209
+ function startDaemonControl({ cfg, requestStop, getActiveCount, isRunning, startedAt, log: log2 = () => {
1210
+ } }) {
1211
+ if (!cfg.controlEnabled) return null;
1212
+ return startControlServer({
1213
+ port: cfg.controlPort,
1214
+ allowedOrigin: cfg.appOrigin,
1215
+ requestStop,
1216
+ getStatus: () => ({
1217
+ running: isRunning(),
1218
+ pid: process.pid,
1219
+ runnerId: cfg.runnerId,
1220
+ servedRepos: cfg.servedRepos,
1221
+ watchEnabled: cfg.watchEnabled,
1222
+ activeTasks: getActiveCount(),
1223
+ startedAt: new Date(startedAt).toISOString(),
1224
+ uptimeSec: Math.round((Date.now() - startedAt) / 1e3)
1225
+ }),
1226
+ log: log2
1227
+ });
1228
+ }
1229
+
1230
+ // ../../scripts/virtual-office/code-runner/effort-mode-config.mjs
1231
+ var EFFORT_MODE_CONFIG = {
1232
+ fast: {
1233
+ tier: "cheap",
1234
+ permissionMode: "acceptEdits",
1235
+ maxTurns: 20,
1236
+ thinkingDirective: "",
1237
+ multiAgentInstruction: ""
1238
+ },
1239
+ standard: {
1240
+ tier: "mid",
1241
+ permissionMode: "acceptEdits",
1242
+ maxTurns: 40,
1243
+ thinkingDirective: "",
1244
+ multiAgentInstruction: ""
1245
+ },
1246
+ deep: {
1247
+ tier: "best",
1248
+ permissionMode: "acceptEdits",
1249
+ maxTurns: 60,
1250
+ thinkingDirective: "Think step-by-step. Verify assumptions against source code. Check edge cases.",
1251
+ multiAgentInstruction: ""
1252
+ },
1253
+ ultra: {
1254
+ tier: "best",
1255
+ permissionMode: "acceptEdits",
1256
+ maxTurns: 80,
1257
+ thinkingDirective: "Think step-by-step. Exhaustively verify every assumption against source code and documentation. Adversarially review your own work.",
1258
+ multiAgentInstruction: "If this task needs multiple phases (research, build, verify), propose a plan first."
1259
+ },
1260
+ ultracode: {
1261
+ tier: "best",
1262
+ permissionMode: "default",
1263
+ maxTurns: 120,
1264
+ thinkingDirective: "Think step-by-step. Exhaustively verify every assumption against source code and documentation. Build worked examples to validate correctness. Adversarially review your own work.",
1265
+ multiAgentInstruction: "Decompose this work into parallel research, build, and verification streams; use workflow orchestration where it helps."
1266
+ }
1267
+ };
1268
+ var DEFAULT_MODE = "standard";
1269
+ function resolveEffortMode(mode) {
1270
+ const normalized = String(mode || "").trim().toLowerCase();
1271
+ return EFFORT_MODE_CONFIG[normalized] || EFFORT_MODE_CONFIG[DEFAULT_MODE];
1272
+ }
1273
+ function composeEffortPrompt(basePrompt, effortConfig) {
1274
+ const parts = [];
1275
+ if (effortConfig.thinkingDirective) {
1276
+ parts.push(`## Thinking directive
1277
+ ${effortConfig.thinkingDirective}
1278
+ `);
1279
+ }
1280
+ if (effortConfig.multiAgentInstruction) {
1281
+ parts.push(`## Multi-agent instruction
1282
+ ${effortConfig.multiAgentInstruction}
1283
+ `);
1284
+ }
1285
+ parts.push(String(basePrompt || "").trim());
1286
+ return parts.join("\n");
1287
+ }
1288
+
1289
+ // ../../scripts/virtual-office/model-registry.mjs
1290
+ import fs2 from "node:fs";
1291
+ import path2 from "node:path";
1292
+ import { fileURLToPath } from "node:url";
1293
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
1294
+ var ROOT = path2.resolve(__dirname, "..", "..");
1295
+ var DEFAULT_CACHE_DIR = path2.join(ROOT, ".virtual-office-cache", "model-registry");
1296
+ var DEFAULT_CACHE_FILE = path2.join(DEFAULT_CACHE_DIR, "catalog.json");
1297
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
1298
+ var ANTHROPIC_API_VERSION = "2023-06-01";
1299
+ var FAMILY_DEFINITIONS = {
1300
+ "anthropic-flagship": {
1301
+ provider: "anthropic",
1302
+ include: [/^claude-opus/i],
1303
+ // Reject `-fast` SKUs: they cost more and route to the same underlying
1304
+ // weights, and at least one (`claude-opus-4-7-fast`) gets silently
1305
+ // substituted server-side when callers ask for it (observed 2026-05-14:
1306
+ // 24 model-fallback events per consensus run, collapsing diversity).
1307
+ exclude: [/haiku/i, /-fast(?:[-.]|$)/i],
1308
+ fallbacks: [
1309
+ "claude-opus-4-8[1m]",
1310
+ "claude-opus-4-8",
1311
+ "claude-opus-4-7",
1312
+ "claude-opus-4-6",
1313
+ "claude-opus-4-5-20251101",
1314
+ "claude-sonnet-4-6"
1315
+ ]
1316
+ },
1317
+ "anthropic-balanced": {
1318
+ provider: "anthropic",
1319
+ include: [/^claude-sonnet/i],
1320
+ exclude: [/haiku/i, /-fast(?:[-.]|$)/i],
1321
+ fallbacks: ["claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "claude-sonnet-4-20250514"]
1322
+ },
1323
+ "openai-flagship": {
1324
+ provider: "openai",
1325
+ include: [/^gpt-\d+(?:[.-]\d+)?$/i, /^gpt-\d+(?:[.-]\d+)?-pro$/i],
1326
+ // -fast SKUs cost more and silently downgrade server-side; want standard.
1327
+ exclude: [/mini|nano|chat|codex/i, /-fast(?:[-.]|$)/i],
1328
+ fallbacks: ["gpt-5.4", "gpt-5.2", "gpt-5.3-codex"]
1329
+ },
1330
+ "openai-coding": {
1331
+ provider: "openai",
1332
+ include: [/codex/i],
1333
+ exclude: [/mini|nano/i, /-fast(?:[-.]|$)/i],
1334
+ fallbacks: ["gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.4"]
1335
+ },
1336
+ "google-pro": {
1337
+ provider: "google",
1338
+ include: [/^gemini-.*pro/i],
1339
+ exclude: [/vision|embedding|customtools/i, /-fast(?:[-.]|$)/i],
1340
+ fallbacks: ["gemini-2.5-pro", "gemini-3.1-pro-preview"]
1341
+ },
1342
+ "google-flash": {
1343
+ provider: "google",
1344
+ // 'google-flash' is the explicit fast/low-latency family — DON'T exclude
1345
+ // -fast here; that's the whole point of this family. Other families
1346
+ // exclude -fast to avoid the silent server-side substitution problem.
1347
+ include: [/^gemini-.*flash/i],
1348
+ exclude: [/vision|embedding/i],
1349
+ fallbacks: ["gemini-2.5-flash", "gemini-3-flash-preview"]
1350
+ }
1351
+ };
1352
+ var memoryCache = null;
1353
+ function uniqueModels(models = []) {
1354
+ return [...new Set(models.map((model) => String(model || "").trim()).filter(Boolean))];
1355
+ }
1356
+ function normalizeProvider(value = "") {
1357
+ const lower = String(value || "").trim().toLowerCase();
1358
+ if (lower.includes("anthropic")) return "anthropic";
1359
+ if (lower.includes("openai")) return "openai";
1360
+ if (lower.includes("google") || lower.includes("gemini")) return "google";
1361
+ return lower;
1362
+ }
1363
+ function stripProviderPrefix(id = "") {
1364
+ const raw = String(id || "").trim();
1365
+ if (!raw.includes("/")) return raw.replace(/^models\//, "");
1366
+ return raw.split("/").slice(1).join("/").replace(/^models\//, "");
1367
+ }
1368
+ function canonicalizeRegistryModelId(id = "", provider = "") {
1369
+ let normalized = stripProviderPrefix(id).trim();
1370
+ const normalizedProvider = normalizeProvider(provider) || inferProviderFromId(normalized);
1371
+ if (normalizedProvider === "anthropic") {
1372
+ normalized = normalized.replace(/^(claude-(?:opus|sonnet|haiku)-\d+)\.(\d+)(.*)$/i, "$1-$2$3");
1373
+ }
1374
+ if (normalizedProvider === "google") {
1375
+ normalized = normalized.replace(/-customtools$/i, "");
1376
+ }
1377
+ return normalized;
1378
+ }
1379
+ function inferProviderFromId(rawId = "", explicitProvider = "") {
1380
+ const provider = normalizeProvider(explicitProvider);
1381
+ if (provider) return provider;
1382
+ const id = String(rawId || "").toLowerCase();
1383
+ if (id.startsWith("anthropic/") || id.includes("claude")) return "anthropic";
1384
+ if (id.startsWith("openai/") || /^gpt-|^o\d/.test(stripProviderPrefix(id))) return "openai";
1385
+ if (id.startsWith("google/") || id.includes("gemini")) return "google";
1386
+ return "unknown";
1387
+ }
1388
+ function normalizeCatalogModel(model = {}) {
1389
+ const rawId = String(model.id || model.name || model.modelId || "").trim();
1390
+ const provider = inferProviderFromId(rawId, model.provider || model.owned_by || model.owner || model.developer);
1391
+ const id = canonicalizeRegistryModelId(rawId, provider);
1392
+ if (!id) return null;
1393
+ return {
1394
+ id,
1395
+ rawId,
1396
+ name: String(model.display_name || model.displayName || model.name || id).replace(/^models\//, ""),
1397
+ provider,
1398
+ source: model.source || "unknown",
1399
+ createdAt: model.created_at || model.createdAt || model.created || ""
1400
+ };
1401
+ }
1402
+ function parseVersionScore(id = "") {
1403
+ const lower = String(id || "").toLowerCase();
1404
+ const numbers = [...lower.matchAll(/\d+/g)].map((match) => Number(match[0])).filter(Number.isFinite);
1405
+ let score = 0;
1406
+ for (let i = 0; i < numbers.length; i++) score += numbers[i] / Math.pow(1e3, i);
1407
+ if (/opus|pro|flagship/.test(lower)) score += 10;
1408
+ if (/sonnet/.test(lower)) score += 5;
1409
+ if (/preview|latest/.test(lower)) score += 0.25;
1410
+ if (/\[1m\]|\(1m\)/i.test(lower)) score += 1;
1411
+ if (/mini|nano|haiku|lite/.test(lower)) score -= 20;
1412
+ return score;
1413
+ }
1414
+ function familyMatches(model, family) {
1415
+ const def = FAMILY_DEFINITIONS[family];
1416
+ if (!def) return false;
1417
+ const id = String(model?.id || "").trim();
1418
+ if (!id || normalizeProvider(model.provider) !== def.provider) return false;
1419
+ if (def.exclude?.some((pattern) => pattern.test(id))) return false;
1420
+ return def.include?.some((pattern) => pattern.test(id)) ?? false;
1421
+ }
1422
+ function selectBestFamilyModel(models = [], family) {
1423
+ const matches = models.filter((model) => familyMatches(model, family));
1424
+ matches.sort((left, right) => {
1425
+ const scoreDelta = parseVersionScore(right.id) - parseVersionScore(left.id);
1426
+ if (scoreDelta !== 0) return scoreDelta;
1427
+ return String(right.createdAt || "").localeCompare(String(left.createdAt || ""));
1428
+ });
1429
+ return matches[0]?.id || "";
1430
+ }
1431
+ async function fetchJson(fetchImpl, url, options = {}) {
1432
+ const res = await fetchImpl(url, options);
1433
+ if (!res?.ok) return null;
1434
+ return await res.json().catch(() => null);
1435
+ }
1436
+ async function fetchOpenRouterModels(fetchImpl) {
1437
+ const data = await fetchJson(fetchImpl, "https://openrouter.ai/api/v1/models");
1438
+ return (data?.data || []).map((model) => normalizeCatalogModel({ ...model, source: "openrouter" })).filter(Boolean);
1439
+ }
1440
+ async function fetchAnthropicModels(fetchImpl, env2 = process.env) {
1441
+ const apiKey = env2.ANTHROPIC_API_KEY;
1442
+ if (!apiKey) return [];
1443
+ const data = await fetchJson(fetchImpl, "https://api.anthropic.com/v1/models?limit=1000", {
1444
+ headers: { "x-api-key": apiKey, "anthropic-version": ANTHROPIC_API_VERSION }
1445
+ });
1446
+ return (data?.data || []).map((model) => normalizeCatalogModel({ ...model, provider: "anthropic", source: "anthropic" })).filter(Boolean);
1447
+ }
1448
+ async function fetchOpenAIModels(fetchImpl, env2 = process.env) {
1449
+ const apiKey = env2.OPENAI_API_KEY;
1450
+ if (!apiKey) return [];
1451
+ const data = await fetchJson(fetchImpl, "https://api.openai.com/v1/models", {
1452
+ headers: { Authorization: `Bearer ${apiKey}` }
1453
+ });
1454
+ return (data?.data || []).map((model) => normalizeCatalogModel({ ...model, provider: "openai", source: "openai" })).filter(Boolean);
1455
+ }
1456
+ async function fetchGoogleModels(fetchImpl, env2 = process.env) {
1457
+ const apiKey = env2.GOOGLE_AI_API_KEY || env2.GEMINI_API_KEY || env2.GOOGLE_API_KEY;
1458
+ if (!apiKey) return [];
1459
+ const data = await fetchJson(fetchImpl, `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`);
1460
+ return (data?.models || []).map((model) => normalizeCatalogModel({ ...model, provider: "google", source: "google" })).filter(Boolean);
1461
+ }
1462
+ function readCache(cacheFile = DEFAULT_CACHE_FILE, nowMs = Date.now(), ttlMs = DEFAULT_TTL_MS) {
1463
+ if (!fs2.existsSync(cacheFile)) return null;
1464
+ try {
1465
+ const parsed = JSON.parse(fs2.readFileSync(cacheFile, "utf-8"));
1466
+ if (nowMs - Number(parsed.checkedAtMs || 0) > ttlMs) return null;
1467
+ if (!Array.isArray(parsed.models)) return null;
1468
+ return parsed;
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ }
1473
+ function writeCache(cacheFile = DEFAULT_CACHE_FILE, payload) {
1474
+ fs2.mkdirSync(path2.dirname(cacheFile), { recursive: true });
1475
+ fs2.writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
1476
+ }
1477
+ async function fetchRegistryCatalog({
1478
+ fetchImpl = fetch,
1479
+ env: env2 = process.env,
1480
+ cacheFile = DEFAULT_CACHE_FILE,
1481
+ nowMs = Date.now()
1482
+ } = {}) {
1483
+ const sources = await Promise.allSettled([
1484
+ fetchOpenRouterModels(fetchImpl),
1485
+ fetchAnthropicModels(fetchImpl, env2),
1486
+ fetchOpenAIModels(fetchImpl, env2),
1487
+ fetchGoogleModels(fetchImpl, env2)
1488
+ ]);
1489
+ const models = uniqueModels(
1490
+ sources.flatMap((result) => result.status === "fulfilled" ? result.value : []).map((model) => JSON.stringify(model))
1491
+ ).map((raw) => JSON.parse(raw));
1492
+ const payload = { checkedAt: new Date(nowMs).toISOString(), checkedAtMs: nowMs, models };
1493
+ if (models.length > 0) writeCache(cacheFile, payload);
1494
+ return payload;
1495
+ }
1496
+ async function getModelRegistryCatalog({
1497
+ fetchImpl = fetch,
1498
+ env: env2 = process.env,
1499
+ cacheFile = DEFAULT_CACHE_FILE,
1500
+ ttlMs = Number(env2.VO_MODEL_REGISTRY_TTL_MS || DEFAULT_TTL_MS),
1501
+ nowMs = Date.now(),
1502
+ forceRefresh = false
1503
+ } = {}) {
1504
+ if (!forceRefresh && memoryCache && nowMs - memoryCache.checkedAtMs <= ttlMs) return memoryCache;
1505
+ if (!forceRefresh) {
1506
+ const cached2 = readCache(cacheFile, nowMs, ttlMs);
1507
+ if (cached2) {
1508
+ memoryCache = cached2;
1509
+ return cached2;
1510
+ }
1511
+ }
1512
+ if (env2.VO_MODEL_REGISTRY_OFFLINE === "1") return { checkedAt: "", checkedAtMs: nowMs, models: [] };
1513
+ try {
1514
+ memoryCache = await fetchRegistryCatalog({ fetchImpl, env: env2, cacheFile, nowMs });
1515
+ return memoryCache;
1516
+ } catch {
1517
+ const cached2 = readCache(cacheFile, nowMs, Number.MAX_SAFE_INTEGER);
1518
+ return cached2 || { checkedAt: "", checkedAtMs: nowMs, models: [] };
1519
+ }
1520
+ }
1521
+ async function resolveModelFamily(family, options = {}) {
1522
+ const def = FAMILY_DEFINITIONS[family];
1523
+ if (!def) return String(family || "").trim();
1524
+ const catalog = await getModelRegistryCatalog(options);
1525
+ const resolved = selectBestFamilyModel(catalog.models || [], family);
1526
+ return resolved || def.fallbacks[0];
1527
+ }
1528
+
1529
+ // ../../scripts/virtual-office/code-runner/model-router.mjs
1530
+ function classifyTier(prompt) {
1531
+ const text = String(prompt || "").trim();
1532
+ if (!text) return "mid";
1533
+ const lower = text.toLowerCase();
1534
+ if (/lint|format|typo|missing import|update deps|chore|maintenance|runner|daemon/.test(
1535
+ lower
1536
+ )) {
1537
+ return "cheap";
1538
+ }
1539
+ if (/generate roadmap|new feature|implement .* feature|major refactor|strategic/.test(
1540
+ lower
1541
+ ) || text.length > 1500) {
1542
+ return "best";
1543
+ }
1544
+ return "mid";
1545
+ }
1546
+ async function resolveModelForTier(tier, { resolveModelFamily: resolver = resolveModelFamily } = {}) {
1547
+ const t = String(tier || "mid").trim();
1548
+ if (t === "cheap") {
1549
+ const resolved2 = await resolver("anthropic-balanced");
1550
+ return resolved2 || "claude-sonnet-4-6";
1551
+ }
1552
+ if (t === "best") {
1553
+ const resolved2 = await resolver("anthropic-flagship");
1554
+ return resolved2 || "claude-opus-4-8";
1555
+ }
1556
+ const resolved = await resolver("anthropic-flagship");
1557
+ return resolved || "claude-opus-4-7";
1558
+ }
1559
+ async function resolveTaskModel(task) {
1560
+ const tier = task.tier && task.tier !== "auto" ? task.tier : classifyTier(task.prompt);
1561
+ const model = await resolveModelForTier(tier);
1562
+ return { tier, model };
1563
+ }
1564
+
1565
+ // ../../scripts/virtual-office/code-runner/apply-effort-mode.mjs
1566
+ async function resolveEffortDispatch({ client, task, env: env2, basePrompt, resolveModel = resolveTaskModel }) {
1567
+ const dispatchMode = await client.getDispatchMode().catch(() => "standard");
1568
+ const effortConfig = resolveEffortMode(dispatchMode);
1569
+ const { tier, model } = await resolveModel({ ...task, tier: task.tier ?? effortConfig.tier });
1570
+ return {
1571
+ dispatchMode,
1572
+ tier,
1573
+ model,
1574
+ permissionMode: env2.VO_CODE_RUNNER_PERMISSION_MODE || effortConfig.permissionMode,
1575
+ maxTurns: typeof task.max_turns === "number" ? task.max_turns : effortConfig.maxTurns,
1576
+ prompt: composeEffortPrompt(basePrompt, effortConfig)
1577
+ };
1578
+ }
1579
+
1580
+ // ../../scripts/virtual-office/code-runner/claim-scoping-log.mjs
1581
+ function envProvided(raw) {
1582
+ return typeof raw === "string" && raw.length > 0;
1583
+ }
1584
+ function describeClaimScoping(cfg = {}, env2 = {}) {
1585
+ const repos = cfg.servedRepos ?? [];
1586
+ const operators = cfg.servedOperators ?? [];
1587
+ const repoScoped = repos.length > 0;
1588
+ const opScoped = operators.length > 0;
1589
+ const reposEnvSet = envProvided(env2.VO_CODE_RUNNER_REPOS);
1590
+ const opsEnvSet = envProvided(env2.VO_CODE_RUNNER_OPERATOR_IDS);
1591
+ const lines = [];
1592
+ if (repoScoped) lines.push(`claim-scoped to repos: ${repos.join(", ")}`);
1593
+ if (opScoped) lines.push(`claim-scoped to operators: ${operators.join(", ")}`);
1594
+ if (reposEnvSet && !repoScoped) {
1595
+ lines.push(
1596
+ "WARNING: VO_CODE_RUNNER_REPOS is set but has no valid entries \u2014 repo scoping is OFF (claims any repo)."
1597
+ );
1598
+ }
1599
+ if (opsEnvSet && !opScoped) {
1600
+ lines.push(
1601
+ "WARNING: VO_CODE_RUNNER_OPERATOR_IDS is set but has no valid entries \u2014 operator scoping is OFF (claims any operator)."
1602
+ );
1603
+ }
1604
+ if (repoScoped && !opScoped) {
1605
+ lines.push(
1606
+ "WARNING: operator scoping is OFF \u2014 this runner may claim ANY operator's tasks on the served repos. Set VO_CODE_RUNNER_OPERATOR_IDS to bind it to your operator (bring-your-own-runner)."
1607
+ );
1608
+ }
1609
+ if (opScoped && !repoScoped) {
1610
+ lines.push(
1611
+ "WARNING: repo scoping is OFF \u2014 this runner may claim the served operators' tasks on ANY repo. Set VO_CODE_RUNNER_REPOS to constrain it."
1612
+ );
1613
+ }
1614
+ if (!repoScoped && !opScoped) {
1615
+ lines.push(
1616
+ "WARNING: no claim scoping \u2014 this daemon claims ANY pending task. Set VO_CODE_RUNNER_REPOS and/or VO_CODE_RUNNER_OPERATOR_IDS to scope claims to this machine."
1617
+ );
1618
+ }
1619
+ return lines;
1620
+ }
1621
+
1622
+ // ../../scripts/virtual-office/code-runner-daemon.mjs
1623
+ function log(msg) {
1624
+ console.log(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
1625
+ }
1626
+ var RATE_LIMIT_RESUME_ENABLED = process.env.VO_RATE_LIMIT_RESUME === "1";
1627
+ var parseList = (s) => String(s || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
1628
+ function loadConfig(env2 = process.env) {
1629
+ return {
1630
+ runnerId: env2.VO_CODE_RUNNER_ID || `vo-code-runner-${os.hostname()}`,
1631
+ claudeBin: env2.VO_CODE_RUNNER_CLAUDE_BIN || "claude",
1632
+ permissionMode: env2.VO_CODE_RUNNER_PERMISSION_MODE || "acceptEdits",
1633
+ maxConcurrency: Math.max(1, Number(env2.VO_CODE_TASK_MAX_CONCURRENCY || 2) || 2),
1634
+ pollSec: Math.max(1, Number(env2.VO_CODE_RUNNER_POLL_SEC || 5) || 5),
1635
+ // Repos this daemon may BUILD + operators it serves (`owner/name` repos /
1636
+ // operator_ids; comma/space/newline separated). Sent on every claim so the
1637
+ // control-plane only hands this machine its own work — another operator's
1638
+ // task can never land here. Both UNSET ⇒ claims any pending task (legacy).
1639
+ servedRepos: parseList(env2.VO_CODE_RUNNER_REPOS),
1640
+ servedOperators: parseList(env2.VO_CODE_RUNNER_OPERATOR_IDS),
1641
+ // 'Sees ALL agents': how often to forward the local session spool to the
1642
+ // cloud (best-effort). Default 30s. Set 0 to disable forwarding.
1643
+ sessionForwardSec: Math.max(0, Number(env2.VO_SESSION_FORWARD_SEC ?? 30) || 0),
1644
+ operatorSeed: env2.VO_LOCAL_OPERATOR_SEED || env2.VO_CODE_RUNNER_ID || `local-${os.hostname()}`,
1645
+ cancelPollMs: Math.max(1e3, Number(env2.VO_CODE_RUNNER_CANCEL_POLL_MS || 2500) || 2500),
1646
+ // HARD enforced spend bound (max_budget_usd can only be checked post-hoc).
1647
+ // Default 30 min; set 0 to disable.
1648
+ maxWallClockMs: Math.max(0, Number(env2.VO_CODE_RUNNER_MAX_WALL_CLOCK_MS ?? 18e5) || 18e5),
1649
+ // Active PR watcher: monitor each dispatched PR's CI + auto-dispatch ONE fix
1650
+ // on failure (never auto-merges). Off: VO_CODE_RUNNER_WATCH=0; cap/interval below.
1651
+ watchEnabled: env2.VO_CODE_RUNNER_WATCH !== "0",
1652
+ watchMaxFix: Math.max(0, Number(env2.VO_CODE_RUNNER_WATCH_MAX_FIX ?? 1) || 0),
1653
+ watchIntervalSec: Math.max(30, Number(env2.VO_CODE_RUNNER_WATCH_SEC ?? 60) || 60),
1654
+ // In-product runner control (Phase 8.4): localhost-only status + Stop surface
1655
+ // for /virtualoffice. Off: VO_CODE_RUNNER_CONTROL=0. appOrigin = CORS allow.
1656
+ controlEnabled: env2.VO_CODE_RUNNER_CONTROL !== "0",
1657
+ controlPort: Math.max(1, Number(env2.VO_CODE_RUNNER_CONTROL_PORT ?? 7787) || 7787),
1658
+ appOrigin: env2.VO_APP_ORIGIN || "https://algosuite.ai"
1659
+ };
1660
+ }
1661
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1662
+ var numOrUndef = (x) => typeof x === "number" ? x : void 0;
1663
+ async function safeProgress(client, id, patch) {
1664
+ try {
1665
+ const r = await client.postProgress(id, patch);
1666
+ if (r && r.terminal) log(`task ${id} is terminal server-side; stopping updates`);
1667
+ return r;
1668
+ } catch (err) {
1669
+ log(`progress post failed for ${id}: ${err.message}`);
1670
+ return null;
1671
+ }
1672
+ }
1673
+ function buildPrBody(task, run2, files) {
1674
+ return [
1675
+ "## VO Command Center \u2014 Code-from-Anywhere task",
1676
+ "",
1677
+ `- **Task:** \`${task.code_task_id}\``,
1678
+ `- **Operator:** ${task.operator_id}`,
1679
+ `- **Repo:** ${task.repo}`,
1680
+ typeof run2.costUsd === "number" ? `- **Agent cost:** $${run2.costUsd.toFixed(4)}` : "- **Agent cost:** n/a",
1681
+ typeof run2.numTurns === "number" ? `- **Turns:** ${run2.numTurns}` : "",
1682
+ `- **Files changed:** ${files.length}`,
1683
+ "",
1684
+ "### Prompt",
1685
+ "",
1686
+ "```",
1687
+ String(task.prompt).slice(0, 2e3),
1688
+ "```",
1689
+ "",
1690
+ "### Agent summary",
1691
+ "",
1692
+ String(run2.summary || "").slice(0, 2e3),
1693
+ "",
1694
+ "---",
1695
+ "_Opened by the VO code-runner daemon. This PR awaits the verify-before-act gate / operator review \u2014 it is NOT auto-merged._"
1696
+ ].filter((l) => l !== "").join("\n");
1697
+ }
1698
+ async function processOneTask(client, task, cfg) {
1699
+ const id = task.code_task_id;
1700
+ let worktreeName = "";
1701
+ try {
1702
+ const wt = createFixWorktree("code-task", { source: id.slice(0, 8) });
1703
+ worktreeName = wt.worktreeName;
1704
+ 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})` });
1706
+ const cap = typeof task.max_budget_usd === "number" ? task.max_budget_usd : resolveSpendCapUsd();
1707
+ const run2 = await runClaudeTask({
1708
+ prompt: effortPrompt,
1709
+ cwd: wt.worktreeDir,
1710
+ claudeBin: cfg.claudeBin,
1711
+ permissionMode: effectivePermissionMode,
1712
+ maxTurns: effectiveMaxTurns,
1713
+ model,
1714
+ env: process.env,
1715
+ onProgress: (text) => {
1716
+ void safeProgress(client, id, { message: text });
1717
+ },
1718
+ shouldCancel: async () => {
1719
+ const t = await client.getTask(id).catch(() => null);
1720
+ return Boolean(t && t.status === "cancelled");
1721
+ },
1722
+ cancelPollMs: cfg.cancelPollMs,
1723
+ maxWallClockMs: cfg.maxWallClockMs
1724
+ });
1725
+ if (run2.killed) {
1726
+ log(`task ${id} cancelled by operator`);
1727
+ return;
1728
+ }
1729
+ if (run2.timedOut) {
1730
+ await safeProgress(client, id, {
1731
+ status: "failed",
1732
+ message: run2.summary,
1733
+ result: "wall_clock_timeout",
1734
+ cost_usd: numOrUndef(run2.costUsd)
1735
+ });
1736
+ return;
1737
+ }
1738
+ if (typeof task.max_turns === "number" && typeof run2.numTurns === "number" && run2.numTurns > task.max_turns) {
1739
+ log(`task ${id} WARNING: agent ran ${run2.numTurns} turns > max_turns ${task.max_turns}`);
1740
+ }
1741
+ if (typeof run2.costUsd === "number" && cap > 0 && run2.costUsd > cap) {
1742
+ log(
1743
+ `task ${id}: usage ~$${run2.costUsd.toFixed(2)} (est, API-equivalent \u2014 not billed on a subscription) exceeded soft cap $${cap}; publishing the agent's work anyway`
1744
+ );
1745
+ }
1746
+ if (!run2.ok) {
1747
+ const v = classifyFailureForResume({ enabled: RATE_LIMIT_RESUME_ENABLED, run: run2, task });
1748
+ if (v.rateLimited) log(`task ${id}: RATE_LIMITED (resumeAfter=${v.resumeAfter || "backoff"}); queued (${v.recorded ? "ok" : "queue-write-failed"})`);
1749
+ await safeProgress(client, id, { ...v.progress, cost_usd: numOrUndef(run2.costUsd) });
1750
+ return;
1751
+ }
1752
+ let files = listChangedFiles(wt.worktreeDir);
1753
+ let alreadyCommitted = false;
1754
+ if (files.length === 0) {
1755
+ const committed = listCommittedFiles(wt.worktreeDir);
1756
+ if (committed.length > 0) {
1757
+ files = committed;
1758
+ alreadyCommitted = true;
1759
+ log(`task ${id}: agent committed ${committed.length} file(s) to a branch; recovering`);
1760
+ }
1761
+ }
1762
+ const scratch = files.filter(isAgentScratch);
1763
+ if (scratch.length > 0) {
1764
+ files = files.filter((f) => !isAgentScratch(f));
1765
+ log(`task ${id}: dropped ${scratch.length} scratch file(s): ${scratch.join(", ")}`);
1766
+ }
1767
+ if (files.length === 0) {
1768
+ await safeProgress(client, id, {
1769
+ status: "failed",
1770
+ message: "agent made no file changes",
1771
+ result: "no_changes",
1772
+ cost_usd: numOrUndef(run2.costUsd)
1773
+ });
1774
+ return;
1775
+ }
1776
+ const fresh = await client.getTask(id).catch(() => null);
1777
+ if (fresh && fresh.status === "cancelled") {
1778
+ log(`task ${id} cancelled before PR open; discarding changes`);
1779
+ return;
1780
+ }
1781
+ await safeProgress(client, id, { message: `opening PR for ${files.length} changed file(s)` });
1782
+ const pr = openCodeTaskPr(wt.worktreeDir, files, {
1783
+ title: `code-task: ${task.prompt}`,
1784
+ body: buildPrBody(task, run2, files),
1785
+ alreadyCommitted
1786
+ });
1787
+ await safeProgress(client, id, {
1788
+ status: "pr_opened",
1789
+ message: `opened ${pr.prUrl}`,
1790
+ pr_url: pr.prUrl,
1791
+ pr_number: pr.prNumber,
1792
+ result: String(run2.summary).slice(0, 2e3),
1793
+ cost_usd: numOrUndef(run2.costUsd)
1794
+ });
1795
+ log(`task ${id} \u2192 PR ${pr.prUrl}`);
1796
+ if (cfg.watchEnabled && !String(task.prompt || "").includes(CI_FIX_MARKER)) {
1797
+ await trackDispatchedPr({
1798
+ prNumber: pr.prNumber,
1799
+ repo: task.repo,
1800
+ branch: pr.branch,
1801
+ taskId: id
1802
+ }).catch((e) => log(`watch: track failed for #${pr.prNumber}: ${e.message}`));
1803
+ }
1804
+ } catch (err) {
1805
+ const msg = err && err.message ? err.message : String(err);
1806
+ log(`task ${id} error: ${msg}`);
1807
+ await safeProgress(client, id, {
1808
+ status: "failed",
1809
+ message: `runner error: ${msg}`.slice(0, 1500),
1810
+ result: msg.slice(0, 2e3)
1811
+ }).catch(() => {
1812
+ });
1813
+ } finally {
1814
+ if (worktreeName) cleanupFixWorktree(worktreeName);
1815
+ }
1816
+ }
1817
+ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
1818
+ const cfg = loadConfig(env2);
1819
+ const client = createControlPlaneClient({ env: env2 });
1820
+ let stopping = false;
1821
+ let active = 0;
1822
+ const stop = (sig) => {
1823
+ if (stopping) return;
1824
+ stopping = true;
1825
+ log(`${sig} received \u2014 draining ${active} active task(s), no new claims`);
1826
+ };
1827
+ process.on("SIGINT", () => stop("SIGINT"));
1828
+ process.on("SIGTERM", () => stop("SIGTERM"));
1829
+ const startedAt = Date.now();
1830
+ const controlServer = startDaemonControl({
1831
+ cfg,
1832
+ requestStop: () => stop("web-control"),
1833
+ getActiveCount: () => active,
1834
+ isRunning: () => !stopping,
1835
+ startedAt,
1836
+ log
1837
+ });
1838
+ log(
1839
+ `up as ${cfg.runnerId} \u2192 ${env2.VO_CONTROL_PLANE_URL} (concurrency ${cfg.maxConcurrency}, poll ${cfg.pollSec}s, once=${once2})`
1840
+ );
1841
+ for (const line of describeClaimScoping(cfg, env2)) log(line);
1842
+ log(
1843
+ 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
+ );
1845
+ let lastSessionForward = 0;
1846
+ let lastWatchCycle = 0;
1847
+ const runWatch = makeWatchRunner({ client, log, maxFixAttempts: cfg.watchMaxFix });
1848
+ while (!stopping) {
1849
+ if (cfg.sessionForwardSec > 0 && Date.now() - lastSessionForward >= cfg.sessionForwardSec * 1e3) {
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
+ }
1858
+ if (cfg.watchEnabled && Date.now() - lastWatchCycle >= cfg.watchIntervalSec * 1e3) {
1859
+ lastWatchCycle = Date.now();
1860
+ runWatch().then((r) => {
1861
+ if (r.checked > 0) log(`watch: ${r.checked} PR(s) checked, ${r.fixed} fix(es), ${r.untracked} untracked`);
1862
+ }).catch((e) => log(`watch cycle error: ${e.message}`));
1863
+ }
1864
+ if (active >= cfg.maxConcurrency) {
1865
+ await sleep(cfg.pollSec * 1e3);
1866
+ continue;
1867
+ }
1868
+ let task;
1869
+ try {
1870
+ task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators);
1871
+ } catch (err) {
1872
+ log(`claim error: ${err.message}`);
1873
+ if (once2) break;
1874
+ await sleep(cfg.pollSec * 1e3);
1875
+ continue;
1876
+ }
1877
+ if (!task) {
1878
+ if (once2) {
1879
+ log("no pending task; --once exiting");
1880
+ break;
1881
+ }
1882
+ await sleep(cfg.pollSec * 1e3);
1883
+ continue;
1884
+ }
1885
+ log(`claimed task ${task.code_task_id} (${task.repo})`);
1886
+ active += 1;
1887
+ const done = processOneTask(client, task, cfg).finally(() => {
1888
+ active -= 1;
1889
+ });
1890
+ if (once2) {
1891
+ await done;
1892
+ break;
1893
+ }
1894
+ }
1895
+ while (active > 0) {
1896
+ await sleep(500);
1897
+ }
1898
+ if (controlServer) controlServer.close();
1899
+ log("stopped");
1900
+ }
1901
+ 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
1902
+ // module is inlined into a bundle (e.g. @algosuite/vo-mcp's dist/runner-cli.js,
1903
+ // which calls main() itself — without this, `node dist/runner-cli.js` would
1904
+ // start a SECOND daemon loop and double-claim tasks).
1905
+ import.meta.url.endsWith("code-runner-daemon.mjs");
1906
+ if (invokedDirectly) {
1907
+ const once2 = process.argv.includes("--once");
1908
+ main({ once: once2 }).catch((err) => {
1909
+ console.error("[code-runner] fatal:", err);
1910
+ process.exit(1);
1911
+ });
1912
+ }
1913
+
1914
+ // src/runner-cli.mjs
1915
+ var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
1916
+ function resolveToken() {
1917
+ if (process.env.VO_CONTROL_PLANE_ADMIN_TOKEN) {
1918
+ return process.env.VO_CONTROL_PLANE_ADMIN_TOKEN;
1919
+ }
1920
+ const cred = readStoredCredential();
1921
+ return cred && cred.vo_credential ? cred.vo_credential : void 0;
1922
+ }
1923
+ var token = resolveToken();
1924
+ if (!token) {
1925
+ console.error("[vo-mcp runner] No credential found. Run `vo-mcp login` first.");
1926
+ console.error(" (or set VO_CONTROL_PLANE_ADMIN_TOKEN to a control-plane bearer)");
1927
+ process.exit(1);
1928
+ }
1929
+ var env = {
1930
+ ...process.env,
1931
+ VO_CONTROL_PLANE_ADMIN_TOKEN: token,
1932
+ VO_CONTROL_PLANE_URL: process.env.VO_CONTROL_PLANE_URL || DEFAULT_CONTROL_PLANE_URL,
1933
+ VO_CODE_RUNNER_REPO: process.env.VO_CODE_RUNNER_REPO || process.cwd()
1934
+ };
1935
+ var once = process.argv.includes("--once");
1936
+ main({ env, once }).catch((err) => {
1937
+ console.error("[vo-mcp runner] fatal:", err);
1938
+ process.exit(1);
1939
+ });
1940
+ //# sourceMappingURL=runner-cli.js.map