@algosuite/vo-mcp 0.2.0-beta.7 → 0.2.0-beta.8

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.
@@ -1297,15 +1297,41 @@ var init_pnpm_hydration = __esm({
1297
1297
  }
1298
1298
  });
1299
1299
 
1300
- // src/runner/worktree-cleanup.mjs
1300
+ // src/runner/worktree-paths.mjs
1301
1301
  import { createHash as createHash2 } from "node:crypto";
1302
- import fs3 from "node:fs";
1303
- import fsp6 from "node:fs/promises";
1304
1302
  import path5 from "node:path";
1305
- function worktreeDirForName(root, worktreeName) {
1303
+ function samePath(left, right) {
1304
+ const a = path5.resolve(left);
1305
+ const b = path5.resolve(right);
1306
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
1307
+ }
1308
+ function worktreePoolForRoot(root, { clonesRootDir = process.env.VO_CODE_RUNNER_CLONES_ROOT || "" } = {}) {
1309
+ const canonicalRoot = path5.resolve(root);
1310
+ if (clonesRootDir) {
1311
+ const clonePool = path5.resolve(clonesRootDir);
1312
+ if (samePath(path5.dirname(canonicalRoot), clonePool)) {
1313
+ return path5.join(clonePool, ".agent-worktrees", path5.basename(canonicalRoot));
1314
+ }
1315
+ }
1316
+ return path5.join(canonicalRoot, ".agent-worktrees");
1317
+ }
1318
+ function worktreeDirForName(root, worktreeName, options = {}) {
1306
1319
  const leaf = createHash2("sha256").update(String(worktreeName)).digest("hex").slice(0, 16);
1307
- return path5.join(root, ".agent-worktrees", leaf);
1320
+ return path5.join(worktreePoolForRoot(root, options), leaf);
1308
1321
  }
1322
+ function recoveryLedgerPathForRoot(root, options = {}) {
1323
+ return path5.join(worktreePoolForRoot(root, options), "recovery-ledger.jsonl");
1324
+ }
1325
+ var init_worktree_paths = __esm({
1326
+ "src/runner/worktree-paths.mjs"() {
1327
+ "use strict";
1328
+ }
1329
+ });
1330
+
1331
+ // src/runner/worktree-cleanup.mjs
1332
+ import fs3 from "node:fs";
1333
+ import fsp6 from "node:fs/promises";
1334
+ import path6 from "node:path";
1309
1335
  function stateFromEntry(entry) {
1310
1336
  return {
1311
1337
  root: entry.root,
@@ -1344,16 +1370,16 @@ function pruneSuccessfulStates(nowMs = Date.now()) {
1344
1370
  }
1345
1371
  }
1346
1372
  function assertTrackedCleanupPath(entry) {
1347
- const poolRoot = path5.join(entry.root, ".agent-worktrees");
1373
+ const poolRoot = worktreePoolForRoot(entry.root);
1348
1374
  const expected = worktreeDirForName(entry.root, entry.worktreeName);
1349
- const resolvedPool = path5.resolve(poolRoot);
1350
- const resolvedTarget = path5.resolve(entry.worktreeDir);
1351
- if (!resolvedTarget.startsWith(`${resolvedPool}${path5.sep}`)) {
1375
+ const resolvedPool = path6.resolve(poolRoot);
1376
+ const resolvedTarget = path6.resolve(entry.worktreeDir);
1377
+ if (!resolvedTarget.startsWith(`${resolvedPool}${path6.sep}`)) {
1352
1378
  const error = new Error(`cleanup refused outside managed pool: ${entry.worktreeDir}`);
1353
1379
  error.cleanupFatal = true;
1354
1380
  throw error;
1355
1381
  }
1356
- if (resolvedTarget !== path5.resolve(expected)) {
1382
+ if (resolvedTarget !== path6.resolve(expected)) {
1357
1383
  const error = new Error(`cleanup refused for unexpected tracked path: ${entry.worktreeDir}`);
1358
1384
  error.cleanupFatal = true;
1359
1385
  throw error;
@@ -1367,8 +1393,8 @@ async function worktreeStillRegistered2(root, worktreeDir, gitRunner) {
1367
1393
  if (result.status !== 0) {
1368
1394
  throw new Error(`git worktree list --porcelain failed during cleanup verification: ${summarizeProcessFailure(result)}`);
1369
1395
  }
1370
- const registered = String(result.stdout || "").split(/\r?\n/u).filter((line) => line.startsWith("worktree ")).map((line) => path5.resolve(line.slice("worktree ".length).trim()));
1371
- return registered.includes(path5.resolve(worktreeDir));
1396
+ const registered = String(result.stdout || "").split(/\r?\n/u).filter((line) => line.startsWith("worktree ")).map((line) => path6.resolve(line.slice("worktree ".length).trim()));
1397
+ return registered.includes(path6.resolve(worktreeDir));
1372
1398
  }
1373
1399
  function cleanupBackoff(attempt) {
1374
1400
  return 250 * attempt;
@@ -1478,7 +1504,7 @@ function scheduleTrackedCleanup(entry, options = {}) {
1478
1504
  }
1479
1505
  function pendingCleanupDirs() {
1480
1506
  return new Set(
1481
- [...CLEANUP_STATES.values()].filter((state) => state.status === "pending").map((state) => path5.resolve(state.worktreeDir))
1507
+ [...CLEANUP_STATES.values()].filter((state) => state.status === "pending").map((state) => path6.resolve(state.worktreeDir))
1482
1508
  );
1483
1509
  }
1484
1510
  var CLEANUP_STATES, CLEANUP_PROMISES, SUCCESS_HISTORY_LIMIT, SUCCESS_HISTORY_TTL_MS, DEFAULT_CLEANUP_ATTEMPTS;
@@ -1487,6 +1513,7 @@ var init_worktree_cleanup = __esm({
1487
1513
  "use strict";
1488
1514
  init_pnpm_link_detach();
1489
1515
  init_process_runner();
1516
+ init_worktree_paths();
1490
1517
  CLEANUP_STATES = /* @__PURE__ */ new Map();
1491
1518
  CLEANUP_PROMISES = /* @__PURE__ */ new Map();
1492
1519
  SUCCESS_HISTORY_LIMIT = 50;
@@ -1498,13 +1525,13 @@ var init_worktree_cleanup = __esm({
1498
1525
  // src/runner/task-root-prepare.mjs
1499
1526
  import fs4 from "node:fs";
1500
1527
  import fsp7 from "node:fs/promises";
1501
- import path6 from "node:path";
1528
+ import path7 from "node:path";
1502
1529
  function prepLockDir(root) {
1503
- return path6.join(root, ".agent-worktrees", "runner-root-prep.lock");
1530
+ return path7.join(root, ".agent-worktrees", "runner-root-prep.lock");
1504
1531
  }
1505
1532
  function readLockMeta(lockDir) {
1506
1533
  try {
1507
- return JSON.parse(fs4.readFileSync(path6.join(lockDir, "owner.json"), "utf8"));
1534
+ return JSON.parse(fs4.readFileSync(path7.join(lockDir, "owner.json"), "utf8"));
1508
1535
  } catch {
1509
1536
  return null;
1510
1537
  }
@@ -1524,9 +1551,9 @@ async function acquirePrepLock(root, options = {}) {
1524
1551
  const staleMs = options.lockStaleMs ?? PREP_LOCK_STALE_MS;
1525
1552
  const sleep3 = options.sleep || sleepMs;
1526
1553
  const lockDir = prepLockDir(root);
1527
- const ownerPath = path6.join(lockDir, "owner.json");
1554
+ const ownerPath = path7.join(lockDir, "owner.json");
1528
1555
  const deadline = nowMs() + waitMs;
1529
- fs4.mkdirSync(path6.dirname(lockDir), { recursive: true });
1556
+ fs4.mkdirSync(path7.dirname(lockDir), { recursive: true });
1530
1557
  for (; ; ) {
1531
1558
  try {
1532
1559
  fs4.mkdirSync(lockDir);
@@ -1574,10 +1601,104 @@ async function gitText(root, args, options = {}) {
1574
1601
  }
1575
1602
  return String(result.stdout || "").trim();
1576
1603
  }
1577
- async function alignCanonicalClone(root, options = {}) {
1604
+ function canonicalRecoveryDir(root, options = {}) {
1605
+ const now = options.now || (() => /* @__PURE__ */ new Date());
1606
+ return path7.join(
1607
+ options.managedPool || path7.join(root, ".agent-worktrees"),
1608
+ ".canonical-recovery",
1609
+ `preexisting-${now().toISOString().replace(/[:.]/gu, "-")}`
1610
+ );
1611
+ }
1612
+ function canonicalPath(root, relative) {
1613
+ const resolvedRoot = path7.resolve(root);
1614
+ const target = path7.resolve(root, relative);
1615
+ const prefix = `${resolvedRoot}${path7.sep}`;
1616
+ if (!target.startsWith(prefix)) {
1617
+ throw new Error(`canonical recovery path escaped the runner clone: ${relative}`);
1618
+ }
1619
+ return target;
1620
+ }
1621
+ async function changedCanonicalPaths(root, options = {}) {
1622
+ const [trackedResult, untrackedResult] = await Promise.all([
1623
+ git(root, ["-c", "core.quotepath=false", "diff", "--name-only", "-z", "HEAD"], options),
1624
+ git(root, ["-c", "core.quotepath=false", "ls-files", "--others", "--exclude-standard", "-z"], options)
1625
+ ]);
1626
+ if (trackedResult.status !== 0 || untrackedResult.status !== 0) {
1627
+ throw new Error("could not enumerate canonical clone residue before recovery");
1628
+ }
1629
+ return {
1630
+ tracked: splitZ(trackedResult.stdout),
1631
+ untracked: splitZ(untrackedResult.stdout)
1632
+ };
1633
+ }
1634
+ async function recoverManagedCanonicalResidue(root, options = {}) {
1635
+ const paths = await changedCanonicalPaths(root, options);
1636
+ const quarantineDir = canonicalRecoveryDir(root, options);
1637
+ const headSha = await gitText(root, ["rev-parse", "HEAD"], options);
1638
+ await fsp7.mkdir(quarantineDir, { recursive: true });
1639
+ const patchResult = await git(root, ["diff", "--binary", "HEAD"], options);
1640
+ if (patchResult.status !== 0) {
1641
+ throw new Error(`could not preserve canonical tracked changes: ${summarizeProcessFailure(patchResult)}`);
1642
+ }
1643
+ await fsp7.writeFile(path7.join(quarantineDir, "tracked.patch"), String(patchResult.stdout || ""), "utf8");
1644
+ const symlinks = [];
1645
+ for (const relative of paths.untracked) {
1646
+ const source = canonicalPath(root, relative);
1647
+ const stat = await fsp7.lstat(source);
1648
+ if (stat.isSymbolicLink()) {
1649
+ symlinks.push({ path: relative, target: await fsp7.readlink(source) });
1650
+ continue;
1651
+ }
1652
+ if (!stat.isFile()) {
1653
+ throw new Error(`canonical recovery refuses unsupported untracked entry: ${relative}`);
1654
+ }
1655
+ const target = canonicalPath(path7.join(quarantineDir, "untracked"), relative);
1656
+ await fsp7.mkdir(path7.dirname(target), { recursive: true });
1657
+ await fsp7.copyFile(source, target);
1658
+ }
1659
+ await fsp7.writeFile(path7.join(quarantineDir, "manifest.json"), `${JSON.stringify({
1660
+ recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
1661
+ canonicalRoot: path7.resolve(root),
1662
+ canonicalHead: headSha,
1663
+ tracked: paths.tracked,
1664
+ untracked: paths.untracked,
1665
+ symlinks
1666
+ }, null, 2)}
1667
+ `, "utf8");
1668
+ if (paths.tracked.length > 0) {
1669
+ const restored = await git(root, [
1670
+ "restore",
1671
+ `--source=${headSha}`,
1672
+ "--staged",
1673
+ "--worktree",
1674
+ "--",
1675
+ ...paths.tracked
1676
+ ], options);
1677
+ if (restored.status !== 0) {
1678
+ throw new Error(`could not restore canonical tracked changes; evidence: ${quarantineDir}`);
1679
+ }
1680
+ }
1681
+ for (const relative of paths.untracked) {
1682
+ await fsp7.rm(canonicalPath(root, relative), { force: true });
1683
+ }
1578
1684
  const status = await gitText(root, ["status", "--porcelain"], options);
1579
1685
  if (status) {
1580
- throw new Error(`canonical clone is dirty: ${status.split(/\r?\n/u, 1)[0]}`);
1686
+ throw new Error(`canonical clone recovery did not restore a clean tree; evidence: ${quarantineDir}`);
1687
+ }
1688
+ (options.logger || console.error)(
1689
+ `[vo-mcp runner] preserved and recovered ${paths.tracked.length + paths.untracked.length} preexisting canonical-clone write(s): ${quarantineDir}`
1690
+ );
1691
+ return { quarantineDir, ...paths };
1692
+ }
1693
+ async function alignCanonicalClone(root, options = {}) {
1694
+ let status = await gitText(root, ["status", "--porcelain"], options);
1695
+ if (status) {
1696
+ if (options.recoverDirtyCanonical !== true) {
1697
+ throw new Error(`canonical clone is dirty: ${status.split(/\r?\n/u, 1)[0]}`);
1698
+ }
1699
+ await recoverManagedCanonicalResidue(root, options);
1700
+ status = await gitText(root, ["status", "--porcelain"], options);
1701
+ if (status) throw new Error("canonical clone remained dirty after managed recovery");
1581
1702
  }
1582
1703
  const branch = await gitText(root, ["branch", "--show-current"], options);
1583
1704
  if (branch !== "main") {
@@ -1609,11 +1730,11 @@ async function registeredWorktreeDirs(root, options = {}) {
1609
1730
  throw new Error(`git worktree list --porcelain failed while checking managed residue: ${summarizeProcessFailure(listed)}`);
1610
1731
  }
1611
1732
  return new Set(
1612
- String(listed.stdout || "").split(/\r?\n/u).filter((line) => line.startsWith("worktree ")).map((line) => path6.resolve(line.slice("worktree ".length).trim()))
1733
+ String(listed.stdout || "").split(/\r?\n/u).filter((line) => line.startsWith("worktree ")).map((line) => path7.resolve(line.slice("worktree ".length).trim()))
1613
1734
  );
1614
1735
  }
1615
1736
  async function reportLegacyResiduals(root, options = {}) {
1616
- const managedRoot = path6.join(root, ".agent-worktrees");
1737
+ const managedRoot = path7.join(root, ".agent-worktrees");
1617
1738
  if (!fs4.existsSync(managedRoot)) return [];
1618
1739
  const registered = await registeredWorktreeDirs(root, options);
1619
1740
  const pending = pendingCleanupDirs();
@@ -1621,7 +1742,7 @@ async function reportLegacyResiduals(root, options = {}) {
1621
1742
  for (const entry of fs4.readdirSync(managedRoot, { withFileTypes: true })) {
1622
1743
  if (!entry.isDirectory()) continue;
1623
1744
  if (shouldIgnoreManagedEntry(entry.name)) continue;
1624
- const absolute = path6.resolve(path6.join(managedRoot, entry.name));
1745
+ const absolute = path7.resolve(path7.join(managedRoot, entry.name));
1625
1746
  if (registered.has(absolute)) continue;
1626
1747
  if (pending.has(absolute)) continue;
1627
1748
  found.push(absolute);
@@ -1649,7 +1770,7 @@ async function prepareTaskRoot(root, options = {}) {
1649
1770
  await release();
1650
1771
  }
1651
1772
  }
1652
- var PREP_LOCK_WAIT_MS, PREP_LOCK_STALE_MS, REPORTED_RESIDUAL_SNAPSHOTS, IGNORED_MANAGED_ENTRIES;
1773
+ var PREP_LOCK_WAIT_MS, PREP_LOCK_STALE_MS, REPORTED_RESIDUAL_SNAPSHOTS, IGNORED_MANAGED_ENTRIES, splitZ;
1653
1774
  var init_task_root_prepare = __esm({
1654
1775
  "src/runner/task-root-prepare.mjs"() {
1655
1776
  "use strict";
@@ -1660,19 +1781,20 @@ var init_task_root_prepare = __esm({
1660
1781
  PREP_LOCK_STALE_MS = 45 * 60 * 1e3;
1661
1782
  REPORTED_RESIDUAL_SNAPSHOTS = /* @__PURE__ */ new Set();
1662
1783
  IGNORED_MANAGED_ENTRIES = /* @__PURE__ */ new Set([
1784
+ ".canonical-recovery",
1663
1785
  "recovery-ledger.jsonl",
1664
1786
  "runner-pnpm-hydration.json",
1665
1787
  "runner-node-modules-quarantine",
1666
1788
  "runner-root-prep.lock"
1667
1789
  ]);
1790
+ splitZ = (value) => String(value || "").split("\0").filter((item) => item.length > 0);
1668
1791
  }
1669
1792
  });
1670
1793
 
1671
1794
  // src/runner/worktree-helper.mjs
1672
- import { createHash as createHash3 } from "node:crypto";
1673
1795
  import fs5 from "node:fs";
1674
1796
  import fsp8 from "node:fs/promises";
1675
- import path7 from "node:path";
1797
+ import path8 from "node:path";
1676
1798
  function repoRoot() {
1677
1799
  return process.env.VO_CODE_RUNNER_REPO || process.cwd();
1678
1800
  }
@@ -1683,16 +1805,12 @@ function sanitize(value, fallback) {
1683
1805
  const cleaned = String(value || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1684
1806
  return cleaned || fallback;
1685
1807
  }
1686
- function worktreeDirForName2(root, worktreeName) {
1687
- const leaf = createHash3("sha256").update(String(worktreeName)).digest("hex").slice(0, 16);
1688
- return path7.join(root, ".agent-worktrees", leaf);
1689
- }
1690
1808
  function cloneDirForSlug(repoSlug, clonesRootDir) {
1691
1809
  if (!clonesRootDir || !repoSlug || !VALID_REPO_SLUG.test(String(repoSlug))) return null;
1692
1810
  const [owner, name] = String(repoSlug).split("/");
1693
1811
  if (owner === "." || owner === ".." || name === "." || name === "..") return null;
1694
1812
  if (owner.startsWith("-") || name.startsWith("-")) return null;
1695
- return path7.join(clonesRootDir, `${sanitize(owner, "owner")}__${sanitize(name, "repo")}`);
1813
+ return path8.join(clonesRootDir, `${sanitize(owner, "owner")}__${sanitize(name, "repo")}`);
1696
1814
  }
1697
1815
  function cloneLockDir(dir) {
1698
1816
  return `${dir}.clone-lock`;
@@ -1707,7 +1825,7 @@ async function pathExists5(target) {
1707
1825
  }
1708
1826
  async function readLockMeta2(lockDir) {
1709
1827
  try {
1710
- return JSON.parse(await fsp8.readFile(path7.join(lockDir, "owner.json"), "utf8"));
1828
+ return JSON.parse(await fsp8.readFile(path8.join(lockDir, "owner.json"), "utf8"));
1711
1829
  } catch {
1712
1830
  return null;
1713
1831
  }
@@ -1728,11 +1846,11 @@ async function acquireCloneLock(dir, options = {}) {
1728
1846
  const sleep3 = options.sleep || sleepMs;
1729
1847
  const lockDir = cloneLockDir(dir);
1730
1848
  const deadline = nowMs() + waitMs;
1731
- await fsp8.mkdir(path7.dirname(lockDir), { recursive: true });
1849
+ await fsp8.mkdir(path8.dirname(lockDir), { recursive: true });
1732
1850
  for (; ; ) {
1733
1851
  try {
1734
1852
  await fsp8.mkdir(lockDir);
1735
- await fsp8.writeFile(path7.join(lockDir, "owner.json"), `${JSON.stringify({
1853
+ await fsp8.writeFile(path8.join(lockDir, "owner.json"), `${JSON.stringify({
1736
1854
  pid: process.pid,
1737
1855
  createdAt: new Date(nowMs()).toISOString(),
1738
1856
  dir
@@ -1761,7 +1879,7 @@ async function acquireCloneLock(dir, options = {}) {
1761
1879
  }
1762
1880
  }
1763
1881
  async function isUsableGitClone(dir, runner = runProcess) {
1764
- if (!await pathExists5(path7.join(dir, ".git"))) return false;
1882
+ if (!await pathExists5(path8.join(dir, ".git"))) return false;
1765
1883
  const result = await runner("git", ["-C", dir, "rev-parse", "HEAD"], { timeoutMs: 1e4 });
1766
1884
  return result.status === 0 && Boolean(String(result.stdout || "").trim());
1767
1885
  }
@@ -1782,7 +1900,7 @@ async function ensureUsableClone(repoSlug, dir, options = {}) {
1782
1900
  const maxAttempts = options.maxAttempts || 5;
1783
1901
  const raceWaitMs = options.raceWaitMs ?? 1e4;
1784
1902
  let lastError = null;
1785
- await fsp8.mkdir(path7.dirname(dir), { recursive: true });
1903
+ await fsp8.mkdir(path8.dirname(dir), { recursive: true });
1786
1904
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1787
1905
  if (await pathExists5(dir)) {
1788
1906
  if (await waitForUsableClone(dir, runner, sleep3, raceWaitMs)) return dir;
@@ -1794,7 +1912,7 @@ async function ensureUsableClone(repoSlug, dir, options = {}) {
1794
1912
  ["clone", "--no-tags", `https://github.com/${owner}/${name}.git`, tmpDir],
1795
1913
  { timeoutMs: 6e5 }
1796
1914
  );
1797
- if (clone.status !== 0 || !await pathExists5(path7.join(tmpDir, ".git"))) {
1915
+ if (clone.status !== 0 || !await pathExists5(path8.join(tmpDir, ".git"))) {
1798
1916
  await fsp8.rm(tmpDir, { recursive: true, force: true });
1799
1917
  lastError = new Error(`[vo-mcp runner] clone failed for ${repoSlug}: ${describeGitFailure(clone)}`);
1800
1918
  continue;
@@ -1821,7 +1939,7 @@ async function ensureUsableClone(repoSlug, dir, options = {}) {
1821
1939
  }
1822
1940
  async function resolveTaskRoot(repoSlug) {
1823
1941
  const root = clonesRoot();
1824
- if (root && !path7.isAbsolute(root)) {
1942
+ if (root && !path8.isAbsolute(root)) {
1825
1943
  throw new Error(`[vo-mcp runner] VO_CODE_RUNNER_CLONES_ROOT must be an absolute path (got '${root}')`);
1826
1944
  }
1827
1945
  const dir = cloneDirForSlug(repoSlug, root);
@@ -1866,9 +1984,12 @@ async function createFixWorktree(kind, error = {}, options = {}) {
1866
1984
  const unique = `${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
1867
1985
  const worktreeName = `${safeKind}-${safeTarget}-${stamp}-${unique}`;
1868
1986
  const branchName = `vo/${worktreeName}`;
1869
- const worktreeDir = worktreeDirForName2(root, worktreeName);
1987
+ const worktreeDir = worktreeDirForName(root, worktreeName);
1870
1988
  const dependencyOwnership = createDependencyOwnershipTracker(worktreeDir);
1871
- const prep = await prepare(root);
1989
+ const prep = await prepare(root, {
1990
+ recoverDirtyCanonical: multiRepo,
1991
+ managedPool: path8.dirname(worktreeDir)
1992
+ });
1872
1993
  await processRunner("git", ["config", "core.longpaths", "true"], { cwd: root, timeoutMs: 3e4 });
1873
1994
  const add = await addWorktree({ root, branchName, worktreeDir });
1874
1995
  if (!add.ok) {
@@ -1920,7 +2041,7 @@ function preserveFailedWorktree(worktreeName, meta = {}) {
1920
2041
  const tracked = TRACKED_WORKTREES.get(worktreeName);
1921
2042
  if (tracked) TRACKED_WORKTREES.delete(worktreeName);
1922
2043
  const root = tracked ? tracked.root : repoRoot();
1923
- const worktreeDir = tracked ? tracked.worktreeDir : worktreeDirForName2(root, worktreeName);
2044
+ const worktreeDir = tracked ? tracked.worktreeDir : worktreeDirForName(root, worktreeName);
1924
2045
  const entry = {
1925
2046
  at: (/* @__PURE__ */ new Date()).toISOString(),
1926
2047
  worktreeName,
@@ -1932,8 +2053,8 @@ function preserveFailedWorktree(worktreeName, meta = {}) {
1932
2053
  reason: String(meta.reason || "task failed").slice(0, 300)
1933
2054
  };
1934
2055
  try {
1935
- const ledger = path7.join(root, ".agent-worktrees", "recovery-ledger.jsonl");
1936
- fs5.mkdirSync(path7.dirname(ledger), { recursive: true });
2056
+ const ledger = recoveryLedgerPathForRoot(root);
2057
+ fs5.mkdirSync(path8.dirname(ledger), { recursive: true });
1937
2058
  fs5.appendFileSync(ledger, `${JSON.stringify(entry)}
1938
2059
  `, "utf8");
1939
2060
  console.error(`[vo-mcp runner] Preserved worktree ${worktreeName} (reason: ${entry.reason})`);
@@ -1951,6 +2072,8 @@ var init_worktree_helper = __esm({
1951
2072
  init_pnpm_hydration();
1952
2073
  init_task_root_prepare();
1953
2074
  init_worktree_cleanup();
2075
+ init_worktree_paths();
2076
+ init_worktree_paths();
1954
2077
  VALID_REPO_SLUG = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/u;
1955
2078
  DEFAULT_CLONE_LOCK_WAIT_MS = 12e4;
1956
2079
  DEFAULT_CLONE_LOCK_STALE_MS = 30 * 60 * 1e3;
@@ -2008,9 +2131,9 @@ function createControlPlaneClient({
2008
2131
  throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
2009
2132
  }
2010
2133
  const root = baseUrl.replace(/\/+$/, "");
2011
- async function req(method, path12, body) {
2134
+ async function req(method, path16, body) {
2012
2135
  const bearer = await resolveBearer(env2);
2013
- return fetchImpl(`${root}${path12}`, {
2136
+ return fetchImpl(`${root}${path16}`, {
2014
2137
  method,
2015
2138
  headers: {
2016
2139
  "content-type": "application/json",
@@ -2200,6 +2323,34 @@ function createControlPlaneClient({
2200
2323
  if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
2201
2324
  return true;
2202
2325
  },
2326
+ /** Poll one authenticated runner's durable Mission Control action queue. */
2327
+ async pollRunnerControl({ runnerId, operatorId }) {
2328
+ const body = { runner_id: runnerId };
2329
+ if (operatorId) body.operator_id = operatorId;
2330
+ const res = await req("POST", "/api/v1/runner/control/poll", body);
2331
+ if (res.status === 401) {
2332
+ cachedFirebaseToken = null;
2333
+ throw new Error("runner control poll unauthorized (401)");
2334
+ }
2335
+ if (!res.ok) throw new Error(`runner control poll failed: HTTP ${res.status}`);
2336
+ const json = await res.json();
2337
+ const action = json?.action;
2338
+ return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
2339
+ },
2340
+ /** Acknowledge a maintenance action after the host has restarted the child. */
2341
+ async completeRunnerControl(actionId, { runnerId, operatorId, status, detail }) {
2342
+ const body = { runner_id: runnerId, status };
2343
+ if (operatorId) body.operator_id = operatorId;
2344
+ if (detail) body.detail = detail;
2345
+ const res = await req("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
2346
+ if (res.status === 401) {
2347
+ cachedFirebaseToken = null;
2348
+ throw new Error("runner control completion unauthorized (401)");
2349
+ }
2350
+ if (!res.ok) throw new Error(`runner control completion failed: HTTP ${res.status}`);
2351
+ const json = await res.json();
2352
+ return json?.action || null;
2353
+ },
2203
2354
  /**
2204
2355
  * Mint a short-lived (~1h), repo-scoped GitHub App installation token for
2205
2356
  * THIS runner's operator (M3). The control-plane keys the mint on the
@@ -2257,9 +2408,121 @@ var init_control_plane_client = __esm({
2257
2408
  }
2258
2409
  });
2259
2410
 
2411
+ // ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
2412
+ import { existsSync as existsSync2, realpathSync } from "node:fs";
2413
+ import { win32 as path9 } from "node:path";
2414
+ import { spawnSync } from "node:child_process";
2415
+ function pathValue(env2) {
2416
+ for (const key of ["Path", "PATH", "path"]) {
2417
+ if (typeof env2?.[key] === "string") return env2[key];
2418
+ }
2419
+ return "";
2420
+ }
2421
+ function cleanPathSegment(value) {
2422
+ const trimmed = String(value || "").trim();
2423
+ return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
2424
+ }
2425
+ function pathCandidates(bin, env2) {
2426
+ if (path9.isAbsolute(bin) || /[\\/]/u.test(bin)) {
2427
+ return [path9.resolve(bin)];
2428
+ }
2429
+ const extension = path9.extname(bin);
2430
+ return pathValue(env2).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path9.join(directory, bin)] : [
2431
+ path9.join(directory, `${bin}.exe`),
2432
+ path9.join(directory, `${bin}.cmd`),
2433
+ path9.join(directory, `${bin}.ps1`),
2434
+ path9.join(directory, bin)
2435
+ ]);
2436
+ }
2437
+ function canonicalExistingPath(candidate, exists, canonicalize) {
2438
+ if (!exists(candidate)) return null;
2439
+ try {
2440
+ return canonicalize(candidate);
2441
+ } catch {
2442
+ return null;
2443
+ }
2444
+ }
2445
+ function resolveWindowsClaudeExecutable({
2446
+ bin = "claude",
2447
+ env: env2 = process.env,
2448
+ exists = existsSync2,
2449
+ canonicalize = realpathSync
2450
+ } = {}) {
2451
+ const requested = String(bin || "").trim();
2452
+ if (!requested || requested.includes("\0")) {
2453
+ throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
2454
+ }
2455
+ for (const candidate of pathCandidates(requested, env2)) {
2456
+ const found = canonicalExistingPath(candidate, exists, canonicalize);
2457
+ if (!found) continue;
2458
+ if (path9.extname(found).toLowerCase() === ".exe") return found;
2459
+ const native = path9.join(path9.dirname(found), ...NATIVE_CLAUDE_PARTS);
2460
+ const resolvedNative = canonicalExistingPath(native, exists, canonicalize);
2461
+ if (resolvedNative) return resolvedNative;
2462
+ }
2463
+ const error = new Error(
2464
+ `Could not resolve a native claude.exe for "${requested}". Update Claude Code with npm install -g @anthropic-ai/claude-code; the VO runner will not execute a shell-only .cmd/.ps1 shim.`
2465
+ );
2466
+ error.code = "ENOENT";
2467
+ throw error;
2468
+ }
2469
+ function buildWindowsClaudeLaunch({
2470
+ bin = "claude",
2471
+ args = [],
2472
+ env: env2 = process.env
2473
+ } = {}) {
2474
+ return {
2475
+ bin: resolveWindowsClaudeExecutable({ bin, env: env2 }),
2476
+ args: Array.from(args, (value) => String(value)),
2477
+ spawnOptions: {
2478
+ shell: false,
2479
+ windowsHide: true,
2480
+ windowsVerbatimArguments: false
2481
+ }
2482
+ };
2483
+ }
2484
+ function spawnClaudeSync(args = [], options = {}) {
2485
+ if (process.platform !== "win32") {
2486
+ return spawnSync("claude", args, { windowsHide: true, ...options });
2487
+ }
2488
+ try {
2489
+ const launch = buildWindowsClaudeLaunch({
2490
+ bin: "claude",
2491
+ args,
2492
+ env: options.env || process.env
2493
+ });
2494
+ return spawnSync(launch.bin, launch.args, {
2495
+ ...options,
2496
+ ...launch.spawnOptions
2497
+ });
2498
+ } catch (error) {
2499
+ return {
2500
+ error,
2501
+ status: null,
2502
+ signal: null,
2503
+ output: null,
2504
+ stdout: null,
2505
+ stderr: null
2506
+ };
2507
+ }
2508
+ }
2509
+ var NATIVE_CLAUDE_PARTS;
2510
+ var init_windows_claude_launch = __esm({
2511
+ "../../scripts/virtual-office/code-runner/windows-claude-launch.mjs"() {
2512
+ "use strict";
2513
+ NATIVE_CLAUDE_PARTS = [
2514
+ "node_modules",
2515
+ "@anthropic-ai",
2516
+ "claude-code",
2517
+ "bin",
2518
+ "claude.exe"
2519
+ ];
2520
+ }
2521
+ });
2522
+
2260
2523
  // ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
2261
2524
  import { createRequire as createRequire2 } from "node:module";
2262
- import { spawnSync } from "node:child_process";
2525
+ import { spawnSync as spawnSync2 } from "node:child_process";
2263
2526
  function defaultEntryCtor() {
2264
2527
  if (_loadTried) return _entryCtor;
2265
2528
  _loadTried = true;
@@ -2283,7 +2546,7 @@ function isTruthyFlag(v) {
2283
2546
  return s === "1" || s === "true" || s === "yes" || s === "on";
2284
2547
  }
2285
2548
  function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey } = {}) {
2286
- if (isTruthyFlag(baseEnv[PREFER_LOGIN_ENV])) {
2549
+ if (isTruthyFlag(baseEnv[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(baseEnv[PREFER_LOGIN_ENV])) {
2287
2550
  const next = { ...baseEnv };
2288
2551
  delete next.ANTHROPIC_API_KEY;
2289
2552
  return next;
@@ -2293,7 +2556,7 @@ function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey } = {}) {
2293
2556
  return key ? { ...baseEnv, ANTHROPIC_API_KEY: key } : { ...baseEnv };
2294
2557
  }
2295
2558
  function describeAnthropicAuthSource(baseEnv = {}, { getKey = getAnthropicKey } = {}) {
2296
- if (isTruthyFlag(baseEnv[PREFER_LOGIN_ENV])) {
2559
+ if (isTruthyFlag(baseEnv[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(baseEnv[PREFER_LOGIN_ENV])) {
2297
2560
  return "claude auth login (VO_RUNNER_PREFER_LOGIN set \u2014 any API key ignored)";
2298
2561
  }
2299
2562
  if (baseEnv.ANTHROPIC_API_KEY) return "ANTHROPIC_API_KEY from environment";
@@ -2306,29 +2569,31 @@ function augmentAuthError(summary) {
2306
2569
  return `${s}
2307
2570
  \u21B3 Anthropic auth failed on the runner. The \`claude\` CLI is a SEPARATE install/login from the Claude Desktop app and the Claude Code IDE extension \u2014 signing into those does NOT authenticate it. Fix: run \`claude auth login\` (Claude subscription) on the runner machine, or clear any stale ANTHROPIC_API_KEY (env / OS keychain / .env.local) and set VO_RUNNER_PREFER_LOGIN=1 \u2014 then restart the runner. Verify with \`claude -p "say hi"\`.`;
2308
2571
  }
2309
- function probeClaudeLoginState({ spawn: spawn4 = spawnSync } = {}) {
2572
+ function probeClaudeLoginState({
2573
+ spawn: spawn4 = spawnSync2,
2574
+ buildWindowsLaunch = buildWindowsClaudeLaunch,
2575
+ platform = process.platform
2576
+ } = {}) {
2310
2577
  try {
2311
- const st = spawn4("claude", ["auth", "status"], {
2312
- shell: process.platform === "win32",
2313
- windowsHide: true,
2314
- timeout: 5e3,
2315
- encoding: "utf8"
2316
- });
2578
+ const launch = platform === "win32" ? buildWindowsLaunch({ bin: "claude", args: ["auth", "status"] }) : { bin: "claude", args: ["auth", "status"], spawnOptions: { windowsHide: true } };
2579
+ const st = spawn4(launch.bin, launch.args, { ...launch.spawnOptions, timeout: 5e3, encoding: "utf8" });
2317
2580
  const parsed = JSON.parse(String(st.stdout || "").trim() || "{}");
2318
2581
  return typeof parsed.loggedIn === "boolean" ? parsed.loggedIn : null;
2319
2582
  } catch {
2320
2583
  return null;
2321
2584
  }
2322
2585
  }
2323
- var require2, KEY_SERVICE, KEY_ACCOUNT, _entryCtor, _loadTried, PREFER_LOGIN_ENV, AUTH_ERROR_RE;
2586
+ var require2, KEY_SERVICE, KEY_ACCOUNT, _entryCtor, _loadTried, PREFER_LOGIN_ENV, CLAUDE_PREFER_LOGIN_ENV, AUTH_ERROR_RE;
2324
2587
  var init_anthropic_key_store = __esm({
2325
2588
  "../../scripts/virtual-office/code-runner/anthropic-key-store.mjs"() {
2326
2589
  "use strict";
2590
+ init_windows_claude_launch();
2327
2591
  require2 = createRequire2(import.meta.url);
2328
2592
  KEY_SERVICE = "algosuite-vo";
2329
2593
  KEY_ACCOUNT = "anthropic-api-key";
2330
2594
  _loadTried = false;
2331
2595
  PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
2596
+ CLAUDE_PREFER_LOGIN_ENV = "VO_RUNNER_CLAUDE_PREFER_LOGIN";
2332
2597
  AUTH_ERROR_RE = /\b401\b|invalid[^.]{0,24}(authentication|credential)|authentication_error|unauthorized|not[ _-]?authenticated/i;
2333
2598
  }
2334
2599
  });
@@ -2416,7 +2681,7 @@ var init_agent_task_stream = __esm({
2416
2681
  });
2417
2682
 
2418
2683
  // ../../scripts/virtual-office/code-runner/sandbox/sandbox-docker.mjs
2419
- import { spawnSync as spawnSync2 } from "node:child_process";
2684
+ import { spawnSync as spawnSync3 } from "node:child_process";
2420
2685
  function buildDockerArgs({
2421
2686
  worktreeDir,
2422
2687
  image = DEFAULT_SANDBOX_IMAGE,
@@ -2501,15 +2766,17 @@ var init_context7_mcp = __esm({
2501
2766
 
2502
2767
  // ../../scripts/virtual-office/code-runner/claude-args.mjs
2503
2768
  function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, env: env2 = process.env } = {}) {
2769
+ const effectivePermissionMode = String(permissionMode || DEFAULT_PERMISSION_MODE);
2770
+ const allowedTools = effectivePermissionMode === DEFAULT_PERMISSION_MODE ? `${VO_SESSION_STATE_TOOL},${VO_HEADLESS_PNPM_TOOL},${VO_HEADLESS_PNPM_FROM_DIR_TOOL}` : VO_SESSION_STATE_TOOL;
2504
2771
  const args = [
2505
2772
  "-p",
2506
2773
  "--output-format",
2507
2774
  "stream-json",
2508
2775
  "--verbose",
2509
2776
  "--permission-mode",
2510
- String(permissionMode || DEFAULT_PERMISSION_MODE),
2777
+ effectivePermissionMode,
2511
2778
  "--allowedTools",
2512
- VO_SESSION_STATE_TOOL
2779
+ allowedTools
2513
2780
  ];
2514
2781
  if (Number.isInteger(maxTurns) && maxTurns > 0) {
2515
2782
  args.push("--max-turns", String(maxTurns));
@@ -2526,22 +2793,24 @@ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, m
2526
2793
  args.push(...context7McpArgs(env2));
2527
2794
  return args;
2528
2795
  }
2529
- var DEFAULT_PERMISSION_MODE, VO_SESSION_STATE_TOOL;
2796
+ var DEFAULT_PERMISSION_MODE, VO_SESSION_STATE_TOOL, VO_HEADLESS_PNPM_TOOL, VO_HEADLESS_PNPM_FROM_DIR_TOOL;
2530
2797
  var init_claude_args = __esm({
2531
2798
  "../../scripts/virtual-office/code-runner/claude-args.mjs"() {
2532
2799
  "use strict";
2533
2800
  init_context7_mcp();
2534
2801
  DEFAULT_PERMISSION_MODE = "acceptEdits";
2535
2802
  VO_SESSION_STATE_TOOL = "mcp__vo-mcp__vo_report_session_state";
2803
+ VO_HEADLESS_PNPM_TOOL = "Bash(pnpm *)";
2804
+ VO_HEADLESS_PNPM_FROM_DIR_TOOL = "Bash(pnpm --dir *)";
2536
2805
  }
2537
2806
  });
2538
2807
 
2539
2808
  // ../../scripts/virtual-office/code-runner/terminal-process-cleanup.mjs
2540
- import { spawnSync as spawnSync3 } from "node:child_process";
2809
+ import { spawnSync as spawnSync4 } from "node:child_process";
2541
2810
  function terminateAgentProcessTree({
2542
2811
  child,
2543
2812
  platform = process.platform,
2544
- spawn: spawn4 = spawnSync3
2813
+ spawn: spawn4 = spawnSync4
2545
2814
  } = {}) {
2546
2815
  if (!child || !Number.isInteger(child.pid) || child.pid <= 0) return false;
2547
2816
  if (platform === "win32") {
@@ -2577,7 +2846,7 @@ var init_terminal_process_cleanup = __esm({
2577
2846
  });
2578
2847
 
2579
2848
  // ../../scripts/virtual-office/code-runner/claude-runner.mjs
2580
- import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
2849
+ import { spawn as spawn2 } from "node:child_process";
2581
2850
  function extractText(content) {
2582
2851
  if (typeof content === "string") return content.trim();
2583
2852
  if (Array.isArray(content)) {
@@ -2657,7 +2926,9 @@ function runAgentTask({
2657
2926
  });
2658
2927
  spawnBin = sandbox.dockerBin || "docker";
2659
2928
  spawnOpts = { windowsHide: true };
2660
- }
2929
+ } else if (typeof runner.prepareSpawn === "function") {
2930
+ ({ bin: spawnBin, args: spawnArgs, spawnOptions: spawnOpts } = runner.prepareSpawn({ bin: spawnBin, args: spawnArgs, spawnOptions: spawnOpts, env: spawnEnv }));
2931
+ } else if (typeof runner.prepareSpawnArgs === "function") spawnArgs = runner.prepareSpawnArgs(spawnArgs);
2661
2932
  const child = spawnImpl(spawnBin, spawnArgs, {
2662
2933
  cwd,
2663
2934
  env: spawnEnv,
@@ -2791,6 +3062,7 @@ var init_claude_runner = __esm({
2791
3062
  init_agent_task_stream();
2792
3063
  init_sandbox_docker();
2793
3064
  init_claude_args();
3065
+ init_windows_claude_launch();
2794
3066
  init_terminal_process_cleanup();
2795
3067
  ClaudeRunner = class {
2796
3068
  get binary() {
@@ -2803,10 +3075,11 @@ var init_claude_runner = __esm({
2803
3075
  return parseStreamEvent(line);
2804
3076
  }
2805
3077
  getSpawnOptions() {
2806
- return {
2807
- shell: process.platform === "win32",
2808
- windowsHide: true
2809
- };
3078
+ return { shell: false, windowsHide: true };
3079
+ }
3080
+ prepareSpawn({ bin, args, spawnOptions, env: env2 = process.env } = {}) {
3081
+ if (process.platform !== "win32") return { bin, args, spawnOptions: spawnOptions ?? this.getSpawnOptions() };
3082
+ return buildWindowsClaudeLaunch({ bin, args, env: env2 });
2810
3083
  }
2811
3084
  /**
2812
3085
  * Fill ANTHROPIC_API_KEY from the OS keychain when not already set (M4 BYO),
@@ -2827,12 +3100,7 @@ var init_claude_runner = __esm({
2827
3100
  */
2828
3101
  async checkAuth() {
2829
3102
  try {
2830
- const probe = spawnSync4("claude", ["--version"], {
2831
- shell: process.platform === "win32",
2832
- windowsHide: true,
2833
- timeout: 3e3,
2834
- stdio: "ignore"
2835
- });
3103
+ const probe = spawnClaudeSync(["--version"], { timeout: 3e3, stdio: "ignore" });
2836
3104
  if (probe.error) {
2837
3105
  return {
2838
3106
  installed: false,
@@ -2946,6 +3214,7 @@ var init_agent_key_store = __esm({
2946
3214
  // ../../scripts/virtual-office/code-runner/codex-runner.mjs
2947
3215
  var codex_runner_exports = {};
2948
3216
  __export(codex_runner_exports, {
3217
+ CODEX_PREFER_LOGIN_ENV: () => CODEX_PREFER_LOGIN_ENV,
2949
3218
  CodexRunner: () => CodexRunner,
2950
3219
  buildCodexArgs: () => buildCodexArgs,
2951
3220
  codexRunner: () => codexRunner,
@@ -2953,17 +3222,23 @@ __export(codex_runner_exports, {
2953
3222
  resolveCodexBinary: () => resolveCodexBinary
2954
3223
  });
2955
3224
  import { spawnSync as spawnSync5 } from "node:child_process";
2956
- import { existsSync as existsSync2 } from "node:fs";
3225
+ import { existsSync as existsSync3 } from "node:fs";
2957
3226
  import { join as join2 } from "node:path";
3227
+ function isTruthyFlag2(value) {
3228
+ return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
3229
+ }
2958
3230
  function resolveCodexBinary({
2959
3231
  env: env2 = process.env,
2960
3232
  platform = process.platform,
2961
- exists = existsSync2
3233
+ exists = existsSync3
2962
3234
  } = {}) {
2963
3235
  if (platform !== "win32") return "codex";
2964
3236
  const appData = String(env2.APPDATA || "").trim();
3237
+ const userProfile = String(env2.USERPROFILE || "").trim();
3238
+ const localAppData = String(env2.LOCALAPPDATA || "").trim();
3239
+ const candidates = [];
2965
3240
  if (appData) {
2966
- const npmVendorBinary = join2(
3241
+ candidates.push(join2(
2967
3242
  appData,
2968
3243
  "npm",
2969
3244
  "node_modules",
@@ -2976,9 +3251,17 @@ function resolveCodexBinary({
2976
3251
  "x86_64-pc-windows-msvc",
2977
3252
  "bin",
2978
3253
  "codex.exe"
2979
- );
2980
- if (exists(npmVendorBinary)) return npmVendorBinary;
3254
+ ));
3255
+ }
3256
+ if (userProfile) {
3257
+ candidates.push(join2(userProfile, ".local", "bin", "codex.exe"));
3258
+ candidates.push(join2(userProfile, ".codex", "bin", "codex.exe"));
2981
3259
  }
3260
+ if (localAppData) {
3261
+ candidates.push(join2(localAppData, "Microsoft", "WindowsApps", "codex.exe"));
3262
+ }
3263
+ const absolute = candidates.find((candidate) => exists(candidate));
3264
+ if (absolute) return absolute;
2982
3265
  return "codex";
2983
3266
  }
2984
3267
  function buildCodexArgs({ model, effort } = {}) {
@@ -3029,14 +3312,21 @@ function parseCodexEvent(line) {
3029
3312
  }
3030
3313
  return null;
3031
3314
  }
3032
- var CodexRunner, codexRunner;
3315
+ var CODEX_PREFER_LOGIN_ENV, LEGACY_PREFER_LOGIN_ENV, CodexRunner, codexRunner;
3033
3316
  var init_codex_runner = __esm({
3034
3317
  "../../scripts/virtual-office/code-runner/codex-runner.mjs"() {
3035
3318
  "use strict";
3036
3319
  init_agent_key_store();
3320
+ CODEX_PREFER_LOGIN_ENV = "VO_RUNNER_CODEX_PREFER_LOGIN";
3321
+ LEGACY_PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
3037
3322
  CodexRunner = class {
3323
+ constructor({ spawn: spawn4 = spawnSync5, resolveBinary = resolveCodexBinary, env: env2 = process.env } = {}) {
3324
+ this.spawn = spawn4;
3325
+ this.resolveBinary = resolveBinary;
3326
+ this.env = env2;
3327
+ }
3038
3328
  get binary() {
3039
- return resolveCodexBinary();
3329
+ return this.resolveBinary();
3040
3330
  }
3041
3331
  buildArgs(opts = {}) {
3042
3332
  return buildCodexArgs(opts);
@@ -3058,25 +3348,58 @@ var init_codex_runner = __esm({
3058
3348
  * env wins; no key stored → unchanged (a prior `codex login` still works).
3059
3349
  */
3060
3350
  applyAuthEnv(env2 = process.env) {
3351
+ if (isTruthyFlag2(env2[CODEX_PREFER_LOGIN_ENV]) || isTruthyFlag2(env2[LEGACY_PREFER_LOGIN_ENV])) {
3352
+ const out = { ...env2 };
3353
+ delete out.OPENAI_API_KEY;
3354
+ delete out.CODEX_API_KEY;
3355
+ return out;
3356
+ }
3061
3357
  return withAgentKey("openai", env2);
3062
3358
  }
3063
- /** Best-effort: is `codex` on PATH? Never throws. */
3359
+ /** Best-effort binary + persisted-login probe. Never throws or spends tokens. */
3064
3360
  async checkAuth() {
3065
3361
  try {
3066
3362
  const bin = this.binary;
3067
- const { status, error } = spawnSync5(bin, ["--version"], {
3363
+ const version = this.spawn(bin, ["--version"], {
3068
3364
  ...this.getSpawnOptions({ bin }),
3069
3365
  windowsHide: true,
3070
3366
  timeout: 3e3,
3071
3367
  stdio: "ignore"
3072
3368
  });
3073
- if (error) {
3074
- return { installed: false, authenticated: false, message: `codex not found on PATH: ${error.message}` };
3369
+ if (version.error) {
3370
+ return { installed: false, authenticated: false, message: `codex not found on PATH: ${version.error.message}` };
3075
3371
  }
3076
- if (status !== 0) {
3372
+ if (version.status !== 0) {
3077
3373
  return { installed: true, authenticated: false, message: "codex exists but --version failed (auth unclear)" };
3078
3374
  }
3079
- return { installed: true, authenticated: true, message: "codex binary found (auth check is best-effort)" };
3375
+ const login = this.spawn(bin, ["login", "status"], {
3376
+ ...this.getSpawnOptions({ bin }),
3377
+ windowsHide: true,
3378
+ timeout: 5e3,
3379
+ encoding: "utf8"
3380
+ });
3381
+ const output = `${login.stdout || ""}
3382
+ ${login.stderr || ""}`.trim();
3383
+ if (login.error || login.status !== 0) {
3384
+ const authEnv = this.applyAuthEnv(this.env);
3385
+ if (authEnv.OPENAI_API_KEY || authEnv.CODEX_API_KEY) {
3386
+ return {
3387
+ installed: true,
3388
+ authenticated: true,
3389
+ message: "codex API key available (no persisted ChatGPT login)"
3390
+ };
3391
+ }
3392
+ return {
3393
+ installed: true,
3394
+ authenticated: false,
3395
+ message: output || login.error?.message || "codex is installed but not logged in"
3396
+ };
3397
+ }
3398
+ return {
3399
+ installed: true,
3400
+ authenticated: true,
3401
+ message: output || "codex login status succeeded"
3402
+ };
3080
3403
  } catch (err) {
3081
3404
  return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
3082
3405
  }
@@ -3262,12 +3585,12 @@ var init_grok_model_catalog = __esm({
3262
3585
 
3263
3586
  // ../../scripts/virtual-office/code-runner/grok-runner.mjs
3264
3587
  import { spawnSync as spawnSync7 } from "node:child_process";
3265
- import { existsSync as existsSync3 } from "node:fs";
3588
+ import { existsSync as existsSync4 } from "node:fs";
3266
3589
  import { join as join3, win32 } from "node:path";
3267
3590
  function resolveGrokBinary({
3268
3591
  env: env2 = process.env,
3269
3592
  platform = process.platform,
3270
- exists = existsSync3
3593
+ exists = existsSync4
3271
3594
  } = {}) {
3272
3595
  const explicit = String(env2.VO_CODE_RUNNER_GROK_BIN || env2.GROK_BIN || "").trim();
3273
3596
  if (explicit) return explicit;
@@ -3940,6 +4263,7 @@ var init_rate_limit_resume = __esm({
3940
4263
  function isTransientGitError(err) {
3941
4264
  if (!err) return false;
3942
4265
  if (err.code && TRANSIENT_CODES.has(err.code)) return true;
4266
+ if (err.code === "UNKNOWN" && /^spawn(?:\s|$)/i.test(String(err.syscall || ""))) return true;
3943
4267
  const msg = String(err.message || err);
3944
4268
  if (/non-fast-forward|fast[- ]forward|\(fetch first\)|permission denied|authentication failed|\b40[134]\b|merge conflict|nothing to commit|did not match any/i.test(msg)) {
3945
4269
  return false;
@@ -3980,7 +4304,7 @@ var init_auto_merge = __esm({
3980
4304
 
3981
4305
  // ../../scripts/virtual-office/code-runner/pr-overlap-gate.mjs
3982
4306
  import { spawnSync as spawnSync8 } from "node:child_process";
3983
- import path8 from "node:path";
4307
+ import path10 from "node:path";
3984
4308
  var init_pr_overlap_gate = __esm({
3985
4309
  "../../scripts/virtual-office/code-runner/pr-overlap-gate.mjs"() {
3986
4310
  "use strict";
@@ -3995,14 +4319,14 @@ function parsePorcelainZ(out) {
3995
4319
  for (let i = 0; i < tokens.length; i += 1) {
3996
4320
  const tok = tokens[i];
3997
4321
  if (!tok) continue;
3998
- const path12 = tok.slice(3);
3999
- if (path12) files.push(path12);
4322
+ const path16 = tok.slice(3);
4323
+ if (path16) files.push(path16);
4000
4324
  if (tok[0] === "R" || tok[0] === "C") i += 1;
4001
4325
  }
4002
4326
  return files;
4003
4327
  }
4004
- function isAgentScratch(path12) {
4005
- const p = String(path12 || "");
4328
+ function isAgentScratch(path16) {
4329
+ const p = String(path16 || "");
4006
4330
  return SCRATCH_PATTERNS.some((re) => re.test(p));
4007
4331
  }
4008
4332
  function isMaxTurnsResult(summary) {
@@ -4063,7 +4387,7 @@ var init_publish = __esm({
4063
4387
  // ../../scripts/virtual-office/code-runner/process-runner.mjs
4064
4388
  import { spawn as spawn3 } from "node:child_process";
4065
4389
  function buildStepLabel(cmd, args = []) {
4066
- return `${cmd} ${args[0] || ""}`.trim();
4390
+ return [cmd, ...args.slice(0, 2)].filter(Boolean).join(" ");
4067
4391
  }
4068
4392
  function buildExitError(cmd, args, { status, signal, stderr, stdout }) {
4069
4393
  const err = new Error(
@@ -4082,6 +4406,18 @@ function buildTimeoutError(cmd, args, timeout, { stderr, stdout } = {}) {
4082
4406
  err.stdout = stdout;
4083
4407
  return err;
4084
4408
  }
4409
+ function buildSpawnError(cmd, args, cwd, err) {
4410
+ const code = err?.code ? String(err.code) : "unknown";
4411
+ const detail = err instanceof Error ? err.message : String(err);
4412
+ const failure = new Error(
4413
+ `${buildStepLabel(cmd, args)} spawn failed${cwd ? ` in ${cwd}` : ""} (${code}): ${detail}`,
4414
+ { cause: err }
4415
+ );
4416
+ for (const key of ["code", "syscall", "path", "spawnargs"]) {
4417
+ if (err?.[key] !== void 0) failure[key] = err[key];
4418
+ }
4419
+ return failure;
4420
+ }
4085
4421
  function runProcess2(cmd, args, {
4086
4422
  cwd,
4087
4423
  timeout = 18e4,
@@ -4127,7 +4463,7 @@ function runProcess2(cmd, args, {
4127
4463
  stderr += chunk.toString();
4128
4464
  });
4129
4465
  child.on("error", (err) => {
4130
- settle(reject, err);
4466
+ settle(reject, buildSpawnError(cmd, args, cwd, err));
4131
4467
  });
4132
4468
  child.on("close", (status, signal) => {
4133
4469
  const result = {
@@ -4181,7 +4517,7 @@ var init_process_runner2 = __esm({
4181
4517
  });
4182
4518
 
4183
4519
  // ../../scripts/virtual-office/code-runner/publish-async.mjs
4184
- import path9 from "node:path";
4520
+ import path11 from "node:path";
4185
4521
  function compactTitle(value, max = 100) {
4186
4522
  return String(value || "").replace(/\s+/g, " ").trim().slice(0, max) || "code-task";
4187
4523
  }
@@ -4222,10 +4558,15 @@ async function resolveOrCreateBranchAsync(worktreeDir, branchPrefix, runCommand
4222
4558
  }
4223
4559
  return branch;
4224
4560
  }
4225
- async function runLocalPrOverlapGateAsync(worktreeDir, files, { env: env2 = process.env, excludePrNumber = null } = {}) {
4226
- const scriptPath = path9.join(worktreeDir, "scripts", "ci", "check-local-pr-overlap.mjs");
4561
+ async function runLocalPrOverlapGateAsync(worktreeDir, files, { branch = "", env: env2 = process.env, excludePrNumber = null } = {}) {
4562
+ const scriptPath = path11.join(worktreeDir, "scripts", "ci", "check-local-pr-overlap.mjs");
4227
4563
  try {
4228
- const output = await runProcess2("node", [scriptPath, "--stdin", ...excludePrNumber ? ["--exclude-pr", String(excludePrNumber)] : []], {
4564
+ const output = await runProcess2("node", [
4565
+ scriptPath,
4566
+ "--stdin",
4567
+ ...branch ? ["--branch", String(branch)] : [],
4568
+ ...excludePrNumber ? ["--exclude-pr", String(excludePrNumber)] : []
4569
+ ], {
4229
4570
  cwd: worktreeDir,
4230
4571
  env: env2,
4231
4572
  input: JSON.stringify([...new Set((files || []).map((file) => String(file || "").trim()).filter(Boolean))]),
@@ -4327,22 +4668,51 @@ async function markPrReadyAsync(worktreeDir, prNumber, githubToken = null, { run
4327
4668
  env: githubToken ? installationTokenEnv(githubToken) : void 0
4328
4669
  });
4329
4670
  }
4330
- async function closeSupersededPrAsync(worktreeDir, prNumber, branch, githubToken = null, { runCommand = defaultRunCommand } = {}) {
4671
+ async function closeSupersededPrAsync(worktreeDir, prNumber, replacementUrl, githubToken = null, { runCommand = defaultRunCommand } = {}) {
4331
4672
  if (!Number.isInteger(prNumber) || prNumber <= 0) return false;
4673
+ if (!/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/u.test(String(replacementUrl || ""))) return false;
4332
4674
  const env2 = githubToken ? installationTokenEnv(githubToken) : void 0;
4333
4675
  const raw = await runCommand("gh", ["pr", "view", String(prNumber), "--json", "state"], worktreeDir, { env: env2 });
4334
4676
  const state = JSON.parse(raw || "{}")?.state;
4335
4677
  if (state !== "OPEN") return false;
4336
4678
  await runCommand(
4337
4679
  "gh",
4338
- ["pr", "close", String(prNumber), "--comment", `VO repair branch ${branch} is pushed from current main and supersedes this failed PR; its replacement is being opened now.`],
4680
+ ["pr", "close", String(prNumber), "--comment", `Superseded by ${replacementUrl}, rebuilt from current main by VO repair.`],
4339
4681
  worktreeDir,
4340
4682
  { env: env2, timeout: 6e4 }
4341
4683
  );
4342
4684
  return true;
4343
4685
  }
4344
- async function pushBranchAsync(worktreeDir, branch, githubToken, { runCommand = defaultRunCommand, allowAmbientFallback = false } = {}) {
4345
- const { primary, fallback } = pushPlan(branch, githubToken, { allowAmbientFallback });
4686
+ async function cleanupSupersededPrAsync({
4687
+ worktreeDir,
4688
+ supersedesPrNumber,
4689
+ replacementUrl,
4690
+ replacementNumber,
4691
+ githubToken,
4692
+ runCommand,
4693
+ onCleanupWarning
4694
+ }) {
4695
+ if (!supersedesPrNumber) return {};
4696
+ if (Number(supersedesPrNumber) === Number(replacementNumber)) {
4697
+ return { supersededPrClosed: false };
4698
+ }
4699
+ try {
4700
+ const closed = await retryTransientAsync(
4701
+ () => closeSupersededPrAsync(worktreeDir, supersedesPrNumber, replacementUrl, githubToken, { runCommand }),
4702
+ { onRetry: gitRetryLog("gh pr close superseded") }
4703
+ );
4704
+ return { supersededPrClosed: closed };
4705
+ } catch (err) {
4706
+ const message = err && err.message ? err.message : String(err);
4707
+ onCleanupWarning?.(
4708
+ `[publish] replacement PR #${replacementNumber} is open; source PR #${supersedesPrNumber} cleanup failed: ${message}`
4709
+ );
4710
+ return { supersededPrClosed: false, supersededPrCloseError: message };
4711
+ }
4712
+ }
4713
+ async function pushBranchAsync(worktreeDir, branch, githubToken, { runCommand = defaultRunCommand, allowAmbientFallback = false, remoteBranch = branch } = {}) {
4714
+ const pushRef = remoteBranch && remoteBranch !== branch ? `HEAD:refs/heads/${remoteBranch}` : branch;
4715
+ const { primary, fallback } = pushPlan(pushRef, githubToken, { allowAmbientFallback });
4346
4716
  try {
4347
4717
  await runCommand("git", primary.args, worktreeDir, { env: primary.env });
4348
4718
  return primary.tokenUsed;
@@ -4364,7 +4734,9 @@ async function openCodeTaskPrAsync(worktreeDir, files, {
4364
4734
  allowAmbientGithubFallback = false,
4365
4735
  draft = false,
4366
4736
  armAutoMerge = false,
4737
+ targetBranch = null,
4367
4738
  supersedesPrNumber = null,
4739
+ onCleanupWarning = console.error,
4368
4740
  runCommand = defaultRunCommand,
4369
4741
  runOverlapGate = runLocalPrOverlapGateAsync
4370
4742
  } = {}) {
@@ -4385,7 +4757,9 @@ async function openCodeTaskPrAsync(worktreeDir, files, {
4385
4757
  branch = committed.branch;
4386
4758
  truncated = committed.truncated;
4387
4759
  }
4760
+ const prBranch = String(targetBranch || branch).trim() || branch;
4388
4761
  const overlap = await runOverlapGate(worktreeDir, files.filter((file) => !isAgentScratch(file)), {
4762
+ branch: prBranch,
4389
4763
  env: githubToken ? installationTokenEnv(githubToken) : process.env,
4390
4764
  excludePrNumber: supersedesPrNumber
4391
4765
  });
@@ -4394,18 +4768,13 @@ ${overlap.output}`);
4394
4768
  const tokenUsed = await retryTransientAsync(
4395
4769
  () => pushBranchAsync(worktreeDir, branch, githubToken, {
4396
4770
  runCommand,
4397
- allowAmbientFallback: allowAmbientGithubFallback
4771
+ allowAmbientFallback: allowAmbientGithubFallback,
4772
+ remoteBranch: prBranch
4398
4773
  }),
4399
4774
  { onRetry: gitRetryLog("git push") }
4400
4775
  );
4401
4776
  const authToken = tokenUsed ? githubToken : null;
4402
- if (supersedesPrNumber) {
4403
- await retryTransientAsync(
4404
- () => closeSupersededPrAsync(worktreeDir, supersedesPrNumber, branch, authToken, { runCommand }),
4405
- { onRetry: gitRetryLog("gh pr close superseded") }
4406
- );
4407
- }
4408
- const existing = await existingPrUrlAsync(worktreeDir, branch, authToken, { runCommand });
4777
+ const existing = await existingPrUrlAsync(worktreeDir, prBranch, authToken, { runCommand });
4409
4778
  if (existing) {
4410
4779
  if (!draft && existing.isDraft) {
4411
4780
  await retryTransientAsync(
@@ -4421,20 +4790,30 @@ ${overlap.output}`);
4421
4790
  draft,
4422
4791
  runCommand
4423
4792
  });
4793
+ const superseded2 = await cleanupSupersededPrAsync({
4794
+ worktreeDir,
4795
+ supersedesPrNumber,
4796
+ replacementUrl: existing.url,
4797
+ replacementNumber: existing.number,
4798
+ githubToken: authToken,
4799
+ runCommand,
4800
+ onCleanupWarning
4801
+ });
4424
4802
  return {
4425
4803
  prUrl: existing.url,
4426
4804
  prNumber: existing.number,
4427
- branch,
4805
+ branch: prBranch,
4428
4806
  truncated,
4429
4807
  resumed: true,
4430
4808
  markedReady: !draft && existing.isDraft,
4431
- ...autoMerge2
4809
+ ...autoMerge2,
4810
+ ...superseded2
4432
4811
  };
4433
4812
  }
4434
4813
  const out = await retryTransientAsync(
4435
4814
  () => runCommand(
4436
4815
  "gh",
4437
- ["pr", "create", "--base", "main", "--head", branch, "--title", compactTitle(title), "--body", String(body || ""), ...draft ? ["--draft"] : []],
4816
+ ["pr", "create", "--base", "main", "--head", prBranch, "--title", compactTitle(title), "--body", String(body || ""), ...draft ? ["--draft"] : []],
4438
4817
  worktreeDir,
4439
4818
  { env: authToken ? installationTokenEnv(authToken) : void 0 }
4440
4819
  ),
@@ -4443,6 +4822,7 @@ ${overlap.output}`);
4443
4822
  const match = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
4444
4823
  if (!match) throw new Error("gh pr create returned no parseable PR URL");
4445
4824
  const prNumber = Number(match[1]);
4825
+ const prUrl = match[0];
4446
4826
  const autoMerge = await maybeArmAutoMergeAsync({
4447
4827
  worktreeDir,
4448
4828
  prNumber,
@@ -4451,7 +4831,16 @@ ${overlap.output}`);
4451
4831
  draft,
4452
4832
  runCommand
4453
4833
  });
4454
- return { prUrl: match[0], prNumber, branch, truncated, ...autoMerge };
4834
+ const superseded = await cleanupSupersededPrAsync({
4835
+ worktreeDir,
4836
+ supersedesPrNumber,
4837
+ replacementUrl: prUrl,
4838
+ replacementNumber: prNumber,
4839
+ githubToken: authToken,
4840
+ runCommand,
4841
+ onCleanupWarning
4842
+ });
4843
+ return { prUrl, prNumber, branch: prBranch, truncated, ...autoMerge, ...superseded };
4455
4844
  }
4456
4845
  var sleep;
4457
4846
  var init_publish_async = __esm({
@@ -4465,6 +4854,111 @@ var init_publish_async = __esm({
4465
4854
  }
4466
4855
  });
4467
4856
 
4857
+ // ../../scripts/virtual-office/code-runner/resume-branch.mjs
4858
+ function compactBranchSegment(value) {
4859
+ return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "resume";
4860
+ }
4861
+ function defaultRunCommand2(cmd, args, cwd, opts = {}) {
4862
+ return runProcess2(cmd, args, { cwd, ...opts });
4863
+ }
4864
+ function buildResumeLocalBranchName(remoteBranch, {
4865
+ now = () => /* @__PURE__ */ new Date(),
4866
+ pid = process.pid,
4867
+ random = Math.random
4868
+ } = {}) {
4869
+ const stamp = now().toISOString().replace(/[:.]/g, "-");
4870
+ const unique = `${pid}-${random().toString(36).slice(2, 8)}`;
4871
+ return `vo/resume-${compactBranchSegment(remoteBranch)}-${stamp}-${unique}`;
4872
+ }
4873
+ async function assertBranchName(worktreeDir, branch, { env: env2, runCommand = defaultRunCommand2 } = {}) {
4874
+ const candidate = String(branch || "").trim();
4875
+ if (!candidate) throw new Error("resume branch is required");
4876
+ await runCommand("git", ["check-ref-format", "--branch", candidate], worktreeDir, {
4877
+ env: env2,
4878
+ timeout: 3e4
4879
+ });
4880
+ return candidate;
4881
+ }
4882
+ async function restoreContinuationBranch(worktreeDir, remoteBranch, {
4883
+ env: env2,
4884
+ runCommand = defaultRunCommand2,
4885
+ localBranchName
4886
+ } = {}) {
4887
+ const branch = await assertBranchName(worktreeDir, remoteBranch, { env: env2, runCommand });
4888
+ const localBranch = await assertBranchName(
4889
+ worktreeDir,
4890
+ localBranchName || buildResumeLocalBranchName(branch),
4891
+ { env: env2, runCommand }
4892
+ );
4893
+ await runCommand(
4894
+ "git",
4895
+ ["fetch", "origin", `${branch}:refs/remotes/origin/${branch}`],
4896
+ worktreeDir,
4897
+ { env: env2, timeout: 12e4 }
4898
+ );
4899
+ await runCommand("git", ["checkout", "-b", localBranch, `origin/${branch}`], worktreeDir, {
4900
+ env: env2,
4901
+ timeout: 6e4
4902
+ });
4903
+ return { localBranch, remoteBranch: branch };
4904
+ }
4905
+ function parseHeadRefName(output) {
4906
+ const parsed = JSON.parse(String(output || "{}"));
4907
+ const branch = String(parsed?.headRefName || "").trim();
4908
+ return branch || null;
4909
+ }
4910
+ async function resolvePrHeadBranch(worktreeDir, {
4911
+ repo,
4912
+ prNumber,
4913
+ githubToken = null,
4914
+ allowAmbientGithubFallback = false,
4915
+ runCommand = defaultRunCommand2
4916
+ } = {}) {
4917
+ if (!repo || !Number.isInteger(prNumber) || prNumber <= 0) return null;
4918
+ const args = ["pr", "view", String(prNumber), "-R", String(repo), "--json", "headRefName"];
4919
+ try {
4920
+ const output = await runCommand("gh", args, worktreeDir, {
4921
+ env: githubToken ? installationTokenEnv(githubToken) : void 0,
4922
+ timeout: 6e4
4923
+ });
4924
+ return parseHeadRefName(output);
4925
+ } catch (error) {
4926
+ if (!githubToken || !allowAmbientGithubFallback) throw error;
4927
+ }
4928
+ const fallback = await runCommand("gh", args, worktreeDir, { timeout: 6e4 });
4929
+ return parseHeadRefName(fallback);
4930
+ }
4931
+ async function prepareContinuationBranch(worktreeDir, {
4932
+ task,
4933
+ parentTask,
4934
+ githubToken = null,
4935
+ allowAmbientGithubFallback = false,
4936
+ runCommand = defaultRunCommand2
4937
+ } = {}) {
4938
+ const continuationBranch = task?.pr_branch || parentTask?.pr_branch || (parentTask?.pr_number ? await resolvePrHeadBranch(worktreeDir, {
4939
+ repo: task?.repo,
4940
+ prNumber: parentTask.pr_number,
4941
+ githubToken,
4942
+ allowAmbientGithubFallback,
4943
+ runCommand
4944
+ }) : null);
4945
+ if (!continuationBranch && (task?.pr_branch || parentTask?.pr_url || parentTask?.pr_number)) {
4946
+ throw new Error(`continuation PR branch could not be resolved for task ${task?.code_task_id || "unknown"}`);
4947
+ }
4948
+ if (!continuationBranch) return null;
4949
+ return restoreContinuationBranch(worktreeDir, continuationBranch, {
4950
+ env: githubToken ? installationTokenEnv(githubToken) : void 0,
4951
+ runCommand
4952
+ });
4953
+ }
4954
+ var init_resume_branch = __esm({
4955
+ "../../scripts/virtual-office/code-runner/resume-branch.mjs"() {
4956
+ "use strict";
4957
+ init_publish();
4958
+ init_process_runner2();
4959
+ }
4960
+ });
4961
+
4468
4962
  // ../../scripts/virtual-office/code-runner/dispatch-onboarding.mjs
4469
4963
  function buildDispatchOnboarding({ repo = "Algosuite-ai/Nexus" } = {}) {
4470
4964
  const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
@@ -4545,6 +5039,7 @@ var init_dispatch_onboarding = __esm({
4545
5039
  "A handoff or roadmap line is a CLAIM, not evidence \u2014 verify shipped state against `git show origin/main:<path>`, never the stale local main tree.",
4546
5040
  `MANDATORY FOR EVERY VO PR (cloud-run/vo-*, packages/vo-mcp, packages/consensus-engine, packages/vo-ratchets, packages/vo-arch-defaults, scripts/virtual-office, vo-claude-plugin): record a dated Change-log entry IN THE SAME PR via EITHER appending to the "\xA7 10 Change log" of docs/vo/vo-roadmap-2026-05-26.md OR (PREFERRED) creating docs/vo/roadmap-log/<YYYY-MM-DD>-<short-slug>.md (fragments avoid conflicts when PRs ship concurrently) and flip any status the work shipped. CI enforces this (check-vo-roadmap-discipline.mjs); bypass ONLY via "VO-ROADMAP-ALLOW: <reason>" in the PR body. The roadmap is the single source of truth \u2014 if you didn't update it, you didn't ship. Finish line = MERGED + DEPLOYED + LIVE-VERIFIED.`,
4547
5041
  "Every UI change ships against docs/current/ui-trust-standard.md and adds VO QA tester coverage; verify in a real browser, not selector-presence.",
5042
+ "UNATTENDED VERIFICATION: no human can approve shell prompts. Run `pnpm ...` directly from the worktree root. For a standalone nested project with its own pnpm-lock.yaml, use `pnpm --dir <project> install --frozen-lockfile --prefer-offline --ignore-scripts --config.confirmModulesPurge=false`, then `pnpm --dir <project> ...` for its focused tests/type-check. These two pnpm forms are pre-authorized; do not skip local verification or wait for approval.",
4548
5043
  'PR \u2192 LIVE is YOUR job end-to-end \u2014 the operator must NEVER be the one to discover a red PR or a backed-up deploy. Own every PR from branch \u2192 CI \u2192 merge \u2192 functions deploy \u2192 LIVE-VERIFIED. "Done" = the functions you changed are actually SERVING in prod in every region; prove it with `node scripts/ci/prove-pr-live.mjs --pr <N>` \u2014 a merge / green deploy checkmark / homepage 200 is NOT proof. If a function staled, re-deploy ONLY the affected functions (targeted), never a full deploy. If you hit a usage/rate limit, STOP cleanly with the PR obligation OPEN \u2014 the watchdog auto-resumes when it resets; do not abandon it. See docs/current/pr-live-stewardship-doctrine.md.',
4549
5044
  `CONTEXT DEPTH IS NOT A REASON TO STOP. "I'm deep in context / fresh context would be better / I'll checkpoint" is the SAME premature-stop failure as doing 20 minutes of work instead of 6 hours \u2014 there is no quality cliff before compaction and the harness carries work forward. Keep BUILDING until the task is genuinely DONE; delicate or fleet-governing work means be CAREFUL, not stop. The ONLY valid pauses are real blockers: an operator decision is required, a dependency is not merged, or a hard external wait.`
4550
5045
  ];
@@ -4648,9 +5143,9 @@ var init_task_prompt = __esm({
4648
5143
  import { homedir as homedir3 } from "node:os";
4649
5144
  import { join as join5 } from "node:path";
4650
5145
  import { readdir, readFile, unlink, writeFile } from "node:fs/promises";
4651
- import { createHash as createHash4 } from "node:crypto";
5146
+ import { createHash as createHash3 } from "node:crypto";
4652
5147
  function deriveUuid(seed) {
4653
- const h = createHash4("sha256").update(seed).digest("hex");
5148
+ const h = createHash3("sha256").update(seed).digest("hex");
4654
5149
  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)}`;
4655
5150
  }
4656
5151
  function spoolToCloud(record, ids) {
@@ -4687,9 +5182,9 @@ async function readSpool(spoolDir = SPOOL_DIR) {
4687
5182
  }
4688
5183
  return out;
4689
5184
  }
4690
- async function readCloudMap(path12) {
5185
+ async function readCloudMap(path16) {
4691
5186
  try {
4692
- return JSON.parse(await readFile(path12, "utf8"));
5187
+ return JSON.parse(await readFile(path16, "utf8"));
4693
5188
  } catch {
4694
5189
  return {};
4695
5190
  }
@@ -4832,13 +5327,13 @@ var init_rate_limit_resume_scheduler_core = __esm({
4832
5327
  });
4833
5328
 
4834
5329
  // ../../scripts/virtual-office/code-runner/rate-limit-resume-scheduler.mjs
4835
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
5330
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
4836
5331
  import { dirname as dirname3, join as join6, resolve } from "node:path";
4837
5332
  function log(msg) {
4838
5333
  console.log(`[rate-limit-scheduler ${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
4839
5334
  }
4840
5335
  function readQueue(queuePath) {
4841
- if (!existsSync4(queuePath)) return [];
5336
+ if (!existsSync5(queuePath)) return [];
4842
5337
  const content = readFileSync2(queuePath, "utf-8");
4843
5338
  const lines = content.split("\n").filter((l) => l.trim());
4844
5339
  const entries = [];
@@ -4861,7 +5356,7 @@ function attemptsStorePath() {
4861
5356
  }
4862
5357
  function readAttemptsStore() {
4863
5358
  const p = attemptsStorePath();
4864
- if (!existsSync4(p)) return {};
5359
+ if (!existsSync5(p)) return {};
4865
5360
  try {
4866
5361
  const parsed = JSON.parse(readFileSync2(p, "utf-8"));
4867
5362
  return parsed && typeof parsed === "object" ? parsed : {};
@@ -5103,7 +5598,7 @@ var init_agent_availability = __esm({
5103
5598
  // ../../scripts/virtual-office/code-runner/account-usage.mjs
5104
5599
  import fs6 from "node:fs";
5105
5600
  import os from "node:os";
5106
- import path10 from "node:path";
5601
+ import path12 from "node:path";
5107
5602
  function readClaudeUsage({ homeDir = os.homedir(), read: rawRead = readJson } = {}) {
5108
5603
  const read = (p) => {
5109
5604
  try {
@@ -5112,7 +5607,7 @@ function readClaudeUsage({ homeDir = os.homedir(), read: rawRead = readJson } =
5112
5607
  return null;
5113
5608
  }
5114
5609
  };
5115
- const status = read(path10.join(homeDir, ".claude", "claude-usage.json"));
5610
+ const status = read(path12.join(homeDir, ".claude", "claude-usage.json"));
5116
5611
  if (status && (status.seven_day || status.five_hour)) {
5117
5612
  const entry = {
5118
5613
  agent: "claude",
@@ -5121,7 +5616,7 @@ function readClaudeUsage({ homeDir = os.homedir(), read: rawRead = readJson } =
5121
5616
  };
5122
5617
  if (entry.seven_day_used_pct !== null || entry.five_hour_used_pct !== null) return entry;
5123
5618
  }
5124
- const weekly = read(path10.join(homeDir, ".claude", "claude-weekly-usage.json"));
5619
+ const weekly = read(path12.join(homeDir, ".claude", "claude-weekly-usage.json"));
5125
5620
  if (weekly) {
5126
5621
  const entry = {
5127
5622
  agent: "claude",
@@ -5252,6 +5747,90 @@ var init_ci_repair_evidence = __esm({
5252
5747
  }
5253
5748
  });
5254
5749
 
5750
+ // ../../scripts/virtual-office/code-runner/pr-watcher-failure-confirmation.mjs
5751
+ function reset(entry) {
5752
+ delete entry.repairFailureFingerprint;
5753
+ delete entry.repairFailureObservations;
5754
+ delete entry.repairFailureFirstSeenAt;
5755
+ }
5756
+ function repairFailureFingerprint(pr) {
5757
+ if (pr?.ci !== "failing" || pr.hasPendingChecks) return null;
5758
+ const headSha = String(pr.headSha || "").trim().toLowerCase();
5759
+ const failedChecks = [...new Set((pr.failedChecks || []).map((name) => String(name).trim()).filter(Boolean))].sort();
5760
+ if (!headSha || failedChecks.length === 0) return null;
5761
+ return JSON.stringify({ headSha, failedChecks });
5762
+ }
5763
+ function observeRepairFailure(entry, pr, observedAt) {
5764
+ const fingerprint = repairFailureFingerprint(pr);
5765
+ if (!fingerprint) {
5766
+ reset(entry);
5767
+ return {
5768
+ confirmed: false,
5769
+ fingerprint: null,
5770
+ observations: 0,
5771
+ required: REPAIR_FAILURE_CONFIRMATIONS_REQUIRED,
5772
+ reason: pr?.hasPendingChecks ? "checks still pending" : pr?.ci === "failing" ? "missing exact head/check evidence" : "not failing"
5773
+ };
5774
+ }
5775
+ if (entry.repairFailureFingerprint === fingerprint) {
5776
+ entry.repairFailureObservations = Math.min(
5777
+ REPAIR_FAILURE_CONFIRMATIONS_REQUIRED,
5778
+ Math.max(1, Number(entry.repairFailureObservations) || 1) + 1
5779
+ );
5780
+ } else {
5781
+ entry.repairFailureFingerprint = fingerprint;
5782
+ entry.repairFailureObservations = 1;
5783
+ entry.repairFailureFirstSeenAt = observedAt;
5784
+ }
5785
+ return {
5786
+ confirmed: entry.repairFailureObservations >= REPAIR_FAILURE_CONFIRMATIONS_REQUIRED,
5787
+ fingerprint,
5788
+ observations: entry.repairFailureObservations,
5789
+ required: REPAIR_FAILURE_CONFIRMATIONS_REQUIRED,
5790
+ reason: entry.repairFailureObservations === 1 ? "first observation" : "stable failure confirmed"
5791
+ };
5792
+ }
5793
+ var REPAIR_FAILURE_CONFIRMATIONS_REQUIRED;
5794
+ var init_pr_watcher_failure_confirmation = __esm({
5795
+ "../../scripts/virtual-office/code-runner/pr-watcher-failure-confirmation.mjs"() {
5796
+ "use strict";
5797
+ REPAIR_FAILURE_CONFIRMATIONS_REQUIRED = 2;
5798
+ }
5799
+ });
5800
+
5801
+ // ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
5802
+ function supersededSourcePrNumber(prompt) {
5803
+ const text = String(prompt || "");
5804
+ if (text.includes(CI_FIX_MARKER)) {
5805
+ const match = text.match(/\bPR:\s*#(\d+)\b/u);
5806
+ return match ? Number(match[1]) : null;
5807
+ }
5808
+ const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
5809
+ if (repairMatch) return Number(repairMatch[1]);
5810
+ const recoverySupersedeMatch = text.match(
5811
+ /^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
5812
+ );
5813
+ if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
5814
+ const restoredContextMatch = text.match(
5815
+ /^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nThe VO 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
5816
+ );
5817
+ if (restoredContextMatch) {
5818
+ return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
5819
+ }
5820
+ const continuationMatch = text.match(
5821
+ /^(?: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
5822
+ );
5823
+ if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
5824
+ return Number(continuationMatch[1]);
5825
+ }
5826
+ var CI_FIX_MARKER;
5827
+ var init_superseded_pr_source = __esm({
5828
+ "../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
5829
+ "use strict";
5830
+ CI_FIX_MARKER = "[VO-CI-FIX]";
5831
+ }
5832
+ });
5833
+
5255
5834
  // ../../scripts/virtual-office/code-runner/pr-watcher.mjs
5256
5835
  import { homedir as homedir4 } from "node:os";
5257
5836
  import { join as join7 } from "node:path";
@@ -5294,6 +5873,7 @@ function parsePrCiStatus(view) {
5294
5873
  ci,
5295
5874
  failedChecks,
5296
5875
  failedCheckLinks,
5876
+ hasPendingChecks: pending,
5297
5877
  branch: view && view.headRefName || null,
5298
5878
  headSha: view && view.headRefOid || null,
5299
5879
  url: view && view.url || null,
@@ -5338,11 +5918,6 @@ function buildCiFixPrompt({ prNumber, repo, branch, headSha, failedChecks, prPat
5338
5918
  "```"
5339
5919
  ].join("\n");
5340
5920
  }
5341
- function ciFixSourcePrNumber(prompt) {
5342
- if (!String(prompt || "").includes(CI_FIX_MARKER)) return null;
5343
- const match = String(prompt).match(/\bPR:\s*#(\d+)\b/u);
5344
- return match ? Number(match[1]) : null;
5345
- }
5346
5921
  async function readState(stateFile) {
5347
5922
  try {
5348
5923
  const parsed = JSON.parse(await readFile2(stateFile, "utf8"));
@@ -5410,7 +5985,8 @@ async function runWatchCycle({
5410
5985
  checked += 1;
5411
5986
  const pr = parsePrCiStatus(view);
5412
5987
  entry.lastCi = pr.ci;
5413
- const action = decideWatchAction(
5988
+ const confirmation = observeRepairFailure(entry, pr, now());
5989
+ const proposedAction = decideWatchAction(
5414
5990
  pr,
5415
5991
  entry.fixAttempts,
5416
5992
  maxFixAttempts,
@@ -5418,6 +5994,10 @@ async function runWatchCycle({
5418
5994
  maxResumeAttempts,
5419
5995
  autoMergeEnabled
5420
5996
  );
5997
+ const action = proposedAction === "fix" && !confirmation.confirmed ? "wait" : proposedAction;
5998
+ if (proposedAction === "fix" && action === "wait") {
5999
+ log3(`watch: pr #${prNumber} CI repair waiting for stable exact-head failure ${confirmation.observations}/${confirmation.required} (${confirmation.reason})`);
6000
+ }
5421
6001
  if (action === "untrack") {
5422
6002
  delete state[prNumber];
5423
6003
  untracked += 1;
@@ -5545,12 +6125,14 @@ function makeWatchRunner({
5545
6125
  stateFile
5546
6126
  });
5547
6127
  }
5548
- var CI_FIX_MARKER, DEFAULT_STATE_FILE, FAIL_CONCLUSIONS, STALE_MS2, MAX_ENQUEUE_ERRORS;
6128
+ var DEFAULT_STATE_FILE, FAIL_CONCLUSIONS, STALE_MS2, MAX_ENQUEUE_ERRORS;
5549
6129
  var init_pr_watcher = __esm({
5550
6130
  "../../scripts/virtual-office/code-runner/pr-watcher.mjs"() {
5551
6131
  "use strict";
5552
6132
  init_ci_repair_evidence();
5553
- CI_FIX_MARKER = "[VO-CI-FIX]";
6133
+ init_pr_watcher_failure_confirmation();
6134
+ init_superseded_pr_source();
6135
+ init_superseded_pr_source();
5554
6136
  DEFAULT_STATE_FILE = join7(homedir4(), ".vo", "dispatched-prs.json");
5555
6137
  FAIL_CONCLUSIONS = /* @__PURE__ */ new Set([
5556
6138
  "FAILURE",
@@ -5626,9 +6208,9 @@ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
5626
6208
  res.end();
5627
6209
  return;
5628
6210
  }
5629
- const path12 = String(req.url || "").split("?")[0];
6211
+ const path16 = String(req.url || "").split("?")[0];
5630
6212
  res.setHeader("content-type", "application/json");
5631
- if (req.method === "GET" && path12 === "/status") {
6213
+ if (req.method === "GET" && path16 === "/status") {
5632
6214
  let status;
5633
6215
  try {
5634
6216
  status = getStatus();
@@ -5639,7 +6221,7 @@ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
5639
6221
  res.end(JSON.stringify({ ok: true, ...status }));
5640
6222
  return;
5641
6223
  }
5642
- if (req.method === "POST" && path12 === "/stop") {
6224
+ if (req.method === "POST" && path16 === "/stop") {
5643
6225
  if (!isControlOriginAllowed(req.headers.origin, allowedOrigin) || !req.headers["x-vo-control"]) {
5644
6226
  res.statusCode = 403;
5645
6227
  res.end(JSON.stringify({ ok: false, error: "forbidden" }));
@@ -5757,35 +6339,35 @@ var init_effort_mode_config = __esm({
5757
6339
  fast: {
5758
6340
  tier: "cheap",
5759
6341
  permissionMode: "acceptEdits",
5760
- maxTurns: 20,
6342
+ maxTurns: 80,
5761
6343
  thinkingDirective: "",
5762
6344
  multiAgentInstruction: ""
5763
6345
  },
5764
6346
  standard: {
5765
6347
  tier: "mid",
5766
6348
  permissionMode: "acceptEdits",
5767
- maxTurns: 40,
6349
+ maxTurns: 200,
5768
6350
  thinkingDirective: "",
5769
6351
  multiAgentInstruction: ""
5770
6352
  },
5771
6353
  deep: {
5772
6354
  tier: "best",
5773
6355
  permissionMode: "acceptEdits",
5774
- maxTurns: 60,
6356
+ maxTurns: 300,
5775
6357
  thinkingDirective: "Think step-by-step. Verify assumptions against source code. Check edge cases.",
5776
6358
  multiAgentInstruction: ""
5777
6359
  },
5778
6360
  ultra: {
5779
6361
  tier: "best",
5780
6362
  permissionMode: "acceptEdits",
5781
- maxTurns: 80,
6363
+ maxTurns: 500,
5782
6364
  thinkingDirective: "Think step-by-step. Exhaustively verify every assumption against source code and documentation. Adversarially review your own work.",
5783
6365
  multiAgentInstruction: "If this task needs multiple phases (research, build, verify), propose a plan first."
5784
6366
  },
5785
6367
  ultracode: {
5786
6368
  tier: "best",
5787
- permissionMode: "default",
5788
- maxTurns: 120,
6369
+ permissionMode: "acceptEdits",
6370
+ maxTurns: 800,
5789
6371
  thinkingDirective: "Think step-by-step. Exhaustively verify every assumption against source code and documentation. Build worked examples to validate correctness. Adversarially review your own work.",
5790
6372
  multiAgentInstruction: "Decompose this work into parallel research, build, and verification streams; use workflow orchestration where it helps."
5791
6373
  }
@@ -5796,7 +6378,7 @@ var init_effort_mode_config = __esm({
5796
6378
 
5797
6379
  // ../../scripts/virtual-office/model-registry.mjs
5798
6380
  import fs7 from "node:fs";
5799
- import path11 from "node:path";
6381
+ import path13 from "node:path";
5800
6382
  import { fileURLToPath } from "node:url";
5801
6383
  function uniqueModels(models = []) {
5802
6384
  return [...new Set(models.map((model) => String(model || "").trim()).filter(Boolean))];
@@ -5919,7 +6501,7 @@ function readCache(cacheFile = DEFAULT_CACHE_FILE, nowMs = Date.now(), ttlMs = D
5919
6501
  }
5920
6502
  }
5921
6503
  function writeCache(cacheFile = DEFAULT_CACHE_FILE, payload) {
5922
- fs7.mkdirSync(path11.dirname(cacheFile), { recursive: true });
6504
+ fs7.mkdirSync(path13.dirname(cacheFile), { recursive: true });
5923
6505
  fs7.writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
5924
6506
  }
5925
6507
  async function fetchRegistryCatalog({
@@ -5977,10 +6559,10 @@ var __dirname, ROOT, DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, DEFAULT_TTL_MS3, ANT
5977
6559
  var init_model_registry = __esm({
5978
6560
  "../../scripts/virtual-office/model-registry.mjs"() {
5979
6561
  "use strict";
5980
- __dirname = path11.dirname(fileURLToPath(import.meta.url));
5981
- ROOT = path11.resolve(__dirname, "..", "..");
5982
- DEFAULT_CACHE_DIR = path11.join(ROOT, ".virtual-office-cache", "model-registry");
5983
- DEFAULT_CACHE_FILE = path11.join(DEFAULT_CACHE_DIR, "catalog.json");
6562
+ __dirname = path13.dirname(fileURLToPath(import.meta.url));
6563
+ ROOT = path13.resolve(__dirname, "..", "..");
6564
+ DEFAULT_CACHE_DIR = path13.join(ROOT, ".virtual-office-cache", "model-registry");
6565
+ DEFAULT_CACHE_FILE = path13.join(DEFAULT_CACHE_DIR, "catalog.json");
5984
6566
  DEFAULT_TTL_MS3 = 60 * 60 * 1e3;
5985
6567
  ANTHROPIC_API_VERSION = "2023-06-01";
5986
6568
  FAMILY_DEFINITIONS = {
@@ -6560,9 +7142,9 @@ function operatorTierToRung(tier, difficulty, thresholds) {
6560
7142
  if (tier === "best" && difficulty >= thresholds.rungBounds.R5) return "R5";
6561
7143
  return base;
6562
7144
  }
6563
- function readCodexModelsCache({ path: path12 = DEFAULT_CODEX_MODELS_CACHE, read = readFileSync3 } = {}) {
7145
+ function readCodexModelsCache({ path: path16 = DEFAULT_CODEX_MODELS_CACHE, read = readFileSync3 } = {}) {
6564
7146
  try {
6565
- const parsed = JSON.parse(read(path12, "utf8"));
7147
+ const parsed = JSON.parse(read(path16, "utf8"));
6566
7148
  return Array.isArray(parsed?.models) ? parsed : null;
6567
7149
  } catch {
6568
7150
  return null;
@@ -6978,6 +7560,320 @@ var init_agent_process_env = __esm({
6978
7560
  }
6979
7561
  });
6980
7562
 
7563
+ // ../../scripts/virtual-office/code-runner/isolation-audit.mjs
7564
+ import fs8 from "node:fs";
7565
+ import fsp9 from "node:fs/promises";
7566
+ import path14 from "node:path";
7567
+ async function defaultRun(command, args, cwd, options = {}) {
7568
+ return runProcess2(command, args, { cwd, timeout: 6e4, ...options });
7569
+ }
7570
+ async function git2(run, cwd, args, options = {}) {
7571
+ return run("git", args, cwd, options);
7572
+ }
7573
+ async function canonicalRootForWorktree(worktreeDir, run) {
7574
+ const commonDir = String(await git2(run, worktreeDir, [
7575
+ "rev-parse",
7576
+ "--path-format=absolute",
7577
+ "--git-common-dir"
7578
+ ])).trim();
7579
+ const root = path14.dirname(commonDir);
7580
+ return samePath2(root, worktreeDir) ? null : root;
7581
+ }
7582
+ async function snapshot(root, run) {
7583
+ const [head, status] = await Promise.all([
7584
+ git2(run, root, ["rev-parse", "HEAD"]),
7585
+ git2(run, root, ["-c", "core.quotepath=false", "status", "--porcelain=v1", "-z"], { raw: true })
7586
+ ]);
7587
+ return { head: String(head).trim(), status: String(status) };
7588
+ }
7589
+ async function isVerifiedRemoteFastForward(baseline, current, run) {
7590
+ if (current.status) return false;
7591
+ try {
7592
+ const branch = String(await git2(run, baseline.root, ["branch", "--show-current"])).trim();
7593
+ if (branch !== "main") return false;
7594
+ await git2(run, baseline.root, ["fetch", "--quiet", "origin", "main"]);
7595
+ const remoteHead = String(await git2(run, baseline.root, ["rev-parse", "FETCH_HEAD"])).trim();
7596
+ await git2(run, baseline.root, ["merge-base", "--is-ancestor", baseline.head, current.head]);
7597
+ await git2(run, baseline.root, ["merge-base", "--is-ancestor", current.head, remoteHead]);
7598
+ return true;
7599
+ } catch {
7600
+ return false;
7601
+ }
7602
+ }
7603
+ async function captureCanonicalBaseline(worktreeDir, { run = defaultRun } = {}) {
7604
+ const root = await canonicalRootForWorktree(worktreeDir, run);
7605
+ if (!root) return { root: null, head: null, status: "", standalone: true };
7606
+ const state = await snapshot(root, run);
7607
+ if (state.status) {
7608
+ throw new Error(`canonical clone is dirty before agent launch; refusing task execution: ${root}`);
7609
+ }
7610
+ return { root, ...state };
7611
+ }
7612
+ async function changedPaths(root, run) {
7613
+ const [tracked, untracked] = await Promise.all([
7614
+ git2(run, root, ["-c", "core.quotepath=false", "diff", "--name-only", "-z", "HEAD"], { raw: true }),
7615
+ git2(run, root, ["-c", "core.quotepath=false", "ls-files", "--others", "--exclude-standard", "-z"], { raw: true })
7616
+ ]);
7617
+ return { tracked: splitZ2(tracked), untracked: splitZ2(untracked) };
7618
+ }
7619
+ async function quarantineCanonicalWrites({ baseline, worktreeDir, taskId, run, now }) {
7620
+ const paths = await changedPaths(baseline.root, run);
7621
+ const quarantineDir = path14.join(
7622
+ path14.dirname(worktreeDir),
7623
+ ".canonical-recovery",
7624
+ `${String(taskId || "unknown").replace(/[^a-z0-9-]/gi, "-")}-${now().toISOString().replace(/[:.]/g, "-")}`
7625
+ );
7626
+ await fsp9.mkdir(quarantineDir, { recursive: true });
7627
+ const patch = await git2(run, baseline.root, ["diff", "--binary", "HEAD"], { raw: true });
7628
+ await fsp9.writeFile(path14.join(quarantineDir, "tracked.patch"), patch, "utf8");
7629
+ for (const relative of paths.untracked) {
7630
+ const source = path14.join(baseline.root, relative);
7631
+ const target = path14.join(quarantineDir, "untracked", relative);
7632
+ await fsp9.mkdir(path14.dirname(target), { recursive: true });
7633
+ await fsp9.copyFile(source, target);
7634
+ }
7635
+ await fsp9.writeFile(path14.join(quarantineDir, "manifest.json"), `${JSON.stringify({
7636
+ taskId,
7637
+ canonicalRoot: baseline.root,
7638
+ canonicalHead: baseline.head,
7639
+ tracked: paths.tracked,
7640
+ untracked: paths.untracked
7641
+ }, null, 2)}
7642
+ `, "utf8");
7643
+ return { quarantineDir, ...paths };
7644
+ }
7645
+ async function restoreExactCanonicalPaths(baseline, evidence, run) {
7646
+ if (evidence.tracked.length > 0) {
7647
+ await git2(run, baseline.root, [
7648
+ "restore",
7649
+ `--source=${baseline.head}`,
7650
+ "--staged",
7651
+ "--worktree",
7652
+ "--",
7653
+ ...evidence.tracked
7654
+ ]);
7655
+ }
7656
+ for (const relative of evidence.untracked) {
7657
+ const target = path14.resolve(baseline.root, relative);
7658
+ const prefix = `${path14.resolve(baseline.root)}${path14.sep}`;
7659
+ if (!target.startsWith(prefix) || !fs8.existsSync(target)) continue;
7660
+ await fsp9.rm(target, { force: true });
7661
+ }
7662
+ }
7663
+ async function assertCanonicalIsolation(baseline, { worktreeDir, taskId, run = defaultRun, now = () => /* @__PURE__ */ new Date() } = {}) {
7664
+ if (baseline.standalone) return { ok: true, standalone: true };
7665
+ const current = await snapshot(baseline.root, run);
7666
+ if (current.head === baseline.head && !current.status) return { ok: true };
7667
+ if (current.head !== baseline.head) {
7668
+ if (await isVerifiedRemoteFastForward(baseline, current, run)) {
7669
+ return {
7670
+ ok: true,
7671
+ canonicalFastForward: true,
7672
+ fromHead: baseline.head,
7673
+ toHead: current.head
7674
+ };
7675
+ }
7676
+ throw new Error(`canonical clone HEAD changed during task ${taskId}; manual recovery required: ${baseline.root}`);
7677
+ }
7678
+ const evidence = await quarantineCanonicalWrites({ baseline, worktreeDir, taskId, run, now });
7679
+ await restoreExactCanonicalPaths(baseline, evidence, run);
7680
+ const restored = await snapshot(baseline.root, run);
7681
+ if (restored.head !== baseline.head || restored.status) {
7682
+ throw new Error(`canonical clone recovery could not restore the exact baseline; evidence: ${evidence.quarantineDir}`);
7683
+ }
7684
+ throw new Error(
7685
+ `agent attempted ${evidence.tracked.length + evidence.untracked.length} canonical-clone write(s); writes were quarantined and the clone was restored exactly: ${evidence.quarantineDir}`
7686
+ );
7687
+ }
7688
+ var splitZ2, samePath2;
7689
+ var init_isolation_audit = __esm({
7690
+ "../../scripts/virtual-office/code-runner/isolation-audit.mjs"() {
7691
+ "use strict";
7692
+ init_process_runner2();
7693
+ splitZ2 = (value) => String(value || "").split("\0").map((item) => item.trim()).filter(Boolean);
7694
+ samePath2 = (left, right) => {
7695
+ const [a, b] = [left, right].map((value) => path14.resolve(value));
7696
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
7697
+ };
7698
+ }
7699
+ });
7700
+
7701
+ // ../../scripts/virtual-office/code-runner/recovery-ledger.mjs
7702
+ import fs9 from "node:fs";
7703
+ import fsp10 from "node:fs/promises";
7704
+ import path15 from "node:path";
7705
+ function recoveryTaskId(prompt) {
7706
+ const match = String(prompt || "").match(/VO_RECOVERY_FROM_CODE_TASK:\s*([0-9a-f-]{36})/i);
7707
+ return match ? match[1].toLowerCase() : null;
7708
+ }
7709
+ function cloneLeaf(repo) {
7710
+ const [owner, name] = String(repo || "").split("/");
7711
+ if (!owner || !name) return null;
7712
+ const clean = (value) => value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
7713
+ return `${clean(owner)}__${clean(name)}`;
7714
+ }
7715
+ function recoveryLedgerCandidates(repo, clonesRoot2) {
7716
+ const leaf = cloneLeaf(repo);
7717
+ if (!leaf || !clonesRoot2) return [];
7718
+ const canonical = path15.join(clonesRoot2, leaf);
7719
+ return [
7720
+ path15.join(clonesRoot2, ".agent-worktrees", leaf, "recovery-ledger.jsonl"),
7721
+ path15.join(canonical, ".agent-worktrees", "recovery-ledger.jsonl")
7722
+ ];
7723
+ }
7724
+ async function readLedger(file, readFile3) {
7725
+ try {
7726
+ return String(await readFile3(file, "utf8")).split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
7727
+ } catch {
7728
+ return [];
7729
+ }
7730
+ }
7731
+ async function findPreservedRecovery(task, {
7732
+ clonesRoot: clonesRoot2 = process.env.VO_CODE_RUNNER_CLONES_ROOT || "",
7733
+ readFile: readFile3 = fsp10.readFile,
7734
+ exists = fs9.existsSync
7735
+ } = {}) {
7736
+ const originalTaskId = recoveryTaskId(task.prompt);
7737
+ if (!originalTaskId) return null;
7738
+ for (const ledgerPath of recoveryLedgerCandidates(task.repo, clonesRoot2)) {
7739
+ const entries = await readLedger(ledgerPath, readFile3);
7740
+ const resolved = entries.some((entry) => RESOLVED_RECOVERY_TYPES.has(entry.type) && entry.taskId === originalTaskId);
7741
+ const preserved = [...entries].reverse().find((entry) => entry.taskId === originalTaskId && entry.worktreeDir);
7742
+ if (!resolved && preserved && exists(preserved.worktreeDir)) {
7743
+ return { originalTaskId, ledgerPath, preserved };
7744
+ }
7745
+ }
7746
+ return null;
7747
+ }
7748
+ async function recoverPreservedCodeTask({
7749
+ task,
7750
+ cfg,
7751
+ client,
7752
+ log: log3,
7753
+ find = findPreservedRecovery,
7754
+ listChanged = listChangedFilesAsync,
7755
+ listCommitted = listCommittedFilesAsync,
7756
+ openPr = openCodeTaskPrAsync,
7757
+ appendFile = fsp10.appendFile,
7758
+ track = trackDispatchedPr
7759
+ } = {}) {
7760
+ const recovery = await find(task);
7761
+ if (!recovery) return null;
7762
+ const cwd = recovery.preserved.worktreeDir;
7763
+ let files = (await listChanged(cwd)).filter((file) => !isAgentScratch(file));
7764
+ let alreadyCommitted = false;
7765
+ if (files.length === 0) {
7766
+ files = (await listCommitted(cwd)).filter((file) => !isAgentScratch(file));
7767
+ alreadyCommitted = files.length > 0;
7768
+ }
7769
+ if (files.length === 0) {
7770
+ await appendFile(recovery.ledgerPath, `${JSON.stringify({
7771
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7772
+ type: "recovery_skipped_no_files",
7773
+ taskId: recovery.originalTaskId,
7774
+ resumedTaskId: task.code_task_id,
7775
+ worktreeDir: cwd
7776
+ })}
7777
+ `, "utf8");
7778
+ log3(`task ${task.code_task_id}: preserved task ${recovery.originalTaskId} has no recoverable files; starting fresh`);
7779
+ return null;
7780
+ }
7781
+ const token2 = (await client.getInstallationToken({ required: cfg.requireGithubAppAuth }))?.token ?? null;
7782
+ const run = { summary: `Recovered preserved work from task ${recovery.originalTaskId}.` };
7783
+ const supersedesPrNumber = supersededSourcePrNumber(recovery.preserved.prompt || task.prompt);
7784
+ const targetBranch = supersedesPrNumber && task.pr_branch ? task.pr_branch : null;
7785
+ const pr = await openPr(cwd, files, {
7786
+ title: `code-task recovery: ${String(task.prompt).split("\n").find((line) => line && !line.startsWith(RECOVERY_MARKER)) || task.prompt}`,
7787
+ body: buildPrBody(task, run, files, { armAutoMerge: cfg.armAutoMerge }),
7788
+ alreadyCommitted,
7789
+ githubToken: token2,
7790
+ allowAmbientGithubFallback: cfg.allowAmbientGithub,
7791
+ draft: false,
7792
+ armAutoMerge: false,
7793
+ supersedesPrNumber,
7794
+ targetBranch
7795
+ });
7796
+ await appendFile(recovery.ledgerPath, `${JSON.stringify({
7797
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7798
+ type: "recovered",
7799
+ taskId: recovery.originalTaskId,
7800
+ resumedTaskId: task.code_task_id,
7801
+ prUrl: pr.prUrl,
7802
+ prNumber: pr.prNumber
7803
+ })}
7804
+ `, "utf8");
7805
+ await client.postProgress(task.code_task_id, {
7806
+ status: "pr_opened",
7807
+ stage: "opening_pr",
7808
+ pr_url: pr.prUrl,
7809
+ pr_number: pr.prNumber,
7810
+ message: `recovered preserved work from ${recovery.originalTaskId} and opened ${pr.prUrl}`,
7811
+ result: run.summary
7812
+ });
7813
+ log3(`task ${task.code_task_id} recovered ${recovery.originalTaskId} \u2192 PR ${pr.prUrl}`);
7814
+ if (cfg.watchEnabled) {
7815
+ await track({
7816
+ prNumber: pr.prNumber,
7817
+ repo: task.repo,
7818
+ branch: pr.branch,
7819
+ taskId: task.code_task_id,
7820
+ operatorId: task.operator_id,
7821
+ tenantId: task.tenant_id,
7822
+ needsContinuation: false,
7823
+ allowFixDispatch: !String(task.prompt || "").includes(CI_FIX_MARKER)
7824
+ }).catch((e) => log3(`watch: track failed for recovered #${pr.prNumber}: ${e.message}`));
7825
+ }
7826
+ return { ...pr, files, originalTaskId: recovery.originalTaskId };
7827
+ }
7828
+ var RECOVERY_MARKER, RESOLVED_RECOVERY_TYPES;
7829
+ var init_recovery_ledger = __esm({
7830
+ "../../scripts/virtual-office/code-runner/recovery-ledger.mjs"() {
7831
+ "use strict";
7832
+ init_publish();
7833
+ init_publish_async();
7834
+ init_task_helpers();
7835
+ init_pr_watcher();
7836
+ init_superseded_pr_source();
7837
+ RECOVERY_MARKER = "VO_RECOVERY_FROM_CODE_TASK:";
7838
+ RESOLVED_RECOVERY_TYPES = /* @__PURE__ */ new Set(["recovered", "recovery_skipped_no_files"]);
7839
+ }
7840
+ });
7841
+
7842
+ // ../../scripts/virtual-office/code-runner/no-changes-terminal-status.mjs
7843
+ function isMaxTurnExhaustion(run = {}, maxTurns) {
7844
+ const summary = String(run.summary || "").trim().toLowerCase();
7845
+ if (summary === "error_max_turns" || summary === "inconclusive_max_turns") return true;
7846
+ return Number.isInteger(maxTurns) && maxTurns > 0 && Number.isInteger(run.numTurns) && run.numTurns > maxTurns;
7847
+ }
7848
+ function decideNoChangesTerminalStatus({ partial, run = {}, maxTurns } = {}) {
7849
+ if (!partial) {
7850
+ return {
7851
+ status: "no_changes_needed",
7852
+ message: "agent completed \u2014 no change needed (already fixed / nothing to do)",
7853
+ result: String(run.summary || "no_changes_needed").slice(0, RESULT_LIMIT)
7854
+ };
7855
+ }
7856
+ if (isMaxTurnExhaustion(run, maxTurns)) {
7857
+ return {
7858
+ status: "failed",
7859
+ message: "agent reached the max-turn limit before producing a verified change",
7860
+ result: "inconclusive_max_turns"
7861
+ };
7862
+ }
7863
+ return {
7864
+ status: "failed",
7865
+ message: "agent made no file changes",
7866
+ result: "no_changes"
7867
+ };
7868
+ }
7869
+ var RESULT_LIMIT;
7870
+ var init_no_changes_terminal_status = __esm({
7871
+ "../../scripts/virtual-office/code-runner/no-changes-terminal-status.mjs"() {
7872
+ "use strict";
7873
+ RESULT_LIMIT = 2e3;
7874
+ }
7875
+ });
7876
+
6981
7877
  // ../../scripts/virtual-office/code-runner-daemon.mjs
6982
7878
  var code_runner_daemon_exports = {};
6983
7879
  __export(code_runner_daemon_exports, {
@@ -7027,10 +7923,23 @@ async function processOneTask(client, task, cfg) {
7027
7923
  let worktreeName = "";
7028
7924
  let preserveReason = null;
7029
7925
  try {
7926
+ if (await recoverPreservedCodeTask({ task, cfg, client, log: log2 })) return;
7030
7927
  await safeProgress(client, id, runnerStagePatch("preparing_worktree", `${cfg.runnerId} preparing an isolated worktree for ${task.repo}`));
7031
7928
  const wt = await Promise.resolve(createFixWorktree("code-task", { source: id.slice(0, 8), repo: task.repo }));
7032
7929
  worktreeName = wt.worktreeName;
7033
7930
  if (!worktreeName || !wt.worktreeDir) throw new Error("worktree isolation failure \u2014 refusing to run in the main tree");
7931
+ const githubToken = (await client.getInstallationToken({ required: cfg.requireGithubAppAuth }))?.token ?? null;
7932
+ const parentTask = task.resumed_from ? await client.getTask(task.resumed_from).catch(() => null) : null;
7933
+ const continuationRestore = await prepareContinuationBranch(wt.worktreeDir, {
7934
+ task,
7935
+ parentTask,
7936
+ githubToken,
7937
+ allowAmbientGithubFallback: cfg.allowAmbientGithub
7938
+ });
7939
+ if (continuationRestore) {
7940
+ log2(`task ${id}: restored continuation branch ${continuationRestore.remoteBranch} into ${continuationRestore.localBranch}`);
7941
+ }
7942
+ const canonicalBaseline = await captureCanonicalBaseline(wt.worktreeDir);
7034
7943
  const sel = resolveTaskRunner(task, cfg, process.env, { warn: (m) => log2(`agent-select: ${m}`) });
7035
7944
  const { dispatchMode, routerMode, tier, model, permissionMode: effectivePermissionMode, maxTurns: effectiveMaxTurns, effort: effectiveEffort, maxBudgetUsd: effectiveMaxBudgetUsd, prompt: effortPrompt, routerDecision } = await resolveEffortDispatch({ client, task, agent: sel.agent, env: process.env, basePrompt: await composeCodeTaskPrompt(client, task, {
7036
7945
  log: log2,
@@ -7063,6 +7972,7 @@ async function processOneTask(client, task, cfg) {
7063
7972
  cancelPollMs: cfg.cancelPollMs,
7064
7973
  maxWallClockMs: cfg.maxWallClockMs
7065
7974
  });
7975
+ await assertCanonicalIsolation(canonicalBaseline, { worktreeDir: wt.worktreeDir, taskId: id });
7066
7976
  if (run.killed) {
7067
7977
  preserveReason = "cancelled by operator \u2014 work preserved for recovery";
7068
7978
  log2(`task ${id} cancelled by operator`);
@@ -7113,10 +8023,9 @@ async function processOneTask(client, task, cfg) {
7113
8023
  log2(`task ${id}: dropped ${scratch.length} scratch file(s): ${scratch.join(", ")}`);
7114
8024
  }
7115
8025
  if (files.length === 0) {
8026
+ const terminal = decideNoChangesTerminalStatus({ partial, run, maxTurns: effectiveMaxTurns });
7116
8027
  await safeProgress(client, id, {
7117
- status: partial ? "failed" : "no_changes_needed",
7118
- message: partial ? "agent made no file changes" : "agent completed \u2014 no change needed (already fixed / nothing to do)",
7119
- result: partial ? "no_changes" : String(run.summary || "no_changes_needed").slice(0, 2e3),
8028
+ ...terminal,
7120
8029
  cost_usd: numOrUndef(run.costUsd),
7121
8030
  num_turns: numOrUndef(run.numTurns)
7122
8031
  });
@@ -7129,7 +8038,6 @@ async function processOneTask(client, task, cfg) {
7129
8038
  return;
7130
8039
  }
7131
8040
  await safeProgress(client, id, runnerStagePatch("opening_pr", `opening PR for ${files.length} changed file(s)`));
7132
- const githubToken = (await client.getInstallationToken({ required: cfg.requireGithubAppAuth }))?.token ?? null;
7133
8041
  const pr = await openCodeTaskPrAsync(wt.worktreeDir, files, {
7134
8042
  title: `${partial ? `${partialPrTitlePrefix(run)} \u2014 ` : ""}code-task: ${task.prompt}`,
7135
8043
  body: buildPrBody(task, run, files, { armAutoMerge: cfg.armAutoMerge }),
@@ -7139,19 +8047,22 @@ async function processOneTask(client, task, cfg) {
7139
8047
  draft: partial,
7140
8048
  armAutoMerge: false,
7141
8049
  // watcher waits for green CI, then uses exact-SHA consensus merge
7142
- supersedesPrNumber: ciFixSourcePrNumber(task.prompt)
8050
+ targetBranch: continuationRestore?.remoteBranch || void 0,
8051
+ supersedesPrNumber: supersededSourcePrNumber(task.prompt)
7143
8052
  });
7144
8053
  const autoMergeMessage = pr.autoMergeArmed ? "; auto-merge armed" : pr.autoMergeError ? `; auto-merge arm failed: ${String(pr.autoMergeError).slice(0, 220)}` : "";
8054
+ const cleanupMessage = pr.supersededPrCloseError ? `; replacement published, source PR cleanup pending: ${String(pr.supersededPrCloseError).slice(0, 180)}` : "";
7145
8055
  await safeProgress(client, id, {
7146
8056
  status: "pr_opened",
7147
- message: `opened ${pr.prUrl}${autoMergeMessage}`,
8057
+ message: `opened ${pr.prUrl}${autoMergeMessage}${cleanupMessage}`,
7148
8058
  pr_url: pr.prUrl,
7149
8059
  pr_number: pr.prNumber,
8060
+ pr_branch: pr.branch,
7150
8061
  result: String(run.summary).slice(0, 2e3),
7151
8062
  cost_usd: numOrUndef(run.costUsd),
7152
8063
  num_turns: numOrUndef(run.numTurns)
7153
8064
  });
7154
- log2(`task ${id} \u2192 PR ${pr.prUrl}${autoMergeMessage}`);
8065
+ log2(`task ${id} \u2192 PR ${pr.prUrl}${autoMergeMessage}${cleanupMessage}`);
7155
8066
  if (cfg.watchEnabled) {
7156
8067
  await trackDispatchedPr({
7157
8068
  prNumber: pr.prNumber,
@@ -7280,13 +8191,14 @@ var init_code_runner_daemon = __esm({
7280
8191
  init_resolve_runner();
7281
8192
  init_rate_limit_resume();
7282
8193
  init_publish();
7283
- init_publish();
7284
8194
  init_publish_async();
8195
+ init_resume_branch();
7285
8196
  init_task_prompt();
7286
8197
  init_loop_ticks();
7287
8198
  init_agent_availability();
7288
8199
  init_account_usage();
7289
8200
  init_pr_watcher();
8201
+ init_superseded_pr_source();
7290
8202
  init_watch_cycle_coordinator();
7291
8203
  init_control_server();
7292
8204
  init_apply_effort_mode();
@@ -7294,6 +8206,9 @@ var init_code_runner_daemon = __esm({
7294
8206
  init_reconnect_backoff();
7295
8207
  init_task_helpers();
7296
8208
  init_agent_process_env();
8209
+ init_isolation_audit();
8210
+ init_recovery_ledger();
8211
+ init_no_changes_terminal_status();
7297
8212
  RATE_LIMIT_RESUME_ENABLED = process.env.VO_RATE_LIMIT_RESUME === "1";
7298
8213
  parseList = (s) => String(s || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
7299
8214
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));