@algosuite/vo-mcp 0.2.0-beta.55 → 0.2.0-beta.57

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.
@@ -675,12 +675,15 @@ function installWindowsAutostart(runnerCommand, log2, env2) {
675
675
  log2(`\u26A0 Could not remove legacy launcher ${legacyCmdPath} (${error instanceof Error ? error.message : String(error)}); remove it manually to avoid a double start`);
676
676
  }
677
677
  }
678
- const hiddenCommand = `cmd /c ${runnerCommand} >> "%USERPROFILE%\\.claude\\vo-runner.log" 2>&1`.replace(/"/g, '""');
678
+ const runnerConsoleCommand = `cmd /c ${runnerCommand} >> "%USERPROFILE%\\.claude\\vo-runner.log" 2>&1`.replace(/"/g, '""');
679
679
  const launcherContent = `' Auto-start launcher for vo-mcp runner
680
680
  ' Created by vo-mcp autostart installer
681
681
  ' Keepalive supervisor: restarts the runner if it exits (parity with launchd
682
682
  ' KeepAlive on macOS and systemd Restart=on-failure on Linux).
683
- ' To stop: create %USERPROFILE%\\.claude\\vo-runner.stop, or end wscript.exe.
683
+ ' Runs the runner in a MINIMIZED (style 7), never a hidden (style 0) window:
684
+ ' hidden script-host exec trips Defender's PowhidSubExec.B heuristic and gets
685
+ ' blocked at logon. To stop: create %USERPROFILE%\\.claude\\vo-runner.stop, or
686
+ ' end wscript.exe.
684
687
  Dim sh, fso, stopFile, backoff, startedAt, ranMs
685
688
  Set sh = CreateObject("WScript.Shell")
686
689
  Set fso = CreateObject("Scripting.FileSystemObject")
@@ -694,7 +697,7 @@ Do
694
697
  WScript.Quit 0
695
698
  End If
696
699
  startedAt = Timer
697
- sh.Run "${hiddenCommand}", 0, True
700
+ sh.Run "${runnerConsoleCommand}", 7, True
698
701
  ranMs = (Timer - startedAt) * 1000
699
702
  If ranMs < 0 Then ranMs = ${WINDOWS_HEALTHY_RUN_MS}
700
703
  If ranMs >= ${WINDOWS_HEALTHY_RUN_MS} Then
@@ -1178,8 +1181,8 @@ async function walkManagedRoots(ownership, options, onLink) {
1178
1181
  const normalized = normalizeOwnership(ownership);
1179
1182
  let scannedEntries = 0;
1180
1183
  for (const rootPath of normalized.managedRoots) {
1181
- const present = await assertManagedRoot(rootPath, normalized.worktreeRoot, fsApi);
1182
- if (!present) continue;
1184
+ const present2 = await assertManagedRoot(rootPath, normalized.worktreeRoot, fsApi);
1185
+ if (!present2) continue;
1183
1186
  const stack = [rootPath];
1184
1187
  while (stack.length > 0) {
1185
1188
  const current = stack.pop();
@@ -1999,12 +2002,12 @@ var init_pnpm_materialize = __esm({
1999
2002
  });
2000
2003
 
2001
2004
  // src/runner/pnpm-hydration.mjs
2002
- import { createHash } from "node:crypto";
2005
+ import { createHash as createHash2 } from "node:crypto";
2003
2006
  import fs3 from "node:fs";
2004
2007
  import fsp5 from "node:fs/promises";
2005
2008
  import path5 from "node:path";
2006
2009
  function hashText(text) {
2007
- return createHash("sha256").update(String(text)).digest("hex");
2010
+ return createHash2("sha256").update(String(text)).digest("hex");
2008
2011
  }
2009
2012
  function statePath(root) {
2010
2013
  return path5.join(root, ".agent-worktrees", "runner-pnpm-hydration.json");
@@ -2314,7 +2317,7 @@ var init_pnpm_hydration = __esm({
2314
2317
  });
2315
2318
 
2316
2319
  // src/runner/worktree-paths.mjs
2317
- import { createHash as createHash2 } from "node:crypto";
2320
+ import { createHash as createHash3 } from "node:crypto";
2318
2321
  import path6 from "node:path";
2319
2322
  function samePath2(left, right) {
2320
2323
  const a = path6.resolve(left);
@@ -2332,7 +2335,7 @@ function worktreePoolForRoot(root, { clonesRootDir = process.env.VO_CODE_RUNNER_
2332
2335
  return path6.join(canonicalRoot, ".agent-worktrees");
2333
2336
  }
2334
2337
  function worktreeDirForName(root, worktreeName, options = {}) {
2335
- const leaf = createHash2("sha256").update(String(worktreeName)).digest("hex").slice(0, 16);
2338
+ const leaf = createHash3("sha256").update(String(worktreeName)).digest("hex").slice(0, 16);
2336
2339
  return path6.join(worktreePoolForRoot(root, options), leaf);
2337
2340
  }
2338
2341
  function recoveryLedgerPathForRoot(root, options = {}) {
@@ -3497,8 +3500,9 @@ async function mergeVerifiedPrRequest(req, prNumber, automationContext, onUnauth
3497
3500
  actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
3498
3501
  };
3499
3502
  }
3500
- if (res.status === 503 && json?.error === "verify_unavailable") {
3501
- return { status: "retry", reason: json.reason || "verification unavailable" };
3503
+ const captureStoreRetry = json?.action_status === "not_attempted" && (json?.error === "capture_preflight_unavailable" || json?.error === "decision_outcome_intent_failed");
3504
+ if (res.status === 503 && (json?.error === "verify_unavailable" || json?.error === "merge_unavailable" || captureStoreRetry)) {
3505
+ return { status: "retry", reason: json.reason || json.message || json.error || "verification unavailable" };
3502
3506
  }
3503
3507
  return {
3504
3508
  status: "blocked",
@@ -3604,6 +3608,64 @@ var init_claim_gate_notice = __esm({
3604
3608
  }
3605
3609
  });
3606
3610
 
3611
+ // ../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs
3612
+ function isRetryableStatus(status) {
3613
+ return status >= 500 && status <= 599;
3614
+ }
3615
+ function defaultSleep(ms) {
3616
+ return new Promise((resolve3) => {
3617
+ setTimeout(resolve3, ms);
3618
+ });
3619
+ }
3620
+ async function getTaskKnowledgeContextRequest(req, taskId, { query } = {}, {
3621
+ taskRequestTimeoutMs,
3622
+ invalidateToken = () => {
3623
+ },
3624
+ sleep: sleep3 = defaultSleep,
3625
+ log: log2 = () => {
3626
+ }
3627
+ } = {}) {
3628
+ const body = {};
3629
+ if (typeof query === "string" && query.trim()) body.query = query;
3630
+ const path22 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
3631
+ const timeoutMs = Math.max(Number(taskRequestTimeoutMs) || 0, MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS);
3632
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
3633
+ let res;
3634
+ let cause;
3635
+ try {
3636
+ res = await req("POST", path22, body, { timeoutMs });
3637
+ } catch (err) {
3638
+ cause = err;
3639
+ }
3640
+ if (!cause) {
3641
+ if (res.status === 401) {
3642
+ invalidateToken();
3643
+ throw new Error("knowledge-context unauthorized (401)");
3644
+ }
3645
+ if (res.status === 404) return null;
3646
+ if (res.ok) return res.json();
3647
+ if (!isRetryableStatus(res.status)) {
3648
+ throw new Error(`knowledge-context failed: HTTP ${res.status}`);
3649
+ }
3650
+ cause = new Error(`knowledge-context failed: HTTP ${res.status}`);
3651
+ }
3652
+ if (attempt === MAX_ATTEMPTS) throw cause;
3653
+ const delayMs = RETRY_DELAYS_MS[attempt - 1];
3654
+ log2(`knowledge-context attempt ${attempt}/${MAX_ATTEMPTS} failed (${cause.message}); retrying in ${delayMs}ms`);
3655
+ await sleep3(delayMs);
3656
+ }
3657
+ throw new Error("knowledge-context retry loop exited unexpectedly");
3658
+ }
3659
+ var MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS, RETRY_DELAYS_MS, MAX_ATTEMPTS;
3660
+ var init_control_plane_knowledge_context = __esm({
3661
+ "../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs"() {
3662
+ "use strict";
3663
+ MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS = 15e3;
3664
+ RETRY_DELAYS_MS = [2e3, 6e3];
3665
+ MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1;
3666
+ }
3667
+ });
3668
+
3607
3669
  // src/runner/control-plane-auth-stub.mjs
3608
3670
  var control_plane_auth_stub_exports = {};
3609
3671
  __export(control_plane_auth_stub_exports, {
@@ -3647,7 +3709,8 @@ function createControlPlaneClient({
3647
3709
  6e4
3648
3710
  ),
3649
3711
  runnerId,
3650
- runnerInstanceId
3712
+ runnerInstanceId,
3713
+ sleep: sleep3
3651
3714
  } = {}) {
3652
3715
  const resolvedBaseUrl = baseUrl ?? env2.VO_CONTROL_PLANE_URL ?? "";
3653
3716
  if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
@@ -3828,17 +3891,16 @@ function createControlPlaneClient({
3828
3891
  if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
3829
3892
  return Buffer.from(await res.arrayBuffer());
3830
3893
  },
3894
+ // Raised per-attempt timeout (>=15s) + bounded retry — see control-plane-knowledge-context.mjs.
3831
3895
  async getTaskKnowledgeContext(taskId, { query } = {}) {
3832
- const body = {};
3833
- if (typeof query === "string" && query.trim()) body.query = query;
3834
- const res = await taskReq("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`, body);
3835
- if (res.status === 401) {
3836
- cachedFirebaseToken = null;
3837
- throw new Error("knowledge-context unauthorized (401)");
3838
- }
3839
- if (res.status === 404) return null;
3840
- if (!res.ok) throw new Error(`knowledge-context failed: HTTP ${res.status}`);
3841
- return res.json();
3896
+ return getTaskKnowledgeContextRequest(req, taskId, { query }, {
3897
+ taskRequestTimeoutMs,
3898
+ invalidateToken: () => {
3899
+ cachedFirebaseToken = null;
3900
+ },
3901
+ sleep: sleep3,
3902
+ log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`)
3903
+ });
3842
3904
  },
3843
3905
  /** Weekly Claude token usage report — see control-plane-weekly-tokens.mjs. */
3844
3906
  async postWeeklyTokens(report) {
@@ -3992,6 +4054,7 @@ var init_control_plane_client = __esm({
3992
4054
  init_control_plane_weekly_tokens();
3993
4055
  init_control_plane_telemetry_relay();
3994
4056
  init_claim_gate_notice();
4057
+ init_control_plane_knowledge_context();
3995
4058
  cachedFirebaseToken = null;
3996
4059
  ClaimAuthorityChangedError = class extends Error {
3997
4060
  constructor() {
@@ -8606,7 +8669,7 @@ var init_task_prompt = __esm({
8606
8669
  });
8607
8670
 
8608
8671
  // ../../scripts/virtual-office/code-runner/task-attachments.mjs
8609
- import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
8672
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
8610
8673
  import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8611
8674
  import os2 from "node:os";
8612
8675
  import path16 from "node:path";
@@ -8630,7 +8693,7 @@ async function createAttachmentDirectory(taskId, tempRoot) {
8630
8693
  const root = path16.resolve(tempRoot);
8631
8694
  await mkdir(root, { recursive: true });
8632
8695
  const directory = await mkdtemp(path16.join(root, `${DIRECTORY_PREFIX}${safeTaskToken(taskId)}-`));
8633
- const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID2(), directory: path16.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
8696
+ const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID3(), directory: path16.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
8634
8697
  await writeFile(path16.join(directory, MARKER_FILE), marker, { encoding: "utf8", mode: 384 });
8635
8698
  return { directory, marker, tempRoot: root };
8636
8699
  }
@@ -8706,7 +8769,7 @@ async function materializeTaskAttachments(client, task, { tempRoot = os2.tmpdir(
8706
8769
  const content = await client.downloadTaskAttachment(task.code_task_id, ref.attachment_id);
8707
8770
  if (!Buffer.isBuffer(content)) throw new Error(`attachment ${ref.attachment_id} did not return binary content`);
8708
8771
  if (content.byteLength !== ref.size_bytes) throw new Error(`attachment ${ref.attachment_id} size mismatch`);
8709
- const sha256 = createHash3("sha256").update(content).digest("hex");
8772
+ const sha256 = createHash4("sha256").update(content).digest("hex");
8710
8773
  if (sha256 !== ref.sha256) throw new Error(`attachment ${ref.attachment_id} sha256 mismatch`);
8711
8774
  const name = sanitizeTaskAttachmentName(ref.name, index);
8712
8775
  const filePath = path16.join(state.directory, name);
@@ -8737,9 +8800,9 @@ var init_task_attachments = __esm({
8737
8800
  import { homedir as homedir7 } from "node:os";
8738
8801
  import { join as join11 } from "node:path";
8739
8802
  import { readdir as readdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
8740
- import { createHash as createHash4 } from "node:crypto";
8803
+ import { createHash as createHash5 } from "node:crypto";
8741
8804
  function deriveUuid(seed) {
8742
- const h = createHash4("sha256").update(seed).digest("hex");
8805
+ const h = createHash5("sha256").update(seed).digest("hex");
8743
8806
  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)}`;
8744
8807
  }
8745
8808
  function spoolToCloud(record, ids) {
@@ -8876,7 +8939,7 @@ function selectDueEntries({ entries = [], now, alreadyDispatched = /* @__PURE__
8876
8939
  if (!code_task_id) continue;
8877
8940
  if (seen.has(code_task_id)) continue;
8878
8941
  const stableAttempts = Number(attemptsByKey[stableTaskKey(e)] || 0);
8879
- if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >= MAX_ATTEMPTS) {
8942
+ if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >= MAX_ATTEMPTS2) {
8880
8943
  exhausted.push(e);
8881
8944
  continue;
8882
8945
  }
@@ -8912,11 +8975,11 @@ function reconcileQueue({ entries = [], dispatchedIds = /* @__PURE__ */ new Set(
8912
8975
  return true;
8913
8976
  });
8914
8977
  }
8915
- var MAX_ATTEMPTS, MAX_DISPATCH_PER_RUN, NULL_RESUME_AFTER_BACKOFF_MS;
8978
+ var MAX_ATTEMPTS2, MAX_DISPATCH_PER_RUN, NULL_RESUME_AFTER_BACKOFF_MS;
8916
8979
  var init_rate_limit_resume_scheduler_core = __esm({
8917
8980
  "../../scripts/virtual-office/code-runner/rate-limit-resume-scheduler-core.mjs"() {
8918
8981
  "use strict";
8919
- MAX_ATTEMPTS = 3;
8982
+ MAX_ATTEMPTS2 = 3;
8920
8983
  MAX_DISPATCH_PER_RUN = 10;
8921
8984
  NULL_RESUME_AFTER_BACKOFF_MS = 15 * 60 * 1e3;
8922
8985
  }
@@ -8943,7 +9006,7 @@ function bumpAttempts(store, key, sourceTaskId, nowIso) {
8943
9006
  store[key] = {
8944
9007
  count: (typeof prior.count === "number" ? prior.count : 0) + 1,
8945
9008
  lastSeen: nowIso,
8946
- resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-MAX_ATTEMPTS)
9009
+ resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-MAX_ATTEMPTS2)
8947
9010
  };
8948
9011
  }
8949
9012
  function pruneAttemptsStore(store, nowIso) {
@@ -8995,7 +9058,7 @@ async function runLockedScheduler({
8995
9058
  await writeResumeAttempts(attemptsPath, pruneAttemptsStore(attemptsStore, nowIso));
8996
9059
  await writeResumeQueue(queuePath, kept);
8997
9060
  if (exhausted.length > 0) {
8998
- log2(`WARN: ${exhausted.length} task(s) reached the ${MAX_ATTEMPTS}-continuation spend ceiling and remain queued for operator review: ${exhausted.map((entry) => entry.code_task_id).join(", ")}`);
9061
+ log2(`WARN: ${exhausted.length} task(s) reached the ${MAX_ATTEMPTS2}-continuation spend ceiling and remain queued for operator review: ${exhausted.map((entry) => entry.code_task_id).join(", ")}`);
8999
9062
  }
9000
9063
  if (due.length >= MAX_DISPATCH_PER_RUN) {
9001
9064
  log2(`WARN: hit the per-run dispatch cap (${MAX_DISPATCH_PER_RUN}); more tasks remain queued`);
@@ -10446,39 +10509,6 @@ var init_pr_watcher_failure_confirmation = __esm({
10446
10509
  }
10447
10510
  });
10448
10511
 
10449
- // ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
10450
- function supersededSourcePrNumber(prompt) {
10451
- const text = String(prompt || "");
10452
- if (text.includes(CI_FIX_MARKER)) {
10453
- const match = text.match(/\bPR:\s*#(\d+)\b/u);
10454
- return match ? Number(match[1]) : null;
10455
- }
10456
- const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
10457
- if (repairMatch) return Number(repairMatch[1]);
10458
- const recoverySupersedeMatch = text.match(
10459
- /^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
10460
- );
10461
- if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
10462
- const restoredContextMatch = text.match(
10463
- /^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nThe HQ runner will restore the existing draft PR context before you start \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+), PR #(\d+)\)\. Continue that work and finish it\./u
10464
- );
10465
- if (restoredContextMatch) {
10466
- return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
10467
- }
10468
- const continuationMatch = text.match(
10469
- /^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nContinue the work already started on the branch for PR #(\d+) \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)\); check it out and finish it\./u
10470
- );
10471
- if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
10472
- return Number(continuationMatch[1]);
10473
- }
10474
- var CI_FIX_MARKER;
10475
- var init_superseded_pr_source = __esm({
10476
- "../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
10477
- "use strict";
10478
- CI_FIX_MARKER = "[VO-CI-FIX]";
10479
- }
10480
- });
10481
-
10482
10512
  // ../../scripts/virtual-office/code-runner/error-message.mjs
10483
10513
  function boundedErrorMessage(error, maxLength = 240) {
10484
10514
  try {
@@ -10494,7 +10524,7 @@ var init_error_message = __esm({
10494
10524
  });
10495
10525
 
10496
10526
  // ../../scripts/virtual-office/code-runner/watcher-coordination.mjs
10497
- import { createHash as createHash5 } from "node:crypto";
10527
+ import { createHash as createHash6 } from "node:crypto";
10498
10528
  function ciFixOccurrenceKey({ repo, prNumber, headSha, repairAttempt }) {
10499
10529
  const occurrence = JSON.stringify([
10500
10530
  String(repo).toLowerCase(),
@@ -10502,7 +10532,7 @@ function ciFixOccurrenceKey({ repo, prNumber, headSha, repairAttempt }) {
10502
10532
  String(headSha).toLowerCase(),
10503
10533
  Number(repairAttempt)
10504
10534
  ]);
10505
- return `ci-fix:v1:${createHash5("sha256").update(occurrence).digest("hex")}`;
10535
+ return `ci-fix:v1:${createHash6("sha256").update(occurrence).digest("hex")}`;
10506
10536
  }
10507
10537
  function coordinationRetryDue(entry, nowMs) {
10508
10538
  return !entry.nextRetryAt || nowMs >= entry.nextRetryAt;
@@ -10513,9 +10543,52 @@ function mergeEnqueueActive(entry, nowMs = Date.now()) {
10513
10543
  if (!at) return true;
10514
10544
  return nowMs - at < MERGE_ENQUEUE_TTL_MS;
10515
10545
  }
10546
+ function migrateLegacyMergeState(entry, nowMs = Date.now()) {
10547
+ if (!entry) return false;
10548
+ if (entry.mergeEnqueued === true && !(Number(entry.mergeEnqueuedAt) > 0)) {
10549
+ entry.mergeEnqueuedAt = nowMs;
10550
+ return true;
10551
+ }
10552
+ const exhausted = entry.mergeRetryExhausted === true || (Number(entry.mergeAttempts) || 0) >= 3 || (Number(entry.mergeErrors) || 0) >= 3;
10553
+ if (entry.mergeTerminal === true || mergeEnqueueActive(entry, nowMs) || !exhausted) return false;
10554
+ entry.mergeRetryExhausted = true;
10555
+ entry.mergeTerminal = true;
10556
+ entry.mergeBlockReason ||= "Legacy watcher state exhausted 3 automatic merge attempts without a terminal result.";
10557
+ return true;
10558
+ }
10516
10559
  function isTerminalResumeRefusal(error) {
10517
10560
  return typeof error?.code === "string" && TERMINAL_RESUME_REFUSALS.includes(error.code);
10518
10561
  }
10562
+ async function reportPendingMergeExhaustion({
10563
+ entry,
10564
+ now,
10565
+ reportBlocker,
10566
+ taskId,
10567
+ prNumber,
10568
+ log: log2 = () => {
10569
+ }
10570
+ }) {
10571
+ if (!entry.mergeRetryExhausted || entry.mergeEscalatedAt || typeof reportBlocker !== "function") return false;
10572
+ const lastError = boundedErrorMessage(entry.mergeRetryReason || entry.mergeBlockReason);
10573
+ try {
10574
+ await reportBlocker({
10575
+ taskId,
10576
+ message: `PR #${prNumber} automatic merge verification exhausted its bounded retry budget; operator review is required. Last error: ${lastError}`
10577
+ });
10578
+ entry.mergeEscalatedAt = now();
10579
+ return true;
10580
+ } catch (reportError) {
10581
+ log2(`watch: pr #${prNumber} merge escalation failed; terminal state retained: ${boundedErrorMessage(reportError)}`);
10582
+ return false;
10583
+ }
10584
+ }
10585
+ async function scheduleMergeRetry(input) {
10586
+ await scheduleCoordinationRetry({ ...input, kind: "merge" });
10587
+ return {
10588
+ terminal: input.entry.mergeTerminal === true,
10589
+ escalated: await reportPendingMergeExhaustion(input)
10590
+ };
10591
+ }
10519
10592
  async function scheduleCoordinationRetry({
10520
10593
  entry,
10521
10594
  kind,
@@ -10534,10 +10607,21 @@ async function scheduleCoordinationRetry({
10534
10607
  log2(`watch: pr #${prNumber} automatic continuation refused by the plane (${error.code}); no retry \u2014 operator may resume explicitly with a larger cap`);
10535
10608
  return;
10536
10609
  }
10537
- const errorsKey = kind === "resume" ? "resumeErrors" : "enqueueErrors";
10538
- const attemptsKey = kind === "resume" ? "resumeAttempts" : "fixAttempts";
10610
+ const errorsKey = kind === "resume" ? "resumeErrors" : kind === "merge" ? "mergeErrors" : "enqueueErrors";
10611
+ const attemptsKey = kind === "resume" ? "resumeAttempts" : kind === "merge" ? "mergeAttempts" : "fixAttempts";
10539
10612
  entry[errorsKey] = (entry[errorsKey] || 0) + 1;
10540
- entry[attemptsKey] = Math.max(0, (entry[attemptsKey] || 1) - 1);
10613
+ if (kind === "merge") {
10614
+ entry.mergeRetryReason = boundedErrorMessage(error);
10615
+ if (entry[errorsKey] >= 3) {
10616
+ entry.mergeTerminal = true;
10617
+ entry.mergeRetryExhausted = true;
10618
+ entry.mergeBlockReason = `Automatic merge verification remained unavailable after 3 attempts: ${entry.mergeRetryReason}`;
10619
+ delete entry.nextRetryAt;
10620
+ log2(`watch: pr #${prNumber} merge retry cap reached \u2014 terminal and operator review required`);
10621
+ return;
10622
+ }
10623
+ }
10624
+ if (kind !== "merge") entry[attemptsKey] = Math.max(0, (entry[attemptsKey] || 1) - 1);
10541
10625
  const delay2 = Math.min(MAX_BACKOFF_MS2, 3e4 * 2 ** Math.min(7, entry[errorsKey] - 1));
10542
10626
  entry.nextRetryAt = now() + delay2;
10543
10627
  if (entry[errorsKey] >= 3 && !entry.coordinationEscalatedAt && typeof reportBlocker === "function") {
@@ -10578,7 +10662,7 @@ var init_watcher_coordination = __esm({
10578
10662
  });
10579
10663
 
10580
10664
  // ../../scripts/virtual-office/code-runner/watcher-state.mjs
10581
- import { randomUUID as randomUUID3 } from "node:crypto";
10665
+ import { randomUUID as randomUUID4 } from "node:crypto";
10582
10666
  import { mkdir as mkdir3, open as open2, readFile as readFile4, rename, unlink as unlink2 } from "node:fs/promises";
10583
10667
  import { dirname as dirname10 } from "node:path";
10584
10668
  async function readWatcherState(stateFile) {
@@ -10598,7 +10682,7 @@ async function readWatcherState(stateFile) {
10598
10682
  async function writeWatcherState(stateFile, state) {
10599
10683
  const directory = dirname10(stateFile);
10600
10684
  await mkdir3(directory, { recursive: true });
10601
- const temp = `${stateFile}.${process.pid}.${randomUUID3()}.tmp`;
10685
+ const temp = `${stateFile}.${process.pid}.${randomUUID4()}.tmp`;
10602
10686
  let handle;
10603
10687
  try {
10604
10688
  handle = await open2(temp, "wx");
@@ -10646,16 +10730,87 @@ var init_watcher_state = __esm({
10646
10730
  function normalizedRepo(repo) {
10647
10731
  return String(repo || "").trim().toLowerCase();
10648
10732
  }
10733
+ function present(value) {
10734
+ return value !== null && value !== void 0 && value !== "";
10735
+ }
10649
10736
  function watcherKey(state, repo, prNumber) {
10737
+ const targetRepo = normalizedRepo(repo);
10738
+ for (const [key, entry] of Object.entries(state)) {
10739
+ if (watcherPrNumber(key, entry) !== Number(prNumber)) continue;
10740
+ if (normalizedRepo(entry?.repo) === targetRepo) return key;
10741
+ }
10650
10742
  const legacy = String(prNumber);
10651
10743
  const existing = state[legacy];
10652
- if (!existing || normalizedRepo(existing.repo) === normalizedRepo(repo)) return legacy;
10653
- return `${normalizedRepo(repo)}#${prNumber}`;
10744
+ if (!existing) return legacy;
10745
+ return `${targetRepo}#${prNumber}`;
10654
10746
  }
10655
10747
  function watcherPrNumber(stateKey, entry) {
10656
10748
  const value = Number(entry?.prNumber ?? String(stateKey).split("#").at(-1));
10657
10749
  return Number.isInteger(value) && value > 0 ? value : null;
10658
10750
  }
10751
+ function collapseDuplicateWatcherEntries(state) {
10752
+ const keeperBySubject = /* @__PURE__ */ new Map();
10753
+ let collapsed = 0;
10754
+ for (const [key, entry] of Object.entries(state)) {
10755
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
10756
+ const repo = normalizedRepo(entry.repo);
10757
+ const prNumber = watcherPrNumber(key, entry);
10758
+ if (!repo || !prNumber) continue;
10759
+ const subject = `${repo}#${prNumber}`;
10760
+ const keeperKey = keeperBySubject.get(subject);
10761
+ if (!keeperKey) {
10762
+ keeperBySubject.set(subject, key);
10763
+ continue;
10764
+ }
10765
+ const keeper = state[keeperKey];
10766
+ let identityConflict = false;
10767
+ for (const field of ["taskId", "operatorId", "tenantId"]) {
10768
+ const current = keeper[field];
10769
+ const duplicate = entry[field];
10770
+ if (!present(current) && present(duplicate)) keeper[field] = duplicate;
10771
+ else if (present(current) && present(duplicate) && current !== duplicate) identityConflict = true;
10772
+ }
10773
+ if (keeper.operatorId === "admin" && present(keeper.tenantId)) identityConflict = true;
10774
+ for (const field of ["fixAttempts", "mergeAttempts", "mergeErrors", "resumeAttempts"]) {
10775
+ const current = Number(keeper[field]);
10776
+ const duplicate = Number(entry[field]);
10777
+ const total = (Number.isSafeInteger(current) && current > 0 ? current : 0) + (Number.isSafeInteger(duplicate) && duplicate > 0 ? duplicate : 0);
10778
+ keeper[field] = Math.min(Number.MAX_SAFE_INTEGER, total);
10779
+ }
10780
+ for (const [field, value] of Object.entries(entry)) {
10781
+ if (["taskId", "operatorId", "tenantId"].includes(field)) continue;
10782
+ if (!present(keeper[field]) && present(value)) keeper[field] = value;
10783
+ }
10784
+ if (entry.allowFixDispatch === false) keeper.allowFixDispatch = false;
10785
+ const continuationExhausted = keeper.continuationExhausted === true || entry.continuationExhausted === true;
10786
+ keeper.continuationExhausted = continuationExhausted;
10787
+ keeper.needsContinuation = !continuationExhausted && (keeper.needsContinuation === true || entry.needsContinuation === true);
10788
+ if (entry.mergeEnqueued === true) {
10789
+ keeper.mergeEnqueued = true;
10790
+ if ((Number(entry.mergeEnqueuedAt) || 0) > (Number(keeper.mergeEnqueuedAt) || 0)) {
10791
+ keeper.mergeEnqueuedAt = entry.mergeEnqueuedAt;
10792
+ keeper.mergeActionReceiptId = entry.mergeActionReceiptId ?? null;
10793
+ }
10794
+ }
10795
+ if (entry.mergeTerminal === true && keeper.mergeTerminal !== true) {
10796
+ keeper.mergeTerminal = true;
10797
+ keeper.mergeBlockReason = entry.mergeBlockReason || "verification refused";
10798
+ }
10799
+ if ((Number(keeper.mergeErrors) || 0) >= 3) {
10800
+ keeper.mergeRetryExhausted = true;
10801
+ keeper.mergeTerminal = true;
10802
+ keeper.mergeBlockReason ||= `Duplicate watcher history exhausted ${keeper.mergeErrors} automatic merge retries.`;
10803
+ }
10804
+ if (identityConflict) {
10805
+ keeper.automationContextConflict = true;
10806
+ keeper.mergeTerminal = true;
10807
+ keeper.mergeBlockReason = "Duplicate watcher entries disagree on automation attribution.";
10808
+ }
10809
+ delete state[key];
10810
+ collapsed += 1;
10811
+ }
10812
+ return collapsed;
10813
+ }
10659
10814
  function deleteWatcherEntry(state, repo, prNumber) {
10660
10815
  const targetRepo = normalizedRepo(repo);
10661
10816
  for (const [key, entry] of Object.entries(state)) {
@@ -10670,6 +10825,46 @@ var init_watcher_key = __esm({
10670
10825
  }
10671
10826
  });
10672
10827
 
10828
+ // ../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs
10829
+ function nonEmptyString(value) {
10830
+ return typeof value === "string" && value.length > 0;
10831
+ }
10832
+ function resolveWatcherMergeAuthority(entry = {}) {
10833
+ if (entry.automationContextConflict === true) {
10834
+ return {
10835
+ kind: "blocked",
10836
+ reason: "Watcher automation attribution conflicts with the durable code task."
10837
+ };
10838
+ }
10839
+ if (String(entry.repo || "").trim().toLowerCase() !== CANONICAL_WATCHER_MERGE_REPO) {
10840
+ return {
10841
+ kind: "blocked",
10842
+ reason: "Watcher merge target is not the canonical Nexus repository."
10843
+ };
10844
+ }
10845
+ const taskId = entry.taskId;
10846
+ const operatorId = entry.operatorId;
10847
+ const tenantId = entry.tenantId;
10848
+ if (nonEmptyString(taskId) && operatorId === "admin" && (tenantId === null || tenantId === void 0)) {
10849
+ return { kind: "legacy-admin", context: void 0 };
10850
+ }
10851
+ if (nonEmptyString(taskId) && nonEmptyString(operatorId) && nonEmptyString(tenantId)) {
10852
+ return { kind: "scoped", context: { taskId, operatorId, tenantId } };
10853
+ }
10854
+ return {
10855
+ kind: "blocked",
10856
+ reason: INCOMPLETE_WATCHER_AUTHORITY_REASON
10857
+ };
10858
+ }
10859
+ var INCOMPLETE_WATCHER_AUTHORITY_REASON, CANONICAL_WATCHER_MERGE_REPO;
10860
+ var init_watcher_merge_authority = __esm({
10861
+ "../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs"() {
10862
+ "use strict";
10863
+ INCOMPLETE_WATCHER_AUTHORITY_REASON = "Watcher automation attribution is incomplete; merge was not attempted.";
10864
+ CANONICAL_WATCHER_MERGE_REPO = "algosuite-ai/nexus";
10865
+ }
10866
+ });
10867
+
10673
10868
  // ../../scripts/virtual-office/code-runner/watcher-adoption.mjs
10674
10869
  async function adoptPrOpenedTasks(tasks, {
10675
10870
  stateFile,
@@ -10690,7 +10885,38 @@ async function adoptPrOpenedTasks(tasks, {
10690
10885
  if (repos.size > 0 && !repos.has(task.repo.toLowerCase())) continue;
10691
10886
  if (operators.size > 0 && !operators.has(task.operator_id)) continue;
10692
10887
  const key = watcherKey(state, task.repo, task.pr_number);
10693
- if (state[key]) continue;
10888
+ const existing = state[key];
10889
+ if (existing) {
10890
+ let changed = false;
10891
+ let conflict = false;
10892
+ for (const [field, durable] of [
10893
+ ["taskId", task.code_task_id],
10894
+ ["operatorId", task.operator_id],
10895
+ ["tenantId", task.tenant_id]
10896
+ ]) {
10897
+ const comparable = durable === null || typeof durable === "string" && durable.length > 0;
10898
+ if (!comparable) continue;
10899
+ const current = existing[field];
10900
+ if ((current === null || current === void 0 || current === "") && durable !== null) {
10901
+ existing[field] = durable;
10902
+ changed = true;
10903
+ } else if (current !== null && current !== void 0 && current !== "" && current !== durable) {
10904
+ conflict = true;
10905
+ }
10906
+ }
10907
+ if (conflict && existing.automationContextConflict !== true) {
10908
+ existing.automationContextConflict = true;
10909
+ changed = true;
10910
+ }
10911
+ if (!conflict && existing.mergeTerminal === true && existing.mergeBlockReason === INCOMPLETE_WATCHER_AUTHORITY_REASON && resolveWatcherMergeAuthority(existing).kind !== "blocked") {
10912
+ delete existing.mergeTerminal;
10913
+ delete existing.mergeBlockReason;
10914
+ existing.mergeAttempts = 0;
10915
+ changed = true;
10916
+ }
10917
+ if (changed) adopted += 1;
10918
+ continue;
10919
+ }
10694
10920
  const partial = String(task.result || "").includes(PARTIAL_PR_CONTINUATION_MARKER);
10695
10921
  const repairChain = task.repair_chain ?? {
10696
10922
  root_pr_number: task.repair_pr_number ?? task.pr_number,
@@ -10726,6 +10952,7 @@ var init_watcher_adoption = __esm({
10726
10952
  init_watcher_state();
10727
10953
  init_partial_pr_continuation();
10728
10954
  init_watcher_key();
10955
+ init_watcher_merge_authority();
10729
10956
  }
10730
10957
  });
10731
10958
 
@@ -10757,6 +10984,39 @@ var init_watcher_github_token = __esm({
10757
10984
  }
10758
10985
  });
10759
10986
 
10987
+ // ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
10988
+ function supersededSourcePrNumber(prompt) {
10989
+ const text = String(prompt || "");
10990
+ if (text.includes(CI_FIX_MARKER)) {
10991
+ const match = text.match(/\bPR:\s*#(\d+)\b/u);
10992
+ return match ? Number(match[1]) : null;
10993
+ }
10994
+ const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
10995
+ if (repairMatch) return Number(repairMatch[1]);
10996
+ const recoverySupersedeMatch = text.match(
10997
+ /^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
10998
+ );
10999
+ if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
11000
+ const restoredContextMatch = text.match(
11001
+ /^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nThe HQ runner will restore the existing draft PR context before you start \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+), PR #(\d+)\)\. Continue that work and finish it\./u
11002
+ );
11003
+ if (restoredContextMatch) {
11004
+ return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
11005
+ }
11006
+ const continuationMatch = text.match(
11007
+ /^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nContinue the work already started on the branch for PR #(\d+) \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)\); check it out and finish it\./u
11008
+ );
11009
+ if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
11010
+ return Number(continuationMatch[1]);
11011
+ }
11012
+ var CI_FIX_MARKER;
11013
+ var init_superseded_pr_source = __esm({
11014
+ "../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
11015
+ "use strict";
11016
+ CI_FIX_MARKER = "[VO-CI-FIX]";
11017
+ }
11018
+ });
11019
+
10760
11020
  // ../../scripts/virtual-office/code-runner/ci-fix-prompt.mjs
10761
11021
  function buildCiFixPrompt({ prNumber, repo, branch, headSha, failedChecks, prPatch, failedLogs }) {
10762
11022
  return [
@@ -10900,7 +11160,7 @@ var init_pr_watcher_github = __esm({
10900
11160
  });
10901
11161
 
10902
11162
  // ../../scripts/virtual-office/code-runner/enqueue-autonomous-code-task.mjs
10903
- import { randomUUID as randomUUID4 } from "node:crypto";
11163
+ import { randomUUID as randomUUID5 } from "node:crypto";
10904
11164
  function isDefiniteRefusal(err) {
10905
11165
  const status = Number(err?.status);
10906
11166
  if (Number.isFinite(status) && status >= 400 && status < 500) return true;
@@ -10919,7 +11179,7 @@ async function enqueueAutonomousCodeTask(client, task, log2 = () => {
10919
11179
  if (typeof client?.reserveAutonomousDispatchBudget !== "function" || typeof client?.releaseAutonomousDispatchBudget !== "function") {
10920
11180
  throw new Error("autonomous dispatch admission client unavailable");
10921
11181
  }
10922
- const reservationId = randomUUID4();
11182
+ const reservationId = randomUUID5();
10923
11183
  const admission = await client.reserveAutonomousDispatchBudget({
10924
11184
  requestedBudgetUsd,
10925
11185
  reservationId,
@@ -11038,6 +11298,7 @@ async function runWatchCycleUnlocked({
11038
11298
  stateFile = DEFAULT_STATE_FILE
11039
11299
  }) {
11040
11300
  const state = await readWatcherState(stateFile);
11301
+ collapseDuplicateWatcherEntries(state);
11041
11302
  const prNumbers = Object.keys(state);
11042
11303
  let checked = 0;
11043
11304
  let fixed = 0;
@@ -11063,6 +11324,7 @@ async function runWatchCycleUnlocked({
11063
11324
  }
11064
11325
  checked += 1;
11065
11326
  const pr = parsePrCiStatus(view);
11327
+ migrateLegacyMergeState(entry, now());
11066
11328
  entry.lastCi = pr.ci;
11067
11329
  const confirmation = observeRepairFailure(entry, pr, now());
11068
11330
  const proposedAction = decideWatchAction(
@@ -11133,10 +11395,30 @@ async function runWatchCycleUnlocked({
11133
11395
  mergeBlocked += 1;
11134
11396
  log2(`watch: pr #${prNumber} verification blocked merge: ${entry.mergeBlockReason}`);
11135
11397
  } else {
11136
- log2(`watch: pr #${prNumber} verification unavailable; retry ${entry.mergeAttempts}/3`);
11398
+ const retry = await scheduleMergeRetry({
11399
+ entry,
11400
+ now,
11401
+ reportBlocker,
11402
+ taskId: entry.taskId,
11403
+ prNumber,
11404
+ error: new Error(outcome.reason || "verification unavailable"),
11405
+ log: log2
11406
+ });
11407
+ if (retry.terminal) mergeBlocked += 1;
11408
+ if (retry.escalated) escalated += 1;
11137
11409
  }
11138
11410
  } catch (err) {
11139
- log2(`watch: pr #${prNumber} gated merge failed (${entry.mergeAttempts}/3): ${err.message}`);
11411
+ const retry = await scheduleMergeRetry({
11412
+ entry,
11413
+ now,
11414
+ reportBlocker,
11415
+ taskId: entry.taskId,
11416
+ prNumber,
11417
+ error: err,
11418
+ log: log2
11419
+ });
11420
+ if (retry.terminal) mergeBlocked += 1;
11421
+ if (retry.escalated) escalated += 1;
11140
11422
  }
11141
11423
  } else if (action === "fix") {
11142
11424
  entry.fixAttempts = (entry.fixAttempts || 0) + 1;
@@ -11169,6 +11451,7 @@ async function runWatchCycleUnlocked({
11169
11451
  }
11170
11452
  } else {
11171
11453
  entry.lastCheckedAt = now();
11454
+ if (await reportPendingMergeExhaustion({ entry, now, reportBlocker, taskId: entry.taskId, prNumber, log: log2 })) escalated += 1;
11172
11455
  const repairCapped = pr.ci === "failing" && ((entry.fixAttempts || 0) >= maxFixAttempts || entry.allowFixDispatch === false);
11173
11456
  if ((repairCapped || entry.continuationExhausted) && !entry.escalatedAt) {
11174
11457
  try {
@@ -11226,7 +11509,7 @@ function makeWatchRunner({
11226
11509
  repairChainMax,
11227
11510
  repairBudgetUsd
11228
11511
  });
11229
- if (adopted > 0) log2(`watch: adopted ${adopted} open task PR(s) from the control plane`);
11512
+ if (adopted > 0) log2(`watch: adopted or reconciled ${adopted} open task PR(s) from the control plane`);
11230
11513
  return runWatchCycle({
11231
11514
  viewPr: watchView,
11232
11515
  enqueueFix: async ({ prNumber, repo, branch, headSha, failedChecks, failedCheckLinks, repairChain, operatorId }) => {
@@ -11268,13 +11551,10 @@ function makeWatchRunner({
11268
11551
  return result;
11269
11552
  },
11270
11553
  mergePr: (prNumber, entry) => {
11271
- const context = {
11272
- taskId: entry.taskId,
11273
- operatorId: entry.operatorId,
11274
- tenantId: entry.tenantId
11275
- };
11276
- const complete = Object.values(context).every((value) => typeof value === "string" && value.length > 0);
11277
- return client.mergeVerifiedPr(prNumber, complete ? context : void 0);
11554
+ const authority = resolveWatcherMergeAuthority(entry);
11555
+ if (authority.kind === "blocked")
11556
+ return Promise.resolve({ status: "blocked", reason: authority.reason });
11557
+ return client.mergeVerifiedPr(prNumber, authority.context);
11278
11558
  },
11279
11559
  log: log2,
11280
11560
  maxFixAttempts,
@@ -11291,7 +11571,6 @@ var init_pr_watcher = __esm({
11291
11571
  init_check_run_latest();
11292
11572
  init_ci_repair_evidence();
11293
11573
  init_pr_watcher_failure_confirmation();
11294
- init_superseded_pr_source();
11295
11574
  init_watcher_coordination();
11296
11575
  init_watcher_adoption();
11297
11576
  init_watcher_key();
@@ -11300,6 +11579,7 @@ var init_pr_watcher = __esm({
11300
11579
  init_pr_watcher_github();
11301
11580
  init_enqueue_autonomous_code_task();
11302
11581
  init_watcher_state();
11582
+ init_watcher_merge_authority();
11303
11583
  init_superseded_pr_source();
11304
11584
  init_ci_fix_prompt();
11305
11585
  DEFAULT_STATE_FILE = join14(homedir9(), ".vo", "dispatched-prs.json");
@@ -11733,7 +12013,7 @@ var init_effort_mode_config = __esm({
11733
12013
  });
11734
12014
 
11735
12015
  // ../../scripts/virtual-office/model-registry.mjs
11736
- import { randomUUID as randomUUID5 } from "node:crypto";
12016
+ import { randomUUID as randomUUID6 } from "node:crypto";
11737
12017
  import fs11 from "node:fs";
11738
12018
  import os4 from "node:os";
11739
12019
  import path18 from "node:path";
@@ -11744,7 +12024,7 @@ function userCacheRoot() {
11744
12024
  if (home) return path18.join(home, ".claude");
11745
12025
  } catch {
11746
12026
  }
11747
- return path18.join(os4.tmpdir(), `vo-model-registry-${randomUUID5()}`);
12027
+ return path18.join(os4.tmpdir(), `vo-model-registry-${randomUUID6()}`);
11748
12028
  }
11749
12029
  function resolveCacheBaseDir(env2 = process.env, moduleDir = __dirname) {
11750
12030
  if (env2.VO_MODEL_REGISTRY_CACHE_DIR) return env2.VO_MODEL_REGISTRY_CACHE_DIR;
@@ -13110,9 +13390,16 @@ function makeSafeProgress(log2) {
13110
13390
  function runnerStagePatch(stage, message, extra = {}) {
13111
13391
  return { stage, message, ...extra };
13112
13392
  }
13393
+ function preservedReceiptLines(run, slicedBodyText) {
13394
+ const full = `${String(run?.summary || "")}
13395
+ ${String(run?.lastAgentMessage || "")}`;
13396
+ const found = [...new Set((full.match(RECEIPT_LINE_RE) || []).map((line) => line.trim()))];
13397
+ const missing = found.filter((line) => !String(slicedBodyText || "").includes(line));
13398
+ return missing.length ? ["", "### Consensus evidence (preserved past truncation)", "", ...missing.map((line) => redactSecrets(line))] : [];
13399
+ }
13113
13400
  function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
13114
13401
  const governedStakes = matchGovernedStakes({ prompt: String(task.prompt || "") });
13115
- return [
13402
+ const slicedSections = [
13116
13403
  "## AlgoHQ Command Center \u2014 Code-from-Anywhere task",
13117
13404
  "",
13118
13405
  `- **Task:** \`${task.code_task_id}\``,
@@ -13135,7 +13422,12 @@ function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
13135
13422
  "",
13136
13423
  // A budget/turn-capped run ends with no assistant text (summary = the bare
13137
13424
  // subtype); its LAST message is the honest report the operator needs.
13138
- ...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : [],
13425
+ ...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : []
13426
+ ];
13427
+ return [
13428
+ ...slicedSections,
13429
+ // Receipt lines the slices dropped — the gate reads only this body.
13430
+ ...preservedReceiptLines(run, slicedSections.join("\n")),
13139
13431
  "---",
13140
13432
  armAutoMerge ? "_Opened by the AlgoHQ code-runner daemon. After CI passes, the watcher must obtain a durable consensus receipt and merge the exact verified SHA._" : "_Opened by the AlgoHQ code-runner daemon. This PR awaits the verify-before-act gate / operator review \u2014 it is NOT auto-merged._"
13141
13433
  ].filter((l) => l !== "").join("\n");
@@ -13153,11 +13445,13 @@ async function mintRunnerGithubTokens({ client, taskId, log: log2, repo = null,
13153
13445
  if (!agentReadToken) log2(`task ${taskId}: no GitHub read access for the agent (${reason}); it cannot read a private repo`);
13154
13446
  return { publishToken, agentReadToken };
13155
13447
  }
13448
+ var RECEIPT_LINE_RE;
13156
13449
  var init_task_helpers = __esm({
13157
13450
  "../../scripts/virtual-office/code-runner/task-helpers.mjs"() {
13158
13451
  "use strict";
13159
13452
  init_redact_tokens();
13160
13453
  init_methodology_composer();
13454
+ RECEIPT_LINE_RE = /receipt id:\s*[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/giu;
13161
13455
  }
13162
13456
  });
13163
13457
 
@@ -15022,7 +15316,7 @@ var init_detached_economics_spool = __esm({
15022
15316
  });
15023
15317
 
15024
15318
  // ../../scripts/virtual-office/code-runner/killed-run-outcome.mjs
15025
- import { randomUUID as randomUUID6 } from "node:crypto";
15319
+ import { randomUUID as randomUUID7 } from "node:crypto";
15026
15320
  async function handleKilledRun({
15027
15321
  client,
15028
15322
  id,
@@ -15059,7 +15353,7 @@ async function handleKilledRun({
15059
15353
  };
15060
15354
  }
15061
15355
  if (reason === "claim_authority_changed") {
15062
- const occurrenceId = randomUUID6();
15356
+ const occurrenceId = randomUUID7();
15063
15357
  const economics = {
15064
15358
  occurrence_id: occurrenceId,
15065
15359
  runner_id: runnerId,
@@ -15359,7 +15653,7 @@ var code_runner_daemon_exports = {};
15359
15653
  __export(code_runner_daemon_exports, {
15360
15654
  main: () => main
15361
15655
  });
15362
- import { randomUUID as randomUUID7 } from "node:crypto";
15656
+ import { randomUUID as randomUUID8 } from "node:crypto";
15363
15657
  import { fileURLToPath as fileURLToPath8 } from "node:url";
15364
15658
  function log(msg) {
15365
15659
  console.log(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
@@ -15592,7 +15886,7 @@ Closes #${publicationTarget.supersedesPrNumber}` : "";
15592
15886
  async function main({ env: env2 = process.env, once: once2 = false } = {}) {
15593
15887
  const cfg = loadCodeRunnerConfig(env2, { log });
15594
15888
  await sweepStaleTaskAttachmentDirectories().catch((error) => log(`stale attachment cleanup failed: ${error.message}`));
15595
- const runnerInstanceId = randomUUID7();
15889
+ const runnerInstanceId = randomUUID8();
15596
15890
  const client = createControlPlaneClient({
15597
15891
  env: env2,
15598
15892
  runnerId: cfg.runnerId,
@@ -15804,8 +16098,163 @@ var init_code_runner_daemon = __esm({
15804
16098
  import { createRequire as createRequire4 } from "node:module";
15805
16099
 
15806
16100
  // src/runner-readiness.mjs
15807
- function failed({ paired = false, operatorId = null, tenantId = null, githubReady = null, error, message }) {
15808
- return { ok: false, paired, operatorId, tenantId, githubReady, error, message };
16101
+ import { createHash, randomUUID } from "node:crypto";
16102
+ var RUNNER_GITHUB_READINESS_TIMEOUT_MS = 5e4;
16103
+ var RUNNER_IDENTITY_READINESS_TIMEOUT_MS = 1e4;
16104
+ var RUNNER_READINESS_MAX_REPOSITORIES = 4;
16105
+ var RUNNER_READINESS_MAX_TIMER_DELAY_MS = 2147e6;
16106
+ var RUNNER_READINESS_TRANSIENT_MAX_RETRY_AFTER_MS = 3e5;
16107
+ var RUNNER_READINESS_IPC_ACK_TIMEOUT_MS = 5e3;
16108
+ var RUNNER_READINESS_DEFERRAL_MESSAGE_TYPE = "vo-runner-readiness-deferred-v1";
16109
+ var RUNNER_READINESS_DEFERRAL_ACK_TYPE = "vo-runner-readiness-deferred-ack-v1";
16110
+ var RUNNER_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
16111
+ function runnerRepositoryScopeFromEnv(env2 = {}) {
16112
+ return String(env2.VO_CODE_RUNNER_REPOS || "").split(/[\s,]+/u).map((repo) => repo.trim()).filter(Boolean);
16113
+ }
16114
+ function normalizeRunnerRepositoryScope(repositories) {
16115
+ if (!Array.isArray(repositories) || repositories.length < 1) {
16116
+ throw new Error("VO_CODE_RUNNER_REPOS must name at least one owner/name repository");
16117
+ }
16118
+ if (repositories.length > RUNNER_READINESS_MAX_REPOSITORIES) {
16119
+ throw new Error(`VO_CODE_RUNNER_REPOS supports at most ${RUNNER_READINESS_MAX_REPOSITORIES} repositories`);
16120
+ }
16121
+ const normalized = repositories.map((repo) => {
16122
+ const [owner, name] = typeof repo === "string" ? repo.split("/") : [];
16123
+ if (typeof repo !== "string" || repo.length > 140 || !RUNNER_REPOSITORY.test(repo) || owner === "." || owner === ".." || name === "." || name === "..") {
16124
+ throw new Error("VO_CODE_RUNNER_REPOS entries must be canonical owner/name repositories");
16125
+ }
16126
+ return repo.toLowerCase();
16127
+ });
16128
+ if (new Set(normalized).size !== normalized.length) {
16129
+ throw new Error("VO_CODE_RUNNER_REPOS entries must be unique");
16130
+ }
16131
+ const owners = new Set(normalized.map((repo) => repo.split("/")[0]));
16132
+ if (owners.size !== 1) {
16133
+ throw new Error("VO_CODE_RUNNER_REPOS entries must share one owner");
16134
+ }
16135
+ return Object.freeze(normalized.sort());
16136
+ }
16137
+ function runnerRepositoryScopeDigest(repositories) {
16138
+ const scope = normalizeRunnerRepositoryScope(repositories);
16139
+ return createHash("sha256").update(JSON.stringify({
16140
+ version: 1,
16141
+ repositories: scope
16142
+ }), "utf8").digest("hex");
16143
+ }
16144
+ function runnerReadinessRetryDelayMs(readiness) {
16145
+ if (readiness?.paired !== true || readiness?.githubReady !== false) return null;
16146
+ if (Number.isFinite(readiness.retryAfterMs) && readiness.retryAfterMs > 0) {
16147
+ return Math.ceil(readiness.retryAfterMs);
16148
+ }
16149
+ return null;
16150
+ }
16151
+ function runnerReadinessFailureDisposition(readiness, delivery = null) {
16152
+ const retryAfterMs = runnerReadinessRetryDelayMs(readiness);
16153
+ if (retryAfterMs === null) {
16154
+ return Object.freeze({ action: "exit", exitCode: 1, retryAfterMs: null });
16155
+ }
16156
+ if (delivery?.attempted === true) {
16157
+ return Object.freeze({
16158
+ action: "exit",
16159
+ exitCode: delivery.delivered === true ? 75 : 1,
16160
+ retryAfterMs
16161
+ });
16162
+ }
16163
+ return Object.freeze({ action: "wait", exitCode: null, retryAfterMs });
16164
+ }
16165
+ async function notifySupervisorReadinessDeferral({
16166
+ processLike,
16167
+ retryAfterMs,
16168
+ error = null,
16169
+ ackTimeoutMs = RUNNER_READINESS_IPC_ACK_TIMEOUT_MS
16170
+ }) {
16171
+ if (processLike?.connected !== true || typeof processLike.send !== "function") {
16172
+ return Object.freeze({ attempted: false, delivered: false });
16173
+ }
16174
+ if (typeof processLike.on !== "function" || typeof processLike.off !== "function") {
16175
+ return Object.freeze({ attempted: true, delivered: false });
16176
+ }
16177
+ const nonce = randomUUID();
16178
+ const delivered = await new Promise((resolve3) => {
16179
+ let settled = false;
16180
+ const onMessage = (message) => {
16181
+ if (message?.type === RUNNER_READINESS_DEFERRAL_ACK_TYPE && message.nonce === nonce) {
16182
+ finish(true);
16183
+ }
16184
+ };
16185
+ const onDisconnect = () => finish(false);
16186
+ const finish = (value) => {
16187
+ if (settled) return;
16188
+ settled = true;
16189
+ clearTimeout(timer);
16190
+ processLike.off("message", onMessage);
16191
+ processLike.off("disconnect", onDisconnect);
16192
+ resolve3(value);
16193
+ };
16194
+ const timer = setTimeout(() => finish(false), ackTimeoutMs);
16195
+ processLike.on("message", onMessage);
16196
+ processLike.on("disconnect", onDisconnect);
16197
+ try {
16198
+ processLike.send({
16199
+ type: RUNNER_READINESS_DEFERRAL_MESSAGE_TYPE,
16200
+ nonce,
16201
+ retryAfterMs,
16202
+ error
16203
+ }, (sendError) => {
16204
+ if (sendError) finish(false);
16205
+ });
16206
+ } catch {
16207
+ finish(false);
16208
+ }
16209
+ });
16210
+ return Object.freeze({ attempted: true, delivered });
16211
+ }
16212
+ async function waitForRunnerReadinessRetry(delayMs, {
16213
+ nowMs = () => Date.now(),
16214
+ wait: wait3 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
16215
+ } = {}) {
16216
+ if (!Number.isFinite(delayMs) || delayMs <= 0) {
16217
+ throw new TypeError("runner readiness retry delay is invalid");
16218
+ }
16219
+ const deadline = nowMs() + Math.ceil(delayMs);
16220
+ while (nowMs() < deadline) {
16221
+ await wait3(Math.min(RUNNER_READINESS_MAX_TIMER_DELAY_MS, deadline - nowMs()));
16222
+ }
16223
+ }
16224
+ function failed({
16225
+ paired = false,
16226
+ operatorId = null,
16227
+ tenantId = null,
16228
+ githubReady = null,
16229
+ retryAfterMs = null,
16230
+ error,
16231
+ message
16232
+ }) {
16233
+ return {
16234
+ ok: false,
16235
+ paired,
16236
+ operatorId,
16237
+ tenantId,
16238
+ githubReady,
16239
+ ...Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? { retryAfterMs } : {},
16240
+ error,
16241
+ message
16242
+ };
16243
+ }
16244
+ function responseRetryAfterMs(response, body) {
16245
+ const transientReadinessFailure = response.status === 503 && body?.error === "github_installation_readiness_retryable";
16246
+ if (response.status !== 429 && !transientReadinessFailure) return null;
16247
+ const bound = (retryAfterMs) => transientReadinessFailure ? Math.min(RUNNER_READINESS_TRANSIENT_MAX_RETRY_AFTER_MS, retryAfterMs) : retryAfterMs;
16248
+ const value = response.headers?.get?.("retry-after")?.trim() ?? "";
16249
+ const seconds = Number(value);
16250
+ if (value && Number.isFinite(seconds) && seconds >= 0) {
16251
+ return bound(Math.max(1e3, Math.ceil(seconds * 1e3)));
16252
+ }
16253
+ const at = value ? Date.parse(value) : Number.NaN;
16254
+ if (Number.isFinite(at)) {
16255
+ return bound(Math.max(1e3, Math.ceil(at - Date.now())));
16256
+ }
16257
+ return bound(6e4);
15809
16258
  }
15810
16259
  async function responseBody(response) {
15811
16260
  try {
@@ -15818,11 +16267,21 @@ async function responseBody(response) {
15818
16267
  function serverMessage(body, fallback) {
15819
16268
  return typeof body.message === "string" && body.message.trim() ? body.message.trim() : fallback;
15820
16269
  }
15821
- async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
16270
+ async function fetchJsonWithTimeout(fetchImpl, url, init, timeoutMs) {
15822
16271
  const controller = new AbortController();
15823
- const timer = setTimeout(() => controller.abort(), timeoutMs);
16272
+ let timer;
16273
+ const timeout = new Promise((_, reject) => {
16274
+ timer = setTimeout(() => {
16275
+ controller.abort();
16276
+ reject(new Error(`request aborted after ${timeoutMs}ms`));
16277
+ }, timeoutMs);
16278
+ });
15824
16279
  try {
15825
- return await fetchImpl(url, { ...init, signal: controller.signal });
16280
+ const requestAndBody = (async () => {
16281
+ const response = await fetchImpl(url, { ...init, signal: controller.signal });
16282
+ return { response, body: await responseBody(response) };
16283
+ })();
16284
+ return await Promise.race([requestAndBody, timeout]);
15826
16285
  } finally {
15827
16286
  clearTimeout(timer);
15828
16287
  }
@@ -15832,18 +16291,24 @@ async function probeRunnerReadiness({
15832
16291
  token: token2,
15833
16292
  fetchImpl = fetch,
15834
16293
  requireGithub = false,
15835
- timeoutMs = 1e4
16294
+ repositories = [],
16295
+ timeoutMs = RUNNER_IDENTITY_READINESS_TIMEOUT_MS,
16296
+ // The server performs at most five sequential App-JWT GETs (installation plus
16297
+ // every one of at most four configured repositories), each bounded at 8s.
16298
+ // Keep a transport margin while preventing a stuck proof from hanging startup.
16299
+ githubTimeoutMs = RUNNER_GITHUB_READINESS_TIMEOUT_MS
15836
16300
  }) {
15837
16301
  const base = controlPlaneUrl2.replace(/\/+$/u, "");
15838
16302
  const headers = { authorization: `Bearer ${token2}` };
15839
16303
  let identityResponse;
16304
+ let identity;
15840
16305
  try {
15841
- identityResponse = await fetchWithTimeout(
16306
+ ({ response: identityResponse, body: identity } = await fetchJsonWithTimeout(
15842
16307
  fetchImpl,
15843
16308
  `${base}/api/v1/auth/me`,
15844
16309
  { headers },
15845
16310
  timeoutMs
15846
- );
16311
+ ));
15847
16312
  } catch (error) {
15848
16313
  const detail = error instanceof Error ? error.message : String(error);
15849
16314
  return failed({
@@ -15851,7 +16316,6 @@ async function probeRunnerReadiness({
15851
16316
  message: `AlgoHQ could not be reached: ${detail}`
15852
16317
  });
15853
16318
  }
15854
- const identity = await responseBody(identityResponse);
15855
16319
  if (!identityResponse.ok) {
15856
16320
  return failed({
15857
16321
  error: "credential_rejected",
@@ -15877,18 +16341,33 @@ async function probeRunnerReadiness({
15877
16341
  message: "Paired to AlgoHQ."
15878
16342
  };
15879
16343
  }
16344
+ let repositoryScope = null;
16345
+ try {
16346
+ if (!Array.isArray(repositories)) {
16347
+ throw new Error("VO_CODE_RUNNER_REPOS must be a repository array");
16348
+ }
16349
+ if (repositories.length > 0) repositoryScope = normalizeRunnerRepositoryScope(repositories);
16350
+ } catch (error) {
16351
+ return failed({
16352
+ paired: true,
16353
+ operatorId,
16354
+ tenantId,
16355
+ githubReady: false,
16356
+ error: "github_repository_scope_invalid",
16357
+ message: error instanceof Error ? error.message : String(error)
16358
+ });
16359
+ }
15880
16360
  let githubResponse;
16361
+ let github;
15881
16362
  try {
15882
- githubResponse = await fetchWithTimeout(
16363
+ const readinessUrl = new URL(`${base}/api/v1/github/installation-readiness`);
16364
+ for (const repo of repositoryScope ?? []) readinessUrl.searchParams.append("repo", repo);
16365
+ ({ response: githubResponse, body: github } = await fetchJsonWithTimeout(
15883
16366
  fetchImpl,
15884
- `${base}/api/v1/github/installation-token`,
15885
- {
15886
- method: "POST",
15887
- headers: { ...headers, "content-type": "application/json" },
15888
- body: "{}"
15889
- },
15890
- timeoutMs
15891
- );
16367
+ readinessUrl.toString(),
16368
+ { headers },
16369
+ githubTimeoutMs
16370
+ ));
15892
16371
  } catch (error) {
15893
16372
  const detail = error instanceof Error ? error.message : String(error);
15894
16373
  return failed({
@@ -15900,14 +16379,25 @@ async function probeRunnerReadiness({
15900
16379
  message: `GitHub publication readiness could not be checked: ${detail}`
15901
16380
  });
15902
16381
  }
15903
- const github = await responseBody(githubResponse);
15904
- if (!githubResponse.ok || typeof github.token !== "string" || !github.token) {
16382
+ const repositorySelection = github.repository_selection;
16383
+ const repositoriesVerified = github.repositories_verified;
16384
+ const returnedScope = github.repository_scope;
16385
+ let normalizedReturnedScope = null;
16386
+ try {
16387
+ normalizedReturnedScope = normalizeRunnerRepositoryScope(returnedScope);
16388
+ } catch {
16389
+ }
16390
+ const effectiveScope = repositoryScope ?? normalizedReturnedScope;
16391
+ const sourceVerified = repositoryScope === null ? github.repository_scope_source === "persisted_installation_singleton" && normalizedReturnedScope?.length === 1 : github.repository_scope_source === "runner_config";
16392
+ const repositoryScopeVerified = effectiveScope !== null && normalizedReturnedScope !== null && Array.isArray(returnedScope) && returnedScope.length === normalizedReturnedScope.length && returnedScope.every((repo, index) => repo === normalizedReturnedScope[index]) && normalizedReturnedScope.length === effectiveScope.length && normalizedReturnedScope.every((repo, index) => repo === effectiveScope[index]) && github.repository_scope_sha256 === runnerRepositoryScopeDigest(effectiveScope) && repositoriesVerified === effectiveScope.length && sourceVerified && (repositorySelection === "all" || repositorySelection === "selected");
16393
+ if (!githubResponse.ok || github.configured !== true || github.verified !== true || github.publication_ready !== true || !repositoryScopeVerified) {
15905
16394
  const error = typeof github.error === "string" && github.error ? github.error : "github_not_ready";
15906
16395
  return failed({
15907
16396
  paired: true,
15908
16397
  operatorId,
15909
16398
  tenantId,
15910
16399
  githubReady: false,
16400
+ retryAfterMs: responseRetryAfterMs(githubResponse, github),
15911
16401
  error,
15912
16402
  message: serverMessage(github, `GitHub publication preflight failed (HTTP ${githubResponse.status}).`)
15913
16403
  });
@@ -15918,8 +16408,10 @@ async function probeRunnerReadiness({
15918
16408
  operatorId,
15919
16409
  tenantId,
15920
16410
  githubReady: true,
16411
+ repositoryScope: effectiveScope,
16412
+ repositoryScopeSource: github.repository_scope_source,
15921
16413
  error: null,
15922
- message: "Paired and ready to publish through the Algosuite GitHub App."
16414
+ message: `Paired with a verified Algosuite GitHub App installation (${repositoriesVerified} repos).`
15923
16415
  };
15924
16416
  }
15925
16417
  function pairedOperatorScope(readiness) {
@@ -15930,7 +16422,7 @@ function pairedOperatorScope(readiness) {
15930
16422
  import { closeSync, existsSync, mkdirSync, openSync, unlinkSync } from "node:fs";
15931
16423
  import { homedir } from "node:os";
15932
16424
  import { posix, win32 } from "node:path";
15933
- import { randomUUID } from "node:crypto";
16425
+ import { randomUUID as randomUUID2 } from "node:crypto";
15934
16426
  var APP_IDENTIFIER = "ai.algosuite.vo-runner";
15935
16427
  var CLONES_DIR = "clones";
15936
16428
  function pathsFor(platform4) {
@@ -16000,7 +16492,7 @@ function runnerWorkingDirectory({ repoRoot: repoRoot2, clonesRoot: clonesRoot2 }
16000
16492
  }
16001
16493
  function assertWritableRunnerDirectory(root) {
16002
16494
  mkdirSync(root, { recursive: true });
16003
- const probe = pathsFor(process.platform).join(root, `.vo-runner-write-probe-${process.pid}-${randomUUID()}`);
16495
+ const probe = pathsFor(process.platform).join(root, `.vo-runner-write-probe-${process.pid}-${randomUUID2()}`);
16004
16496
  let handle;
16005
16497
  try {
16006
16498
  handle = openSync(probe, "wx", 384);
@@ -16120,21 +16612,51 @@ if (!token) {
16120
16612
  }
16121
16613
  var controlPlaneUrl = process.env.VO_CONTROL_PLANE_URL || DEFAULT_CONTROL_PLANE_URL3;
16122
16614
  var pairedOperatorId = null;
16123
- if (!explicitAdminToken) {
16124
- const readiness = await probeRunnerReadiness({
16125
- controlPlaneUrl,
16126
- token,
16127
- requireGithub: true
16615
+ async function deferRunnerReadinessRetry(readiness) {
16616
+ const initial = runnerReadinessFailureDisposition(readiness);
16617
+ if (initial.action === "exit") return initial;
16618
+ const { retryAfterMs } = initial;
16619
+ console.error(
16620
+ `[vo-mcp runner] GitHub readiness is temporarily unavailable; retrying after ${retryAfterMs}ms: ${readiness.message}`
16621
+ );
16622
+ const delivery = await notifySupervisorReadinessDeferral({
16623
+ processLike: process,
16624
+ retryAfterMs,
16625
+ error: readiness.error
16128
16626
  });
16129
- if (!readiness.ok) {
16130
- console.error(`[vo-mcp runner] Readiness check failed: ${readiness.message}`);
16131
- process.exit(1);
16627
+ if (delivery.attempted) {
16628
+ const disposition = runnerReadinessFailureDisposition(readiness, delivery);
16629
+ if (disposition.exitCode === 1) {
16630
+ console.error("[vo-mcp runner] Supervisor readiness deferral could not be delivered; failing closed.");
16631
+ }
16632
+ return disposition;
16633
+ }
16634
+ await waitForRunnerReadinessRetry(retryAfterMs);
16635
+ return Object.freeze({ action: "retry", exitCode: null, retryAfterMs });
16636
+ }
16637
+ if (!explicitAdminToken) {
16638
+ let readiness;
16639
+ while (true) {
16640
+ readiness = await probeRunnerReadiness({
16641
+ controlPlaneUrl,
16642
+ token,
16643
+ requireGithub: true,
16644
+ repositories: runnerRepositoryScopeFromEnv(process.env)
16645
+ });
16646
+ if (readiness.ok) break;
16647
+ const disposition = await deferRunnerReadinessRetry(readiness);
16648
+ if (disposition.action === "retry") continue;
16649
+ if (disposition.retryAfterMs === null) {
16650
+ console.error(`[vo-mcp runner] Readiness check failed: ${readiness.message}`);
16651
+ }
16652
+ process.exit(disposition.exitCode);
16132
16653
  }
16133
16654
  pairedOperatorId = pairedOperatorScope(readiness);
16134
16655
  if (!pairedOperatorId) {
16135
16656
  console.error("[vo-mcp runner] Readiness check failed: paired operator scope is missing. Pair this computer again.");
16136
16657
  process.exit(1);
16137
16658
  }
16659
+ process.env.VO_CODE_RUNNER_REPOS = readiness.repositoryScope.join(",");
16138
16660
  }
16139
16661
  var rootConfig;
16140
16662
  try {