@algosuite/vo-mcp 0.2.0-beta.54 → 0.2.0-beta.56

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.
@@ -1178,8 +1178,8 @@ async function walkManagedRoots(ownership, options, onLink) {
1178
1178
  const normalized = normalizeOwnership(ownership);
1179
1179
  let scannedEntries = 0;
1180
1180
  for (const rootPath of normalized.managedRoots) {
1181
- const present = await assertManagedRoot(rootPath, normalized.worktreeRoot, fsApi);
1182
- if (!present) continue;
1181
+ const present2 = await assertManagedRoot(rootPath, normalized.worktreeRoot, fsApi);
1182
+ if (!present2) continue;
1183
1183
  const stack = [rootPath];
1184
1184
  while (stack.length > 0) {
1185
1185
  const current = stack.pop();
@@ -3497,8 +3497,9 @@ async function mergeVerifiedPrRequest(req, prNumber, automationContext, onUnauth
3497
3497
  actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
3498
3498
  };
3499
3499
  }
3500
- if (res.status === 503 && json?.error === "verify_unavailable") {
3501
- return { status: "retry", reason: json.reason || "verification unavailable" };
3500
+ const captureStoreRetry = json?.action_status === "not_attempted" && (json?.error === "capture_preflight_unavailable" || json?.error === "decision_outcome_intent_failed");
3501
+ if (res.status === 503 && (json?.error === "verify_unavailable" || json?.error === "merge_unavailable" || captureStoreRetry)) {
3502
+ return { status: "retry", reason: json.reason || json.message || json.error || "verification unavailable" };
3502
3503
  }
3503
3504
  return {
3504
3505
  status: "blocked",
@@ -3604,6 +3605,64 @@ var init_claim_gate_notice = __esm({
3604
3605
  }
3605
3606
  });
3606
3607
 
3608
+ // ../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs
3609
+ function isRetryableStatus(status) {
3610
+ return status >= 500 && status <= 599;
3611
+ }
3612
+ function defaultSleep(ms) {
3613
+ return new Promise((resolve3) => {
3614
+ setTimeout(resolve3, ms);
3615
+ });
3616
+ }
3617
+ async function getTaskKnowledgeContextRequest(req, taskId, { query } = {}, {
3618
+ taskRequestTimeoutMs,
3619
+ invalidateToken = () => {
3620
+ },
3621
+ sleep: sleep3 = defaultSleep,
3622
+ log: log2 = () => {
3623
+ }
3624
+ } = {}) {
3625
+ const body = {};
3626
+ if (typeof query === "string" && query.trim()) body.query = query;
3627
+ const path22 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
3628
+ const timeoutMs = Math.max(Number(taskRequestTimeoutMs) || 0, MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS);
3629
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
3630
+ let res;
3631
+ let cause;
3632
+ try {
3633
+ res = await req("POST", path22, body, { timeoutMs });
3634
+ } catch (err) {
3635
+ cause = err;
3636
+ }
3637
+ if (!cause) {
3638
+ if (res.status === 401) {
3639
+ invalidateToken();
3640
+ throw new Error("knowledge-context unauthorized (401)");
3641
+ }
3642
+ if (res.status === 404) return null;
3643
+ if (res.ok) return res.json();
3644
+ if (!isRetryableStatus(res.status)) {
3645
+ throw new Error(`knowledge-context failed: HTTP ${res.status}`);
3646
+ }
3647
+ cause = new Error(`knowledge-context failed: HTTP ${res.status}`);
3648
+ }
3649
+ if (attempt === MAX_ATTEMPTS) throw cause;
3650
+ const delayMs = RETRY_DELAYS_MS[attempt - 1];
3651
+ log2(`knowledge-context attempt ${attempt}/${MAX_ATTEMPTS} failed (${cause.message}); retrying in ${delayMs}ms`);
3652
+ await sleep3(delayMs);
3653
+ }
3654
+ throw new Error("knowledge-context retry loop exited unexpectedly");
3655
+ }
3656
+ var MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS, RETRY_DELAYS_MS, MAX_ATTEMPTS;
3657
+ var init_control_plane_knowledge_context = __esm({
3658
+ "../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs"() {
3659
+ "use strict";
3660
+ MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS = 15e3;
3661
+ RETRY_DELAYS_MS = [2e3, 6e3];
3662
+ MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1;
3663
+ }
3664
+ });
3665
+
3607
3666
  // src/runner/control-plane-auth-stub.mjs
3608
3667
  var control_plane_auth_stub_exports = {};
3609
3668
  __export(control_plane_auth_stub_exports, {
@@ -3647,7 +3706,8 @@ function createControlPlaneClient({
3647
3706
  6e4
3648
3707
  ),
3649
3708
  runnerId,
3650
- runnerInstanceId
3709
+ runnerInstanceId,
3710
+ sleep: sleep3
3651
3711
  } = {}) {
3652
3712
  const resolvedBaseUrl = baseUrl ?? env2.VO_CONTROL_PLANE_URL ?? "";
3653
3713
  if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
@@ -3828,17 +3888,16 @@ function createControlPlaneClient({
3828
3888
  if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
3829
3889
  return Buffer.from(await res.arrayBuffer());
3830
3890
  },
3891
+ // Raised per-attempt timeout (>=15s) + bounded retry — see control-plane-knowledge-context.mjs.
3831
3892
  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();
3893
+ return getTaskKnowledgeContextRequest(req, taskId, { query }, {
3894
+ taskRequestTimeoutMs,
3895
+ invalidateToken: () => {
3896
+ cachedFirebaseToken = null;
3897
+ },
3898
+ sleep: sleep3,
3899
+ log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`)
3900
+ });
3842
3901
  },
3843
3902
  /** Weekly Claude token usage report — see control-plane-weekly-tokens.mjs. */
3844
3903
  async postWeeklyTokens(report) {
@@ -3992,6 +4051,7 @@ var init_control_plane_client = __esm({
3992
4051
  init_control_plane_weekly_tokens();
3993
4052
  init_control_plane_telemetry_relay();
3994
4053
  init_claim_gate_notice();
4054
+ init_control_plane_knowledge_context();
3995
4055
  cachedFirebaseToken = null;
3996
4056
  ClaimAuthorityChangedError = class extends Error {
3997
4057
  constructor() {
@@ -8350,6 +8410,15 @@ function classifyTaskShape(task) {
8350
8410
  }
8351
8411
  return "feature";
8352
8412
  }
8413
+ function declaredStakes(prompt) {
8414
+ const m = GOVERNED_STAKES_DECLARATION_RE.exec(prompt);
8415
+ if (!m) return void 0;
8416
+ const remainder = m[1].trim();
8417
+ if (remainder.length === 0) return void 0;
8418
+ if (/^none\b/iu.test(remainder)) return null;
8419
+ const value = remainder.split(".")[0].trim().slice(0, 60);
8420
+ return value.length > 0 ? value : void 0;
8421
+ }
8353
8422
  function stripDispatchBoilerplate(text) {
8354
8423
  const contractLines = HEADLESS_EXECUTION_CONTRACT.split("\n").map((l) => l.trim()).filter((l) => l.length >= 20);
8355
8424
  return String(text ?? "").split("\n").map((rawLine) => {
@@ -8365,6 +8434,8 @@ function stripDispatchBoilerplate(text) {
8365
8434
  }
8366
8435
  function matchGovernedStakes(task) {
8367
8436
  const prompt = stripDispatchBoilerplate(String(task?.prompt || ""));
8437
+ const declared = declaredStakes(prompt);
8438
+ if (declared !== void 0) return declared;
8368
8439
  const m = GOVERNED_STAKES_PATTERN.exec(prompt);
8369
8440
  return m ? m[0] : null;
8370
8441
  }
@@ -8390,7 +8461,7 @@ function withMethodology(prompt, task) {
8390
8461
 
8391
8462
  ${block}` };
8392
8463
  }
8393
- var UI_ROADMAP_DISPATCH_MARKER, SHAPE_RULES, GOVERNED_STAKES_PATTERN, RESEARCH_WORKFLOW_DIRECTIVE, CONSENSUS_DIRECTIVES, UNIVERSAL_DIRECTIVES, SHAPE_DIRECTIVES;
8464
+ var UI_ROADMAP_DISPATCH_MARKER, SHAPE_RULES, GOVERNED_STAKES_PATTERN, GOVERNED_STAKES_DECLARATION_RE, RESEARCH_WORKFLOW_DIRECTIVE, CONSENSUS_DIRECTIVES, UNIVERSAL_DIRECTIVES, SHAPE_DIRECTIVES;
8394
8465
  var init_methodology_composer = __esm({
8395
8466
  "../../scripts/virtual-office/code-runner/methodology-composer.mjs"() {
8396
8467
  "use strict";
@@ -8434,6 +8505,7 @@ var init_methodology_composer = __esm({
8434
8505
  }
8435
8506
  ];
8436
8507
  GOVERNED_STAKES_PATTERN = /\b(FERPA|IDOR|HIPAA|PII|privacy|security|authz|authorization|access[- ]control|permission[- ]denied|IRS|tax|§\s?\d|payroll|1099|W-2|MACRS|depreciation|billing|payment|refund|ledger|journal entr|reconcil|compliance|IEP\b|§?504\b|safeguard|governed fact)\b/iu;
8508
+ GOVERNED_STAKES_DECLARATION_RE = /\bgoverned stakes:[ \t]*([^\n]{1,120})/iu;
8437
8509
  RESEARCH_WORKFLOW_DIRECTIVE = "For a genuinely open research question (not a single-file or single-log lookup \u2014 those need no harness), use the office research harness rather than ad-hoc browsing: for an open-web question run the workflow at ~/.claude/workflows/storm-deep-research-budget.mjs (Workflow tool, scriptPath, the question as args); for a question about THIS repo run ~/.claude/workflows/deep-research-internal-budget.mjs. Use ONLY the -budget variants (sonnet investigators, one opus synthesis) and stay inside this task budget. If the Workflow tool or those scripts are unavailable on this host, say so in the report and run the same four stages yourself \u2014 perspectives, WebSearch/WebFetch investigation, an adversarial pass that tries to REFUTE each key claim, then a cited write-up \u2014 with at most cheap-tier subagents; never a Fable/Opus fan-out.";
8438
8510
  CONSENSUS_DIRECTIVES = [
8439
8511
  "This task touches governed or high-stakes facts. BEFORE building tests around your central domain claim, run a multi-model consensus check on that claim (vo-mcp: vo_consensus_judgment or vo_verify_answer) and paste the verdict AND the tool result's receipt_id (a UUID; present when the cloud moat verified) into the PR body as `receipt id: <uuid>` \u2014 never invent one, and if the result has no receipt_id say so. A wrong governed fact caught at the claim stage costs one panel call; caught at the PR stage it costs the whole task; caught in production it costs a user.",
@@ -8864,7 +8936,7 @@ function selectDueEntries({ entries = [], now, alreadyDispatched = /* @__PURE__
8864
8936
  if (!code_task_id) continue;
8865
8937
  if (seen.has(code_task_id)) continue;
8866
8938
  const stableAttempts = Number(attemptsByKey[stableTaskKey(e)] || 0);
8867
- if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >= MAX_ATTEMPTS) {
8939
+ if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >= MAX_ATTEMPTS2) {
8868
8940
  exhausted.push(e);
8869
8941
  continue;
8870
8942
  }
@@ -8900,11 +8972,11 @@ function reconcileQueue({ entries = [], dispatchedIds = /* @__PURE__ */ new Set(
8900
8972
  return true;
8901
8973
  });
8902
8974
  }
8903
- var MAX_ATTEMPTS, MAX_DISPATCH_PER_RUN, NULL_RESUME_AFTER_BACKOFF_MS;
8975
+ var MAX_ATTEMPTS2, MAX_DISPATCH_PER_RUN, NULL_RESUME_AFTER_BACKOFF_MS;
8904
8976
  var init_rate_limit_resume_scheduler_core = __esm({
8905
8977
  "../../scripts/virtual-office/code-runner/rate-limit-resume-scheduler-core.mjs"() {
8906
8978
  "use strict";
8907
- MAX_ATTEMPTS = 3;
8979
+ MAX_ATTEMPTS2 = 3;
8908
8980
  MAX_DISPATCH_PER_RUN = 10;
8909
8981
  NULL_RESUME_AFTER_BACKOFF_MS = 15 * 60 * 1e3;
8910
8982
  }
@@ -8931,7 +9003,7 @@ function bumpAttempts(store, key, sourceTaskId, nowIso) {
8931
9003
  store[key] = {
8932
9004
  count: (typeof prior.count === "number" ? prior.count : 0) + 1,
8933
9005
  lastSeen: nowIso,
8934
- resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-MAX_ATTEMPTS)
9006
+ resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-MAX_ATTEMPTS2)
8935
9007
  };
8936
9008
  }
8937
9009
  function pruneAttemptsStore(store, nowIso) {
@@ -8983,7 +9055,7 @@ async function runLockedScheduler({
8983
9055
  await writeResumeAttempts(attemptsPath, pruneAttemptsStore(attemptsStore, nowIso));
8984
9056
  await writeResumeQueue(queuePath, kept);
8985
9057
  if (exhausted.length > 0) {
8986
- 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(", ")}`);
9058
+ 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(", ")}`);
8987
9059
  }
8988
9060
  if (due.length >= MAX_DISPATCH_PER_RUN) {
8989
9061
  log2(`WARN: hit the per-run dispatch cap (${MAX_DISPATCH_PER_RUN}); more tasks remain queued`);
@@ -10279,6 +10351,31 @@ var init_agent_unfixable_checks = __esm({
10279
10351
  }
10280
10352
  });
10281
10353
 
10354
+ // ../../scripts/virtual-office/code-runner/check-run-latest.mjs
10355
+ function latestCheckRunsByName(rollup) {
10356
+ if (!Array.isArray(rollup)) return [];
10357
+ const passthrough = [];
10358
+ const latest = /* @__PURE__ */ new Map();
10359
+ for (const entry of rollup) {
10360
+ if (!entry || typeof entry !== "object") continue;
10361
+ const isCheckRun = Boolean(String(entry.conclusion || "")) || Boolean(entry.status);
10362
+ const name = isCheckRun && entry.name ? String(entry.name) : "";
10363
+ if (!name) {
10364
+ passthrough.push(entry);
10365
+ continue;
10366
+ }
10367
+ const startedAt = String(entry.startedAt || entry.started_at || "");
10368
+ const held = latest.get(name);
10369
+ if (!held || startedAt >= held.startedAt) latest.set(name, { startedAt, entry });
10370
+ }
10371
+ return [...passthrough, ...[...latest.values()].map((h) => h.entry)];
10372
+ }
10373
+ var init_check_run_latest = __esm({
10374
+ "../../scripts/virtual-office/code-runner/check-run-latest.mjs"() {
10375
+ "use strict";
10376
+ }
10377
+ });
10378
+
10282
10379
  // ../../scripts/virtual-office/code-runner/ci-repair-evidence.mjs
10283
10380
  function extractWorkflowRunIds(links = []) {
10284
10381
  const ids = [];
@@ -10409,39 +10506,6 @@ var init_pr_watcher_failure_confirmation = __esm({
10409
10506
  }
10410
10507
  });
10411
10508
 
10412
- // ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
10413
- function supersededSourcePrNumber(prompt) {
10414
- const text = String(prompt || "");
10415
- if (text.includes(CI_FIX_MARKER)) {
10416
- const match = text.match(/\bPR:\s*#(\d+)\b/u);
10417
- return match ? Number(match[1]) : null;
10418
- }
10419
- const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
10420
- if (repairMatch) return Number(repairMatch[1]);
10421
- const recoverySupersedeMatch = text.match(
10422
- /^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
10423
- );
10424
- if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
10425
- const restoredContextMatch = text.match(
10426
- /^(?: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
10427
- );
10428
- if (restoredContextMatch) {
10429
- return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
10430
- }
10431
- const continuationMatch = text.match(
10432
- /^(?: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
10433
- );
10434
- if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
10435
- return Number(continuationMatch[1]);
10436
- }
10437
- var CI_FIX_MARKER;
10438
- var init_superseded_pr_source = __esm({
10439
- "../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
10440
- "use strict";
10441
- CI_FIX_MARKER = "[VO-CI-FIX]";
10442
- }
10443
- });
10444
-
10445
10509
  // ../../scripts/virtual-office/code-runner/error-message.mjs
10446
10510
  function boundedErrorMessage(error, maxLength = 240) {
10447
10511
  try {
@@ -10476,9 +10540,52 @@ function mergeEnqueueActive(entry, nowMs = Date.now()) {
10476
10540
  if (!at) return true;
10477
10541
  return nowMs - at < MERGE_ENQUEUE_TTL_MS;
10478
10542
  }
10543
+ function migrateLegacyMergeState(entry, nowMs = Date.now()) {
10544
+ if (!entry) return false;
10545
+ if (entry.mergeEnqueued === true && !(Number(entry.mergeEnqueuedAt) > 0)) {
10546
+ entry.mergeEnqueuedAt = nowMs;
10547
+ return true;
10548
+ }
10549
+ const exhausted = entry.mergeRetryExhausted === true || (Number(entry.mergeAttempts) || 0) >= 3 || (Number(entry.mergeErrors) || 0) >= 3;
10550
+ if (entry.mergeTerminal === true || mergeEnqueueActive(entry, nowMs) || !exhausted) return false;
10551
+ entry.mergeRetryExhausted = true;
10552
+ entry.mergeTerminal = true;
10553
+ entry.mergeBlockReason ||= "Legacy watcher state exhausted 3 automatic merge attempts without a terminal result.";
10554
+ return true;
10555
+ }
10479
10556
  function isTerminalResumeRefusal(error) {
10480
10557
  return typeof error?.code === "string" && TERMINAL_RESUME_REFUSALS.includes(error.code);
10481
10558
  }
10559
+ async function reportPendingMergeExhaustion({
10560
+ entry,
10561
+ now,
10562
+ reportBlocker,
10563
+ taskId,
10564
+ prNumber,
10565
+ log: log2 = () => {
10566
+ }
10567
+ }) {
10568
+ if (!entry.mergeRetryExhausted || entry.mergeEscalatedAt || typeof reportBlocker !== "function") return false;
10569
+ const lastError = boundedErrorMessage(entry.mergeRetryReason || entry.mergeBlockReason);
10570
+ try {
10571
+ await reportBlocker({
10572
+ taskId,
10573
+ message: `PR #${prNumber} automatic merge verification exhausted its bounded retry budget; operator review is required. Last error: ${lastError}`
10574
+ });
10575
+ entry.mergeEscalatedAt = now();
10576
+ return true;
10577
+ } catch (reportError) {
10578
+ log2(`watch: pr #${prNumber} merge escalation failed; terminal state retained: ${boundedErrorMessage(reportError)}`);
10579
+ return false;
10580
+ }
10581
+ }
10582
+ async function scheduleMergeRetry(input) {
10583
+ await scheduleCoordinationRetry({ ...input, kind: "merge" });
10584
+ return {
10585
+ terminal: input.entry.mergeTerminal === true,
10586
+ escalated: await reportPendingMergeExhaustion(input)
10587
+ };
10588
+ }
10482
10589
  async function scheduleCoordinationRetry({
10483
10590
  entry,
10484
10591
  kind,
@@ -10497,10 +10604,21 @@ async function scheduleCoordinationRetry({
10497
10604
  log2(`watch: pr #${prNumber} automatic continuation refused by the plane (${error.code}); no retry \u2014 operator may resume explicitly with a larger cap`);
10498
10605
  return;
10499
10606
  }
10500
- const errorsKey = kind === "resume" ? "resumeErrors" : "enqueueErrors";
10501
- const attemptsKey = kind === "resume" ? "resumeAttempts" : "fixAttempts";
10607
+ const errorsKey = kind === "resume" ? "resumeErrors" : kind === "merge" ? "mergeErrors" : "enqueueErrors";
10608
+ const attemptsKey = kind === "resume" ? "resumeAttempts" : kind === "merge" ? "mergeAttempts" : "fixAttempts";
10502
10609
  entry[errorsKey] = (entry[errorsKey] || 0) + 1;
10503
- entry[attemptsKey] = Math.max(0, (entry[attemptsKey] || 1) - 1);
10610
+ if (kind === "merge") {
10611
+ entry.mergeRetryReason = boundedErrorMessage(error);
10612
+ if (entry[errorsKey] >= 3) {
10613
+ entry.mergeTerminal = true;
10614
+ entry.mergeRetryExhausted = true;
10615
+ entry.mergeBlockReason = `Automatic merge verification remained unavailable after 3 attempts: ${entry.mergeRetryReason}`;
10616
+ delete entry.nextRetryAt;
10617
+ log2(`watch: pr #${prNumber} merge retry cap reached \u2014 terminal and operator review required`);
10618
+ return;
10619
+ }
10620
+ }
10621
+ if (kind !== "merge") entry[attemptsKey] = Math.max(0, (entry[attemptsKey] || 1) - 1);
10504
10622
  const delay2 = Math.min(MAX_BACKOFF_MS2, 3e4 * 2 ** Math.min(7, entry[errorsKey] - 1));
10505
10623
  entry.nextRetryAt = now() + delay2;
10506
10624
  if (entry[errorsKey] >= 3 && !entry.coordinationEscalatedAt && typeof reportBlocker === "function") {
@@ -10609,16 +10727,87 @@ var init_watcher_state = __esm({
10609
10727
  function normalizedRepo(repo) {
10610
10728
  return String(repo || "").trim().toLowerCase();
10611
10729
  }
10730
+ function present(value) {
10731
+ return value !== null && value !== void 0 && value !== "";
10732
+ }
10612
10733
  function watcherKey(state, repo, prNumber) {
10734
+ const targetRepo = normalizedRepo(repo);
10735
+ for (const [key, entry] of Object.entries(state)) {
10736
+ if (watcherPrNumber(key, entry) !== Number(prNumber)) continue;
10737
+ if (normalizedRepo(entry?.repo) === targetRepo) return key;
10738
+ }
10613
10739
  const legacy = String(prNumber);
10614
10740
  const existing = state[legacy];
10615
- if (!existing || normalizedRepo(existing.repo) === normalizedRepo(repo)) return legacy;
10616
- return `${normalizedRepo(repo)}#${prNumber}`;
10741
+ if (!existing) return legacy;
10742
+ return `${targetRepo}#${prNumber}`;
10617
10743
  }
10618
10744
  function watcherPrNumber(stateKey, entry) {
10619
10745
  const value = Number(entry?.prNumber ?? String(stateKey).split("#").at(-1));
10620
10746
  return Number.isInteger(value) && value > 0 ? value : null;
10621
10747
  }
10748
+ function collapseDuplicateWatcherEntries(state) {
10749
+ const keeperBySubject = /* @__PURE__ */ new Map();
10750
+ let collapsed = 0;
10751
+ for (const [key, entry] of Object.entries(state)) {
10752
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
10753
+ const repo = normalizedRepo(entry.repo);
10754
+ const prNumber = watcherPrNumber(key, entry);
10755
+ if (!repo || !prNumber) continue;
10756
+ const subject = `${repo}#${prNumber}`;
10757
+ const keeperKey = keeperBySubject.get(subject);
10758
+ if (!keeperKey) {
10759
+ keeperBySubject.set(subject, key);
10760
+ continue;
10761
+ }
10762
+ const keeper = state[keeperKey];
10763
+ let identityConflict = false;
10764
+ for (const field of ["taskId", "operatorId", "tenantId"]) {
10765
+ const current = keeper[field];
10766
+ const duplicate = entry[field];
10767
+ if (!present(current) && present(duplicate)) keeper[field] = duplicate;
10768
+ else if (present(current) && present(duplicate) && current !== duplicate) identityConflict = true;
10769
+ }
10770
+ if (keeper.operatorId === "admin" && present(keeper.tenantId)) identityConflict = true;
10771
+ for (const field of ["fixAttempts", "mergeAttempts", "mergeErrors", "resumeAttempts"]) {
10772
+ const current = Number(keeper[field]);
10773
+ const duplicate = Number(entry[field]);
10774
+ const total = (Number.isSafeInteger(current) && current > 0 ? current : 0) + (Number.isSafeInteger(duplicate) && duplicate > 0 ? duplicate : 0);
10775
+ keeper[field] = Math.min(Number.MAX_SAFE_INTEGER, total);
10776
+ }
10777
+ for (const [field, value] of Object.entries(entry)) {
10778
+ if (["taskId", "operatorId", "tenantId"].includes(field)) continue;
10779
+ if (!present(keeper[field]) && present(value)) keeper[field] = value;
10780
+ }
10781
+ if (entry.allowFixDispatch === false) keeper.allowFixDispatch = false;
10782
+ const continuationExhausted = keeper.continuationExhausted === true || entry.continuationExhausted === true;
10783
+ keeper.continuationExhausted = continuationExhausted;
10784
+ keeper.needsContinuation = !continuationExhausted && (keeper.needsContinuation === true || entry.needsContinuation === true);
10785
+ if (entry.mergeEnqueued === true) {
10786
+ keeper.mergeEnqueued = true;
10787
+ if ((Number(entry.mergeEnqueuedAt) || 0) > (Number(keeper.mergeEnqueuedAt) || 0)) {
10788
+ keeper.mergeEnqueuedAt = entry.mergeEnqueuedAt;
10789
+ keeper.mergeActionReceiptId = entry.mergeActionReceiptId ?? null;
10790
+ }
10791
+ }
10792
+ if (entry.mergeTerminal === true && keeper.mergeTerminal !== true) {
10793
+ keeper.mergeTerminal = true;
10794
+ keeper.mergeBlockReason = entry.mergeBlockReason || "verification refused";
10795
+ }
10796
+ if ((Number(keeper.mergeErrors) || 0) >= 3) {
10797
+ keeper.mergeRetryExhausted = true;
10798
+ keeper.mergeTerminal = true;
10799
+ keeper.mergeBlockReason ||= `Duplicate watcher history exhausted ${keeper.mergeErrors} automatic merge retries.`;
10800
+ }
10801
+ if (identityConflict) {
10802
+ keeper.automationContextConflict = true;
10803
+ keeper.mergeTerminal = true;
10804
+ keeper.mergeBlockReason = "Duplicate watcher entries disagree on automation attribution.";
10805
+ }
10806
+ delete state[key];
10807
+ collapsed += 1;
10808
+ }
10809
+ return collapsed;
10810
+ }
10622
10811
  function deleteWatcherEntry(state, repo, prNumber) {
10623
10812
  const targetRepo = normalizedRepo(repo);
10624
10813
  for (const [key, entry] of Object.entries(state)) {
@@ -10633,14 +10822,56 @@ var init_watcher_key = __esm({
10633
10822
  }
10634
10823
  });
10635
10824
 
10825
+ // ../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs
10826
+ function nonEmptyString(value) {
10827
+ return typeof value === "string" && value.length > 0;
10828
+ }
10829
+ function resolveWatcherMergeAuthority(entry = {}) {
10830
+ if (entry.automationContextConflict === true) {
10831
+ return {
10832
+ kind: "blocked",
10833
+ reason: "Watcher automation attribution conflicts with the durable code task."
10834
+ };
10835
+ }
10836
+ if (String(entry.repo || "").trim().toLowerCase() !== CANONICAL_WATCHER_MERGE_REPO) {
10837
+ return {
10838
+ kind: "blocked",
10839
+ reason: "Watcher merge target is not the canonical Nexus repository."
10840
+ };
10841
+ }
10842
+ const taskId = entry.taskId;
10843
+ const operatorId = entry.operatorId;
10844
+ const tenantId = entry.tenantId;
10845
+ if (nonEmptyString(taskId) && operatorId === "admin" && (tenantId === null || tenantId === void 0)) {
10846
+ return { kind: "legacy-admin", context: void 0 };
10847
+ }
10848
+ if (nonEmptyString(taskId) && nonEmptyString(operatorId) && nonEmptyString(tenantId)) {
10849
+ return { kind: "scoped", context: { taskId, operatorId, tenantId } };
10850
+ }
10851
+ return {
10852
+ kind: "blocked",
10853
+ reason: INCOMPLETE_WATCHER_AUTHORITY_REASON
10854
+ };
10855
+ }
10856
+ var INCOMPLETE_WATCHER_AUTHORITY_REASON, CANONICAL_WATCHER_MERGE_REPO;
10857
+ var init_watcher_merge_authority = __esm({
10858
+ "../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs"() {
10859
+ "use strict";
10860
+ INCOMPLETE_WATCHER_AUTHORITY_REASON = "Watcher automation attribution is incomplete; merge was not attempted.";
10861
+ CANONICAL_WATCHER_MERGE_REPO = "algosuite-ai/nexus";
10862
+ }
10863
+ });
10864
+
10636
10865
  // ../../scripts/virtual-office/code-runner/watcher-adoption.mjs
10637
10866
  async function adoptPrOpenedTasks(tasks, {
10638
10867
  stateFile,
10639
10868
  now = () => Date.now(),
10640
10869
  servedRepos = [],
10641
10870
  servedOperators = [],
10871
+ // $5 mirrors pr-watcher.mjs / daemon-config.mjs after the 2026-08-21/22 `error_max_budget_usd`
10872
+ // kills (#9967 $2.41, #9968/#9969 ~$1.65); makeWatchRunner always passes its own value.
10642
10873
  repairChainMax = 3,
10643
- repairBudgetUsd = 1
10874
+ repairBudgetUsd = 5
10644
10875
  }) {
10645
10876
  const repos = new Set(servedRepos.map((repo) => repo.toLowerCase()));
10646
10877
  const operators = new Set(servedOperators);
@@ -10651,7 +10882,38 @@ async function adoptPrOpenedTasks(tasks, {
10651
10882
  if (repos.size > 0 && !repos.has(task.repo.toLowerCase())) continue;
10652
10883
  if (operators.size > 0 && !operators.has(task.operator_id)) continue;
10653
10884
  const key = watcherKey(state, task.repo, task.pr_number);
10654
- if (state[key]) continue;
10885
+ const existing = state[key];
10886
+ if (existing) {
10887
+ let changed = false;
10888
+ let conflict = false;
10889
+ for (const [field, durable] of [
10890
+ ["taskId", task.code_task_id],
10891
+ ["operatorId", task.operator_id],
10892
+ ["tenantId", task.tenant_id]
10893
+ ]) {
10894
+ const comparable = durable === null || typeof durable === "string" && durable.length > 0;
10895
+ if (!comparable) continue;
10896
+ const current = existing[field];
10897
+ if ((current === null || current === void 0 || current === "") && durable !== null) {
10898
+ existing[field] = durable;
10899
+ changed = true;
10900
+ } else if (current !== null && current !== void 0 && current !== "" && current !== durable) {
10901
+ conflict = true;
10902
+ }
10903
+ }
10904
+ if (conflict && existing.automationContextConflict !== true) {
10905
+ existing.automationContextConflict = true;
10906
+ changed = true;
10907
+ }
10908
+ if (!conflict && existing.mergeTerminal === true && existing.mergeBlockReason === INCOMPLETE_WATCHER_AUTHORITY_REASON && resolveWatcherMergeAuthority(existing).kind !== "blocked") {
10909
+ delete existing.mergeTerminal;
10910
+ delete existing.mergeBlockReason;
10911
+ existing.mergeAttempts = 0;
10912
+ changed = true;
10913
+ }
10914
+ if (changed) adopted += 1;
10915
+ continue;
10916
+ }
10655
10917
  const partial = String(task.result || "").includes(PARTIAL_PR_CONTINUATION_MARKER);
10656
10918
  const repairChain = task.repair_chain ?? {
10657
10919
  root_pr_number: task.repair_pr_number ?? task.pr_number,
@@ -10687,6 +10949,7 @@ var init_watcher_adoption = __esm({
10687
10949
  init_watcher_state();
10688
10950
  init_partial_pr_continuation();
10689
10951
  init_watcher_key();
10952
+ init_watcher_merge_authority();
10690
10953
  }
10691
10954
  });
10692
10955
 
@@ -10718,6 +10981,39 @@ var init_watcher_github_token = __esm({
10718
10981
  }
10719
10982
  });
10720
10983
 
10984
+ // ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
10985
+ function supersededSourcePrNumber(prompt) {
10986
+ const text = String(prompt || "");
10987
+ if (text.includes(CI_FIX_MARKER)) {
10988
+ const match = text.match(/\bPR:\s*#(\d+)\b/u);
10989
+ return match ? Number(match[1]) : null;
10990
+ }
10991
+ const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
10992
+ if (repairMatch) return Number(repairMatch[1]);
10993
+ const recoverySupersedeMatch = text.match(
10994
+ /^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
10995
+ );
10996
+ if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
10997
+ const restoredContextMatch = text.match(
10998
+ /^(?: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
10999
+ );
11000
+ if (restoredContextMatch) {
11001
+ return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
11002
+ }
11003
+ const continuationMatch = text.match(
11004
+ /^(?: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
11005
+ );
11006
+ if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
11007
+ return Number(continuationMatch[1]);
11008
+ }
11009
+ var CI_FIX_MARKER;
11010
+ var init_superseded_pr_source = __esm({
11011
+ "../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
11012
+ "use strict";
11013
+ CI_FIX_MARKER = "[VO-CI-FIX]";
11014
+ }
11015
+ });
11016
+
10721
11017
  // ../../scripts/virtual-office/code-runner/ci-fix-prompt.mjs
10722
11018
  function buildCiFixPrompt({ prNumber, repo, branch, headSha, failedChecks, prPatch, failedLogs }) {
10723
11019
  return [
@@ -10914,7 +11210,7 @@ import { homedir as homedir9 } from "node:os";
10914
11210
  import { join as join14 } from "node:path";
10915
11211
  function parsePrCiStatus(view) {
10916
11212
  const state = (view && typeof view.state === "string" ? view.state : "UNKNOWN").toUpperCase();
10917
- const rollup = view && Array.isArray(view.statusCheckRollup) ? view.statusCheckRollup : [];
11213
+ const rollup = latestCheckRunsByName(view && Array.isArray(view.statusCheckRollup) ? view.statusCheckRollup : []);
10918
11214
  const failedChecks = [];
10919
11215
  const failedCheckLinks = [];
10920
11216
  let pending = false;
@@ -10999,6 +11295,7 @@ async function runWatchCycleUnlocked({
10999
11295
  stateFile = DEFAULT_STATE_FILE
11000
11296
  }) {
11001
11297
  const state = await readWatcherState(stateFile);
11298
+ collapseDuplicateWatcherEntries(state);
11002
11299
  const prNumbers = Object.keys(state);
11003
11300
  let checked = 0;
11004
11301
  let fixed = 0;
@@ -11024,6 +11321,7 @@ async function runWatchCycleUnlocked({
11024
11321
  }
11025
11322
  checked += 1;
11026
11323
  const pr = parsePrCiStatus(view);
11324
+ migrateLegacyMergeState(entry, now());
11027
11325
  entry.lastCi = pr.ci;
11028
11326
  const confirmation = observeRepairFailure(entry, pr, now());
11029
11327
  const proposedAction = decideWatchAction(
@@ -11094,10 +11392,30 @@ async function runWatchCycleUnlocked({
11094
11392
  mergeBlocked += 1;
11095
11393
  log2(`watch: pr #${prNumber} verification blocked merge: ${entry.mergeBlockReason}`);
11096
11394
  } else {
11097
- log2(`watch: pr #${prNumber} verification unavailable; retry ${entry.mergeAttempts}/3`);
11395
+ const retry = await scheduleMergeRetry({
11396
+ entry,
11397
+ now,
11398
+ reportBlocker,
11399
+ taskId: entry.taskId,
11400
+ prNumber,
11401
+ error: new Error(outcome.reason || "verification unavailable"),
11402
+ log: log2
11403
+ });
11404
+ if (retry.terminal) mergeBlocked += 1;
11405
+ if (retry.escalated) escalated += 1;
11098
11406
  }
11099
11407
  } catch (err) {
11100
- log2(`watch: pr #${prNumber} gated merge failed (${entry.mergeAttempts}/3): ${err.message}`);
11408
+ const retry = await scheduleMergeRetry({
11409
+ entry,
11410
+ now,
11411
+ reportBlocker,
11412
+ taskId: entry.taskId,
11413
+ prNumber,
11414
+ error: err,
11415
+ log: log2
11416
+ });
11417
+ if (retry.terminal) mergeBlocked += 1;
11418
+ if (retry.escalated) escalated += 1;
11101
11419
  }
11102
11420
  } else if (action === "fix") {
11103
11421
  entry.fixAttempts = (entry.fixAttempts || 0) + 1;
@@ -11130,6 +11448,7 @@ async function runWatchCycleUnlocked({
11130
11448
  }
11131
11449
  } else {
11132
11450
  entry.lastCheckedAt = now();
11451
+ if (await reportPendingMergeExhaustion({ entry, now, reportBlocker, taskId: entry.taskId, prNumber, log: log2 })) escalated += 1;
11133
11452
  const repairCapped = pr.ci === "failing" && ((entry.fixAttempts || 0) >= maxFixAttempts || entry.allowFixDispatch === false);
11134
11453
  if ((repairCapped || entry.continuationExhausted) && !entry.escalatedAt) {
11135
11454
  try {
@@ -11163,7 +11482,11 @@ function makeWatchRunner({
11163
11482
  servedRepos = [],
11164
11483
  servedOperators = [],
11165
11484
  repairChainMax = 3,
11166
- repairBudgetUsd = 1,
11485
+ // $5, not $1: measured kills 2026-08-21/22 — #9967 ($2.41), #9968/#9969 (~$1.65 each) died
11486
+ // `error_max_budget_usd` with the fix finished but unpublished; the repair that landed (#9984)
11487
+ // cost $2.36. The daemon normally passes cfg.watchRepairBudgetUsd (daemon-config.mjs), which
11488
+ // carries the same default; this is the fallback when the watcher is built without one.
11489
+ repairBudgetUsd = 5,
11167
11490
  allowAmbientGithub = false,
11168
11491
  // Default ON (operator directive 2026-07-24); VO_CODE_RUNNER_ARM_AUTOMERGE=0 opts out.
11169
11492
  autoMergeEnabled = process.env.VO_CODE_RUNNER_ARM_AUTOMERGE !== "0"
@@ -11183,7 +11506,7 @@ function makeWatchRunner({
11183
11506
  repairChainMax,
11184
11507
  repairBudgetUsd
11185
11508
  });
11186
- if (adopted > 0) log2(`watch: adopted ${adopted} open task PR(s) from the control plane`);
11509
+ if (adopted > 0) log2(`watch: adopted or reconciled ${adopted} open task PR(s) from the control plane`);
11187
11510
  return runWatchCycle({
11188
11511
  viewPr: watchView,
11189
11512
  enqueueFix: async ({ prNumber, repo, branch, headSha, failedChecks, failedCheckLinks, repairChain, operatorId }) => {
@@ -11225,13 +11548,10 @@ function makeWatchRunner({
11225
11548
  return result;
11226
11549
  },
11227
11550
  mergePr: (prNumber, entry) => {
11228
- const context = {
11229
- taskId: entry.taskId,
11230
- operatorId: entry.operatorId,
11231
- tenantId: entry.tenantId
11232
- };
11233
- const complete = Object.values(context).every((value) => typeof value === "string" && value.length > 0);
11234
- return client.mergeVerifiedPr(prNumber, complete ? context : void 0);
11551
+ const authority = resolveWatcherMergeAuthority(entry);
11552
+ if (authority.kind === "blocked")
11553
+ return Promise.resolve({ status: "blocked", reason: authority.reason });
11554
+ return client.mergeVerifiedPr(prNumber, authority.context);
11235
11555
  },
11236
11556
  log: log2,
11237
11557
  maxFixAttempts,
@@ -11245,9 +11565,9 @@ var init_pr_watcher = __esm({
11245
11565
  "../../scripts/virtual-office/code-runner/pr-watcher.mjs"() {
11246
11566
  "use strict";
11247
11567
  init_agent_unfixable_checks();
11568
+ init_check_run_latest();
11248
11569
  init_ci_repair_evidence();
11249
11570
  init_pr_watcher_failure_confirmation();
11250
- init_superseded_pr_source();
11251
11571
  init_watcher_coordination();
11252
11572
  init_watcher_adoption();
11253
11573
  init_watcher_key();
@@ -11256,6 +11576,7 @@ var init_pr_watcher = __esm({
11256
11576
  init_pr_watcher_github();
11257
11577
  init_enqueue_autonomous_code_task();
11258
11578
  init_watcher_state();
11579
+ init_watcher_merge_authority();
11259
11580
  init_superseded_pr_source();
11260
11581
  init_ci_fix_prompt();
11261
11582
  DEFAULT_STATE_FILE = join14(homedir9(), ".vo", "dispatched-prs.json");
@@ -13066,9 +13387,16 @@ function makeSafeProgress(log2) {
13066
13387
  function runnerStagePatch(stage, message, extra = {}) {
13067
13388
  return { stage, message, ...extra };
13068
13389
  }
13390
+ function preservedReceiptLines(run, slicedBodyText) {
13391
+ const full = `${String(run?.summary || "")}
13392
+ ${String(run?.lastAgentMessage || "")}`;
13393
+ const found = [...new Set((full.match(RECEIPT_LINE_RE) || []).map((line) => line.trim()))];
13394
+ const missing = found.filter((line) => !String(slicedBodyText || "").includes(line));
13395
+ return missing.length ? ["", "### Consensus evidence (preserved past truncation)", "", ...missing.map((line) => redactSecrets(line))] : [];
13396
+ }
13069
13397
  function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
13070
13398
  const governedStakes = matchGovernedStakes({ prompt: String(task.prompt || "") });
13071
- return [
13399
+ const slicedSections = [
13072
13400
  "## AlgoHQ Command Center \u2014 Code-from-Anywhere task",
13073
13401
  "",
13074
13402
  `- **Task:** \`${task.code_task_id}\``,
@@ -13091,7 +13419,12 @@ function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
13091
13419
  "",
13092
13420
  // A budget/turn-capped run ends with no assistant text (summary = the bare
13093
13421
  // subtype); its LAST message is the honest report the operator needs.
13094
- ...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : [],
13422
+ ...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : []
13423
+ ];
13424
+ return [
13425
+ ...slicedSections,
13426
+ // Receipt lines the slices dropped — the gate reads only this body.
13427
+ ...preservedReceiptLines(run, slicedSections.join("\n")),
13095
13428
  "---",
13096
13429
  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._"
13097
13430
  ].filter((l) => l !== "").join("\n");
@@ -13109,11 +13442,13 @@ async function mintRunnerGithubTokens({ client, taskId, log: log2, repo = null,
13109
13442
  if (!agentReadToken) log2(`task ${taskId}: no GitHub read access for the agent (${reason}); it cannot read a private repo`);
13110
13443
  return { publishToken, agentReadToken };
13111
13444
  }
13445
+ var RECEIPT_LINE_RE;
13112
13446
  var init_task_helpers = __esm({
13113
13447
  "../../scripts/virtual-office/code-runner/task-helpers.mjs"() {
13114
13448
  "use strict";
13115
13449
  init_redact_tokens();
13116
13450
  init_methodology_composer();
13451
+ 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;
13117
13452
  }
13118
13453
  });
13119
13454
 
@@ -14275,7 +14610,7 @@ async function finalizePublishedPr({
14275
14610
  root_pr_number: pr.prNumber,
14276
14611
  attempt: 0,
14277
14612
  max_attempts: cfg.watchRepairChainMax ?? 3,
14278
- per_attempt_budget_usd: cfg.watchRepairBudgetUsd ?? 1
14613
+ per_attempt_budget_usd: cfg.watchRepairBudgetUsd ?? 5
14279
14614
  },
14280
14615
  // A CI-fix task, or a DRAFT published only because the local overlap gate
14281
14616
  // blocked it, must not spend repair attempts: the overlap resolves when the
@@ -14312,7 +14647,7 @@ async function finalizePublishedPr({
14312
14647
  root_pr_number: pr.prNumber,
14313
14648
  attempt: 0,
14314
14649
  max_attempts: cfg.watchRepairChainMax ?? 3,
14315
- per_attempt_budget_usd: cfg.watchRepairBudgetUsd ?? 1
14650
+ per_attempt_budget_usd: cfg.watchRepairBudgetUsd ?? 5
14316
14651
  },
14317
14652
  ...fixDispatchGuard
14318
14653
  });
@@ -15137,7 +15472,10 @@ function loadCodeRunnerConfig(env2 = process.env, { log: log2 = () => {
15137
15472
  watchEnabled: env2.VO_CODE_RUNNER_WATCH !== "0",
15138
15473
  watchMaxFix: Math.max(0, Number(env2.VO_CODE_RUNNER_WATCH_MAX_FIX ?? 1) || 0),
15139
15474
  watchRepairChainMax: Math.max(1, Math.min(10, Number(env2.VO_CODE_RUNNER_REPAIR_CHAIN_MAX ?? 3) || 3)),
15140
- watchRepairBudgetUsd: Math.max(0.25, Math.min(10, Number(env2.VO_CODE_RUNNER_REPAIR_BUDGET_USD ?? 1) || 1)),
15475
+ // $5 default (2026-08-22): at $1 real repairs died `error_max_budget_usd` a few percent short —
15476
+ // #9967 ($2.41), #9968/#9969 (~$1.65 each) left finished fixes unpublished as dead PARTIAL
15477
+ // drafts; the repair that landed (#9984) cost $2.36. Ceiling/override behaviour unchanged.
15478
+ watchRepairBudgetUsd: Math.max(0.25, Math.min(10, Number(env2.VO_CODE_RUNNER_REPAIR_BUDGET_USD ?? 5) || 5)),
15141
15479
  watchIntervalSec: Math.max(30, Number(env2.VO_CODE_RUNNER_WATCH_SEC ?? 60) || 60),
15142
15480
  armAutoMerge: env2.VO_CODE_RUNNER_ARM_AUTOMERGE !== "0",
15143
15481
  controlEnabled: env2.VO_CODE_RUNNER_CONTROL !== "0",