@rallycry/conveyor-agent 10.0.0 → 10.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -116,6 +116,44 @@ function applyBootstrapToEnv(config) {
116
116
 
117
117
  // src/connection/agent-connection.ts
118
118
  import { io } from "socket.io-client";
119
+
120
+ // src/setup/bootstrap-poll.ts
121
+ var PollUntilBoundHttpError = class extends Error {
122
+ constructor(status) {
123
+ super(`pollUntilBound got unexpected status ${status}`);
124
+ this.status = status;
125
+ this.name = "PollUntilBoundHttpError";
126
+ }
127
+ status;
128
+ };
129
+ async function sleep2(ms) {
130
+ await new Promise((resolve) => {
131
+ setTimeout(resolve, ms);
132
+ });
133
+ }
134
+ async function pollUntilBound(opts) {
135
+ const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
136
+ const maxWaitMs = opts.maxWaitMs ?? 30 * 60 * 1e3;
137
+ const deadline = Date.now() + maxWaitMs;
138
+ while (true) {
139
+ const response = await fetch(`${opts.apiUrl}/api/v3/pods/bootstrap`, {
140
+ headers: { Authorization: `Bearer ${opts.bootstrapToken}` }
141
+ });
142
+ if (response.status === 200) {
143
+ return await response.json();
144
+ }
145
+ if (response.status === 204) {
146
+ if (Date.now() >= deadline) {
147
+ throw new Error(`pollUntilBound timed out after ${maxWaitMs}ms waiting for pod bind`);
148
+ }
149
+ await sleep2(pollIntervalMs);
150
+ continue;
151
+ }
152
+ throw new PollUntilBoundHttpError(response.status);
153
+ }
154
+ }
155
+
156
+ // src/connection/agent-connection.ts
119
157
  var EVENT_BATCH_MS = 500;
120
158
  var MAX_EVENT_BUFFER = 5e3;
121
159
  var TOKEN_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
@@ -147,6 +185,7 @@ var AgentConnection = class _AgentConnection {
147
185
  modeChangeCallback = null;
148
186
  apiKeyUpdateCallback = null;
149
187
  pullBranchCallback = null;
188
+ finalizeSnapshotCallback = null;
150
189
  earlyPullBranches = [];
151
190
  // PTY relay (S5 terminal). Single-slot callbacks, set per PtySession run.
152
191
  ptyInputCallback = null;
@@ -301,6 +340,9 @@ var AgentConnection = class _AgentConnection {
301
340
  if (this.pullBranchCallback) this.pullBranchCallback(data);
302
341
  else this.earlyPullBranches.push(data);
303
342
  });
343
+ this.socket.on("session:finalizeSnapshot", () => {
344
+ this.finalizeSnapshotCallback?.();
345
+ });
304
346
  this.socket.on("pty:input", (data) => {
305
347
  if (data.sessionId && data.sessionId !== this.config.sessionId) return;
306
348
  this.ptyInputCallback?.(data.data);
@@ -523,6 +565,9 @@ var AgentConnection = class _AgentConnection {
523
565
  for (const data of this.earlyPullBranches) callback(data);
524
566
  this.earlyPullBranches = [];
525
567
  }
568
+ onFinalizeSnapshot(callback) {
569
+ this.finalizeSnapshotCallback = callback;
570
+ }
526
571
  // ── PTY relay (S5 Connected-TUI terminal) ──────────────────────────
527
572
  /**
528
573
  * Forward a raw chunk of terminal output to the S2 relay (fire-and-forget).
@@ -671,6 +716,12 @@ var AgentConnection = class _AgentConnection {
671
716
  }
672
717
  );
673
718
  }
719
+ /** Report the full current set of runtime-discovered listening ports.
720
+ * Throws on failure so the PortDiscovery poller can retry on its next
721
+ * tick (a swallowed error here would silently drop the delta). */
722
+ async reportDiscoveredPorts(ports) {
723
+ await this.call("reportDiscoveredPorts", { sessionId: this.config.sessionId, ports });
724
+ }
674
725
  // ── Typing indicators ───────────────────────────────────────────────
675
726
  sendTypingStart() {
676
727
  this.sendEvent({ type: "agent_typing_start" });
@@ -738,15 +789,27 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
738
789
  const result = await this.refreshFromBootstrap();
739
790
  return result.refreshedTaskToken;
740
791
  }
741
- async refreshFromBootstrap() {
792
+ refreshFromBootstrap() {
793
+ const none = Promise.resolve({ refreshedClaude: false, refreshedTaskToken: false });
794
+ const podBootstrapToken = process.env.POD_BOOTSTRAP_TOKEN;
742
795
  const codespaceName = process.env.CODESPACE_NAME || process.env.CLAUDESPACE_NAME;
743
796
  const apiUrl = this.config.apiUrl;
744
- if (!codespaceName || !apiUrl) return { refreshedClaude: false, refreshedTaskToken: false };
797
+ if (!apiUrl || !podBootstrapToken && !codespaceName) {
798
+ return none;
799
+ }
745
800
  const now = Date.now();
746
801
  if (now - this.lastTaskTokenRefreshAt < 6e4) {
747
- return { refreshedClaude: false, refreshedTaskToken: false };
802
+ return none;
748
803
  }
749
804
  this.lastTaskTokenRefreshAt = now;
805
+ if (podBootstrapToken) {
806
+ return this.refreshFromV3Bootstrap(apiUrl, podBootstrapToken);
807
+ }
808
+ if (!codespaceName) return none;
809
+ return this.refreshFromCodespaceBootstrap(apiUrl, codespaceName);
810
+ }
811
+ /** Legacy GitHub Codespaces refresh path — keys on instance name. */
812
+ async refreshFromCodespaceBootstrap(apiUrl, codespaceName) {
750
813
  const bootstrapToken = process.env.CONVEYOR_BOOTSTRAP_TOKEN;
751
814
  const result = await fetchBootstrap({
752
815
  apiUrl,
@@ -772,6 +835,61 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
772
835
  const refreshedClaude = Boolean(result.config.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
773
836
  return { refreshedClaude, refreshedTaskToken };
774
837
  }
838
+ /**
839
+ * v3 refresh: re-fetch the full bootstrap bundle from the pod's bound v3
840
+ * route and swap the credentials in place. The GitHub installation token
841
+ * dies at ~1h and the sessionJwt at 24h; re-polling the bootstrap GET with
842
+ * the same pod token is the designed refresh mechanism.
843
+ */
844
+ async refreshFromV3Bootstrap(apiUrl, bootstrapToken) {
845
+ const bundle = await this.pollBundleWithRateLimitRetry(apiUrl, bootstrapToken);
846
+ if (!bundle) {
847
+ return { refreshedClaude: false, refreshedTaskToken: false };
848
+ }
849
+ const previousTaskToken = process.env.CONVEYOR_TASK_TOKEN;
850
+ for (const [key, value] of Object.entries(bundle.envVars ?? {})) {
851
+ process.env[key] = value;
852
+ }
853
+ if (bundle.githubToken) process.env.CONVEYOR_GITHUB_TOKEN = bundle.githubToken;
854
+ if (bundle.anthropicKey) process.env.ANTHROPIC_API_KEY = bundle.anthropicKey;
855
+ if (bundle.gcpToken) process.env.CLOUDSDK_AUTH_ACCESS_TOKEN = bundle.gcpToken;
856
+ const refreshedTaskToken = Boolean(bundle.sessionJwt) && bundle.sessionJwt !== previousTaskToken;
857
+ if (refreshedTaskToken) {
858
+ process.env.CONVEYOR_TASK_TOKEN = bundle.sessionJwt;
859
+ this.config.taskToken = bundle.sessionJwt;
860
+ if (this.socket) {
861
+ const auth = this.socket.auth;
862
+ if (auth && typeof auth === "object") {
863
+ auth.taskToken = bundle.sessionJwt;
864
+ }
865
+ }
866
+ }
867
+ const refreshedClaude = Boolean(bundle.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
868
+ return { refreshedClaude, refreshedTaskToken };
869
+ }
870
+ /**
871
+ * Poll the bootstrap bundle once (maxWaitMs 0), retrying only on a transient
872
+ * 429 (standby-pool pods share one Cloud NAT IP against podBootstrapLimiter)
873
+ * with a short backoff. Returns null when the refresh should be abandoned —
874
+ * a non-429 error, or 429s past the retry budget — so the caller no-ops
875
+ * instead of parking a RUNNING pod in a poll loop.
876
+ */
877
+ async pollBundleWithRateLimitRetry(apiUrl, bootstrapToken) {
878
+ const retryDelaysMs = [1e3, 3e3];
879
+ for (let attempt = 0; ; attempt++) {
880
+ try {
881
+ return await pollUntilBound({ apiUrl, bootstrapToken, maxWaitMs: 0 });
882
+ } catch (err) {
883
+ const isRateLimited = err instanceof PollUntilBoundHttpError && err.status === 429;
884
+ if (!isRateLimited || attempt >= retryDelaysMs.length) {
885
+ return null;
886
+ }
887
+ await new Promise((resolve) => {
888
+ setTimeout(resolve, retryDelaysMs[attempt]);
889
+ });
890
+ }
891
+ }
892
+ }
775
893
  // ── Event buffering ────────────────────────────────────────────────
776
894
  sendEvent(event) {
777
895
  if (!this.socket) return;
@@ -1356,6 +1474,13 @@ function preToolUse(payload) {
1356
1474
  );
1357
1475
  process.exit(0);
1358
1476
  };
1477
+ // MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
1478
+ // Plan mode prompts for them despite permissions.allow (the CLI can't
1479
+ // classify MCP tools as read-only), and the pod is the sandbox.
1480
+ if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
1481
+ respond("allow");
1482
+ return;
1483
+ }
1359
1484
  // AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
1360
1485
  // missing/slow socket never denies the questionnaire. Everything else
1361
1486
  // (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
@@ -1459,6 +1584,15 @@ async function writeHookSettings(dir) {
1459
1584
  // tool, so the questionnaire is never blocked by Conveyor.
1460
1585
  matcher: "AskUserQuestion",
1461
1586
  hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
1587
+ },
1588
+ {
1589
+ // Plan-permission spawns prompt for MCP tools even when they're in
1590
+ // permissions.allow — the CLI can't classify them as read-only. A
1591
+ // hook allow bypasses the permission engine in every mode. Loose by
1592
+ // design: the pod is the sandbox, and planning agents must call
1593
+ // update_task etc. The helper answers locally (no socket).
1594
+ matcher: "mcp__.*",
1595
+ hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
1462
1596
  }
1463
1597
  ],
1464
1598
  PostToolUse: [
@@ -1497,8 +1631,8 @@ var PlanSync = class {
1497
1631
  for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
1498
1632
  try {
1499
1633
  const fullPath = join2(plansDir, file);
1500
- const stat4 = statSync(fullPath);
1501
- this.planFileSnapshot.set(fullPath, stat4.mtimeMs);
1634
+ const stat6 = statSync(fullPath);
1635
+ this.planFileSnapshot.set(fullPath, stat6.mtimeMs);
1502
1636
  } catch {
1503
1637
  continue;
1504
1638
  }
@@ -1519,11 +1653,11 @@ var PlanSync = class {
1519
1653
  for (const file of files) {
1520
1654
  const fullPath = join2(plansDir, file);
1521
1655
  try {
1522
- const stat4 = statSync(fullPath);
1656
+ const stat6 = statSync(fullPath);
1523
1657
  const prevMtime = this.planFileSnapshot.get(fullPath);
1524
- const isNew = prevMtime === void 0 || stat4.mtimeMs > prevMtime;
1525
- if (isNew && (!newest || stat4.mtimeMs > newest.mtime)) {
1526
- newest = { path: fullPath, mtime: stat4.mtimeMs };
1658
+ const isNew = prevMtime === void 0 || stat6.mtimeMs > prevMtime;
1659
+ if (isNew && (!newest || stat6.mtimeMs > newest.mtime)) {
1660
+ newest = { path: fullPath, mtime: stat6.mtimeMs };
1527
1661
  }
1528
1662
  } catch {
1529
1663
  continue;
@@ -1564,6 +1698,511 @@ var PlanSync = class {
1564
1698
  }
1565
1699
  };
1566
1700
 
1701
+ // src/setup/git-ready.ts
1702
+ import { access, stat } from "fs/promises";
1703
+ var DEFAULT_FAILED_PATH = "/workspaces/.conveyor-git-failed";
1704
+ var DEFAULT_TIMEOUT_MS = 6e5;
1705
+ var DEFAULT_POLL_MS = 200;
1706
+ async function fileExists(path4) {
1707
+ try {
1708
+ await access(path4);
1709
+ const s = await stat(path4);
1710
+ return s.isFile();
1711
+ } catch {
1712
+ return false;
1713
+ }
1714
+ }
1715
+ function awaitGitReady(opts = {}) {
1716
+ const markerPath = opts.markerPath ?? process.env.CONVEYOR_GIT_READY_MARKER;
1717
+ if (!markerPath) {
1718
+ return Promise.resolve("not-gated");
1719
+ }
1720
+ const failedPath = opts.failedPath ?? process.env.CONVEYOR_GIT_FAILED_MARKER ?? DEFAULT_FAILED_PATH;
1721
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1722
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
1723
+ opts.onLog?.(`waiting for workspace git (marker=${markerPath})`);
1724
+ return pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, opts.onLog);
1725
+ }
1726
+ async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog) {
1727
+ const deadline = Date.now() + timeoutMs;
1728
+ for (; ; ) {
1729
+ if (await fileExists(markerPath)) {
1730
+ onLog?.("workspace git ready");
1731
+ return "ready";
1732
+ }
1733
+ if (await fileExists(failedPath)) {
1734
+ onLog?.("workspace git preparation failed (marker present)");
1735
+ return "failed";
1736
+ }
1737
+ if (Date.now() >= deadline) {
1738
+ onLog?.(`workspace git not ready after ${timeoutMs}ms \u2014 giving up`);
1739
+ return "timeout";
1740
+ }
1741
+ await new Promise((resolve) => {
1742
+ setTimeout(resolve, pollMs);
1743
+ });
1744
+ }
1745
+ }
1746
+
1747
+ // src/runner/work-preservation/index.ts
1748
+ import { rm as rm3 } from "fs/promises";
1749
+
1750
+ // src/runner/work-preservation/pg-dump.ts
1751
+ import { execSync as execSync2 } from "child_process";
1752
+ import { randomUUID } from "crypto";
1753
+ import { tmpdir } from "os";
1754
+ import path from "path";
1755
+ var PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
1756
+ function connectionDefaults(opts) {
1757
+ return {
1758
+ host: opts.host ?? "localhost",
1759
+ port: opts.port ?? 5432,
1760
+ db: opts.db ?? "conveyor"
1761
+ };
1762
+ }
1763
+ function buildPgDumpCommand(opts) {
1764
+ const { host, port, db } = connectionDefaults(opts);
1765
+ return `pg_dump -h ${host} -p ${port} -U postgres ${db} -f ${JSON.stringify(
1766
+ opts.outputPath
1767
+ )} --no-owner --no-privileges`;
1768
+ }
1769
+ function buildPsqlRestoreCommand(opts) {
1770
+ const { host, port, db } = connectionDefaults(opts);
1771
+ return `psql -h ${host} -p ${port} -U postgres -d ${db} -f ${JSON.stringify(opts.dumpPath)}`;
1772
+ }
1773
+ function hasPostgresDep() {
1774
+ return (process.env.CONVEYOR_DEPS ?? "").split(",").map((dep) => dep.trim()).includes("postgresql");
1775
+ }
1776
+ function execErrorDetail(err) {
1777
+ const withStreams = err;
1778
+ const stderr = withStreams.stderr instanceof Buffer ? withStreams.stderr.toString("utf8").trim() : typeof withStreams.stderr === "string" ? withStreams.stderr.trim() : "";
1779
+ const message = withStreams.message ?? String(err);
1780
+ return stderr ? `${message} \u2014 stderr: ${stderr}` : message;
1781
+ }
1782
+ function dumpPostgresIfPresent(exec = execSync2) {
1783
+ if (!hasPostgresDep()) return Promise.resolve({ dumped: false });
1784
+ const outputPath = path.join(tmpdir(), `conveyor-pgdump-${randomUUID()}.sql`);
1785
+ try {
1786
+ exec(buildPgDumpCommand({ outputPath }), {
1787
+ stdio: ["ignore", "pipe", "pipe"],
1788
+ timeout: 6e4
1789
+ });
1790
+ return Promise.resolve({ dumped: true, path: outputPath });
1791
+ } catch (err) {
1792
+ process.stderr.write(`[conveyor-agent] pg_dump failed: ${execErrorDetail(err)}
1793
+ `);
1794
+ return Promise.resolve({ dumped: false });
1795
+ }
1796
+ }
1797
+ function applyPgDumpIfConfigured(dumpPath, exec = execSync2) {
1798
+ if (!hasPostgresDep()) return false;
1799
+ try {
1800
+ exec(buildPsqlRestoreCommand({ dumpPath }), {
1801
+ stdio: ["ignore", "pipe", "pipe"],
1802
+ timeout: 12e4
1803
+ });
1804
+ process.stderr.write("[conveyor-agent] Restored postgres state from snapshot pg dump\n");
1805
+ return true;
1806
+ } catch (err) {
1807
+ process.stderr.write(`[conveyor-agent] psql dump restore failed: ${execErrorDetail(err)}
1808
+ `);
1809
+ return false;
1810
+ }
1811
+ }
1812
+
1813
+ // src/runner/work-preservation/restore-precedence.ts
1814
+ function selectRestoreSource(probe) {
1815
+ if (probe.gcsAvailable) return "gcs";
1816
+ if (probe.wipRestoreResult === "applied") return "wip";
1817
+ return "clean";
1818
+ }
1819
+
1820
+ // src/runner/work-preservation/snapshot-tar.ts
1821
+ import { execSync as execSync3 } from "child_process";
1822
+ import { createReadStream, createWriteStream, mkdtempSync } from "fs";
1823
+ import { copyFile, mkdtemp, rm, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1824
+ import { tmpdir as tmpdir2 } from "os";
1825
+ import path2 from "path";
1826
+ import { pipeline } from "stream/promises";
1827
+ import { createGzip } from "zlib";
1828
+ import * as tar from "tar";
1829
+
1830
+ // src/runner/work-preservation/snapshot-artifact.ts
1831
+ function planSnapshotFiles(git) {
1832
+ const include = git.trackedAndUntracked();
1833
+ const includeSet = new Set(include);
1834
+ const deletions = [...new Set(git.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
1835
+ return { include, deletions };
1836
+ }
1837
+
1838
+ // src/runner/work-preservation/snapshot-tar.ts
1839
+ var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
1840
+ var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
1841
+ function parseStatusPorcelainZ(raw) {
1842
+ const includes = [];
1843
+ const deletions = [];
1844
+ const tokens = raw.split("\0");
1845
+ for (let i = 0; i < tokens.length; i++) {
1846
+ const token = tokens[i];
1847
+ if (!token || token.length < 4) continue;
1848
+ const x = token[0];
1849
+ const y = token[1];
1850
+ const filePath = token.slice(3);
1851
+ if (x === "R" || x === "C") {
1852
+ const origPath = tokens[++i];
1853
+ if (x === "R" && origPath) deletions.push(origPath);
1854
+ }
1855
+ if (x === "!") continue;
1856
+ const missingFromWorktree = y === "D" || x === "D" && y === " ";
1857
+ if (missingFromWorktree) deletions.push(filePath);
1858
+ else includes.push(filePath);
1859
+ }
1860
+ return { includes, deletions };
1861
+ }
1862
+ function realGitSurface(cwd) {
1863
+ let cached = null;
1864
+ const load = () => {
1865
+ if (!cached) {
1866
+ const out = execSync3("git status --porcelain=v1 -z -uall", {
1867
+ cwd,
1868
+ stdio: ["ignore", "pipe", "ignore"],
1869
+ maxBuffer: STATUS_MAX_BUFFER
1870
+ }).toString("utf8");
1871
+ cached = parseStatusPorcelainZ(out);
1872
+ }
1873
+ return cached;
1874
+ };
1875
+ return {
1876
+ trackedAndUntracked: () => load().includes,
1877
+ deletedSinceHead: () => load().deletions
1878
+ };
1879
+ }
1880
+ function gitHead(cwd) {
1881
+ try {
1882
+ return execSync3("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
1883
+ } catch {
1884
+ return null;
1885
+ }
1886
+ }
1887
+ async function buildSnapshotTar(cwd, opts) {
1888
+ const staging = await mkdtemp(path2.join(tmpdir2(), "conveyor-snapshot-"));
1889
+ const cleanup = async () => {
1890
+ await rm(staging, { recursive: true, force: true }).catch(() => {
1891
+ });
1892
+ };
1893
+ try {
1894
+ const plan = planSnapshotFiles(realGitSurface(cwd));
1895
+ const head = gitHead(cwd);
1896
+ if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
1897
+ const capturedAt = Date.now();
1898
+ const manifest = {
1899
+ head,
1900
+ deletions: plan.deletions,
1901
+ capturedAt,
1902
+ pgDump: Boolean(opts?.pgDumpPath)
1903
+ };
1904
+ await writeFile2(path2.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
1905
+ const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
1906
+ if (opts?.pgDumpPath) {
1907
+ await copyFile(opts.pgDumpPath, path2.join(staging, PG_DUMP_FILENAME));
1908
+ stagedEntries.push(PG_DUMP_FILENAME);
1909
+ }
1910
+ const rawTarPath = path2.join(staging, "snapshot.tar");
1911
+ await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
1912
+ if (plan.include.length > 0) {
1913
+ await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
1914
+ }
1915
+ const tarPath = path2.join(staging, "snapshot.tar.gz");
1916
+ await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
1917
+ await rm(rawTarPath, { force: true });
1918
+ const { size } = await stat2(tarPath);
1919
+ return {
1920
+ tarPath,
1921
+ sizeBytes: size,
1922
+ fileCount: plan.include.length,
1923
+ deletionCount: plan.deletions.length,
1924
+ capturedAt,
1925
+ cleanup
1926
+ };
1927
+ } catch (err) {
1928
+ await cleanup();
1929
+ throw err;
1930
+ }
1931
+ }
1932
+ function isWithinWorkspace(cwd, rel) {
1933
+ if (!rel || path2.isAbsolute(rel)) return false;
1934
+ const root = path2.resolve(cwd);
1935
+ const abs = path2.resolve(root, rel);
1936
+ return abs !== root && abs.startsWith(root + path2.sep);
1937
+ }
1938
+ async function readSnapshotArchive(tarPath) {
1939
+ let manifestRaw = null;
1940
+ let pgDumpDir = null;
1941
+ let pgDumpPath;
1942
+ let pgDumpWritten = Promise.resolve();
1943
+ const cleanup = async () => {
1944
+ if (pgDumpDir) await rm(pgDumpDir, { recursive: true, force: true }).catch(() => {
1945
+ });
1946
+ };
1947
+ try {
1948
+ await tar.list({
1949
+ file: tarPath,
1950
+ onReadEntry: (entry) => {
1951
+ if (entry.path === SNAPSHOT_MANIFEST_NAME) {
1952
+ const chunks = [];
1953
+ entry.on("data", (chunk) => chunks.push(chunk));
1954
+ entry.on("end", () => {
1955
+ manifestRaw = Buffer.concat(chunks);
1956
+ });
1957
+ } else if (entry.path === PG_DUMP_FILENAME) {
1958
+ pgDumpDir = mkdtempSync(path2.join(tmpdir2(), "conveyor-pgdump-restore-"));
1959
+ pgDumpPath = path2.join(pgDumpDir, "dump.sql");
1960
+ const dest = createWriteStream(pgDumpPath);
1961
+ pgDumpWritten = new Promise((resolve, reject) => {
1962
+ dest.on("finish", () => resolve());
1963
+ dest.on("error", reject);
1964
+ });
1965
+ entry.pipe(dest);
1966
+ }
1967
+ }
1968
+ });
1969
+ await pgDumpWritten;
1970
+ } catch {
1971
+ await cleanup();
1972
+ return null;
1973
+ }
1974
+ if (!manifestRaw) {
1975
+ await cleanup();
1976
+ return null;
1977
+ }
1978
+ try {
1979
+ const parsed = JSON.parse(manifestRaw.toString("utf8"));
1980
+ if (typeof parsed.head !== "string" || !parsed.head) {
1981
+ await cleanup();
1982
+ return null;
1983
+ }
1984
+ return {
1985
+ manifest: {
1986
+ head: parsed.head,
1987
+ deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
1988
+ capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0,
1989
+ pgDump: parsed.pgDump === true
1990
+ },
1991
+ ...pgDumpPath ? { pgDumpPath } : {},
1992
+ cleanup
1993
+ };
1994
+ } catch {
1995
+ await cleanup();
1996
+ return null;
1997
+ }
1998
+ }
1999
+ async function extractSnapshotTar(cwd, tarPath, opts) {
2000
+ const read = await readSnapshotArchive(tarPath);
2001
+ if (!read) return { status: "invalid" };
2002
+ try {
2003
+ const { manifest, pgDumpPath } = read;
2004
+ const currentHead = gitHead(cwd);
2005
+ if (manifest.head !== currentHead) {
2006
+ return { status: "stale-head", snapshotHead: manifest.head, currentHead };
2007
+ }
2008
+ let filesExtracted = 0;
2009
+ await tar.extract({
2010
+ file: tarPath,
2011
+ cwd,
2012
+ filter: (entryPath) => {
2013
+ if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === PG_DUMP_FILENAME) return false;
2014
+ filesExtracted++;
2015
+ return true;
2016
+ }
2017
+ });
2018
+ let deletionsApplied = 0;
2019
+ for (const rel of manifest.deletions) {
2020
+ if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
2021
+ await rm(path2.resolve(cwd, rel), { recursive: true, force: true });
2022
+ deletionsApplied++;
2023
+ }
2024
+ const pgDumpApplied = manifest.pgDump && pgDumpPath ? applyPgDumpIfConfigured(pgDumpPath, opts?.pgExec) : false;
2025
+ return { status: "extracted", filesExtracted, deletionsApplied, pgDumpApplied };
2026
+ } finally {
2027
+ await read.cleanup();
2028
+ }
2029
+ }
2030
+
2031
+ // src/runner/work-preservation/snapshot-transfer.ts
2032
+ import { randomUUID as randomUUID2 } from "crypto";
2033
+ import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
2034
+ import { rm as rm2 } from "fs/promises";
2035
+ import { tmpdir as tmpdir3 } from "os";
2036
+ import path3 from "path";
2037
+ import { Readable } from "stream";
2038
+ import { pipeline as pipeline2 } from "stream/promises";
2039
+ var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
2040
+ var SNAPSHOT_CAPTURED_AT_HEADER = "x-snapshot-captured-at";
2041
+ async function putSnapshotToGcs(url, tarResult) {
2042
+ try {
2043
+ const body = Readable.toWeb(createReadStream2(tarResult.tarPath));
2044
+ const res = await fetch(url, {
2045
+ method: "PUT",
2046
+ headers: {
2047
+ "Content-Type": "application/gzip",
2048
+ "Content-Length": String(tarResult.sizeBytes),
2049
+ [SNAPSHOT_CAPTURED_AT_HEADER]: String(tarResult.capturedAt)
2050
+ },
2051
+ body,
2052
+ duplex: "half",
2053
+ signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
2054
+ });
2055
+ return res.ok;
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ }
2060
+ async function downloadSnapshotToTemp(url) {
2061
+ const tmpTar = path3.join(tmpdir3(), `conveyor-snapshot-restore-${randomUUID2()}.tar.gz`);
2062
+ try {
2063
+ const res = await fetch(url, {
2064
+ method: "GET",
2065
+ signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
2066
+ });
2067
+ if (!res.ok || !res.body) return null;
2068
+ await pipeline2(
2069
+ Readable.fromWeb(res.body),
2070
+ createWriteStream2(tmpTar)
2071
+ );
2072
+ return tmpTar;
2073
+ } catch {
2074
+ await rm2(tmpTar, { force: true }).catch(() => {
2075
+ });
2076
+ return null;
2077
+ }
2078
+ }
2079
+
2080
+ // src/runner/work-preservation/index.ts
2081
+ var periodicTimer = null;
2082
+ var inFlightCapture = null;
2083
+ var inFlightGcsUpload = null;
2084
+ var EMPTY_GCS_RESULT = {
2085
+ uploaded: false,
2086
+ fileCount: 0,
2087
+ deletionCount: 0,
2088
+ sizeBytes: 0,
2089
+ pgDumped: false
2090
+ };
2091
+ async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
2092
+ let tarResult;
2093
+ try {
2094
+ tarResult = await buildSnapshotTar(cwd, { pgDumpPath: opts?.pgDumpPath });
2095
+ } catch {
2096
+ return EMPTY_GCS_RESULT;
2097
+ }
2098
+ try {
2099
+ const uploaded = (opts?.forceUpload || tarResult.sizeBytes > 0) && uploadUrl ? await putSnapshotToGcs(uploadUrl, tarResult) : false;
2100
+ return {
2101
+ uploaded,
2102
+ fileCount: tarResult.fileCount,
2103
+ deletionCount: tarResult.deletionCount,
2104
+ sizeBytes: tarResult.sizeBytes,
2105
+ pgDumped: Boolean(opts?.pgDumpPath)
2106
+ };
2107
+ } finally {
2108
+ await tarResult.cleanup();
2109
+ }
2110
+ }
2111
+ async function uploadSnapshotToGcs(cwd, uploadUrl, opts) {
2112
+ const previous = inFlightGcsUpload;
2113
+ const run = (async () => {
2114
+ if (previous) await previous.catch(() => {
2115
+ });
2116
+ return doUploadSnapshotToGcs(cwd, uploadUrl, opts);
2117
+ })();
2118
+ inFlightGcsUpload = run;
2119
+ try {
2120
+ return await run;
2121
+ } finally {
2122
+ if (inFlightGcsUpload === run) inFlightGcsUpload = null;
2123
+ }
2124
+ }
2125
+ async function captureSnapshot(ctx) {
2126
+ const gcs = await uploadSnapshotToGcs(ctx.cwd, ctx.snapshotUploadUrl, {
2127
+ pgDumpPath: ctx.pgDumpPath,
2128
+ forceUpload: ctx.forceUpload
2129
+ });
2130
+ const wipResult = await flushPendingChanges(ctx.cwd, {
2131
+ wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation periodic snapshot",
2132
+ refreshToken: ctx.refreshToken
2133
+ });
2134
+ return {
2135
+ fileCount: gcs.fileCount,
2136
+ deletionCount: gcs.deletionCount,
2137
+ sizeBytes: gcs.sizeBytes,
2138
+ gcsUploaded: gcs.uploaded,
2139
+ wipPushed: wipResult.committed || wipResult.pushed,
2140
+ pgDumped: gcs.pgDumped
2141
+ };
2142
+ }
2143
+ function startPeriodic(intervalMs, ctx) {
2144
+ stop();
2145
+ periodicTimer = setInterval(() => {
2146
+ if (inFlightCapture) return;
2147
+ const run = captureSnapshot(ctx);
2148
+ inFlightCapture = run;
2149
+ void run.catch(() => {
2150
+ }).finally(() => {
2151
+ if (inFlightCapture === run) inFlightCapture = null;
2152
+ });
2153
+ }, intervalMs);
2154
+ }
2155
+ function stop() {
2156
+ if (periodicTimer) {
2157
+ clearInterval(periodicTimer);
2158
+ periodicTimer = null;
2159
+ }
2160
+ }
2161
+ async function finalizeForSleep(ctx) {
2162
+ stop();
2163
+ if (inFlightCapture) await inFlightCapture.catch(() => {
2164
+ });
2165
+ const dump = await dumpPostgresIfPresent();
2166
+ try {
2167
+ const artifact = await captureSnapshot({
2168
+ ...ctx,
2169
+ pgDumpPath: dump.path,
2170
+ wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation sleep finalize snapshot",
2171
+ // Always stamp snapshotAt on sleep — even an empty workspace must confirm
2172
+ // the finalize so teardown doesn't wait out the reconciler's cap.
2173
+ forceUpload: true
2174
+ });
2175
+ return { ...artifact, pgDumped: dump.dumped };
2176
+ } finally {
2177
+ if (dump.path) await rm3(dump.path, { force: true }).catch(() => {
2178
+ });
2179
+ }
2180
+ }
2181
+ async function restoreOnBoot(bundle, cwd) {
2182
+ let gcsAvailable = false;
2183
+ let gcsFileCount;
2184
+ if (bundle.snapshotUrl) {
2185
+ const tmpTar = await downloadSnapshotToTemp(bundle.snapshotUrl);
2186
+ if (tmpTar) {
2187
+ try {
2188
+ const result = await extractSnapshotTar(cwd, tmpTar);
2189
+ if (result.status === "extracted") {
2190
+ gcsAvailable = true;
2191
+ gcsFileCount = result.filesExtracted;
2192
+ }
2193
+ } catch {
2194
+ gcsAvailable = false;
2195
+ } finally {
2196
+ await rm3(tmpTar, { force: true }).catch(() => {
2197
+ });
2198
+ }
2199
+ }
2200
+ }
2201
+ const wipRestoreResult = gcsAvailable ? "none" : restoreWipSnapshot(cwd, bundle.gitPlan.branch);
2202
+ const source = selectRestoreSource({ gcsAvailable, wipRestoreResult });
2203
+ return source === "gcs" ? { source, fileCount: gcsFileCount } : { source };
2204
+ }
2205
+
1567
2206
  // ../shared/dist/index.js
1568
2207
  import { z } from "zod";
1569
2208
  import { z as z2 } from "zod";
@@ -1704,6 +2343,16 @@ var NotifyAgentVersionRequestSchema = z.object({
1704
2343
  sessionId: z.string(),
1705
2344
  agentVersion: z.string()
1706
2345
  });
2346
+ var DiscoveredPortSchema = z.object({
2347
+ port: z.number().int().min(1).max(65535),
2348
+ label: z.string().min(1).max(64).optional(),
2349
+ protocol: z.enum(["http", "tcp"]).optional(),
2350
+ detectedAt: z.string()
2351
+ });
2352
+ var ReportDiscoveredPortsRequestSchema = z.object({
2353
+ sessionId: z.string(),
2354
+ ports: z.array(DiscoveredPortSchema).max(64)
2355
+ });
1707
2356
  var CreateSubtaskRequestSchema = z.object({
1708
2357
  sessionId: z.string(),
1709
2358
  title: z.string().min(1),
@@ -2037,6 +2686,15 @@ var StopProjectBuildRequestSchema = z2.object({
2037
2686
  taskId: z2.string(),
2038
2687
  requestingUserId: z2.string().optional()
2039
2688
  });
2689
+ var StartProjectWorkspaceRequestSchema = z2.object({
2690
+ projectId: z2.string(),
2691
+ requestingUserId: z2.string().optional()
2692
+ });
2693
+ var StopProjectWorkspaceRequestSchema = z2.object({
2694
+ projectId: z2.string(),
2695
+ destroy: z2.boolean().optional(),
2696
+ requestingUserId: z2.string().optional()
2697
+ });
2040
2698
  var CreateProjectReleaseRequestSchema = z2.object({
2041
2699
  projectId: z2.string(),
2042
2700
  taskIds: z2.array(z2.string()).optional(),
@@ -2513,8 +3171,8 @@ var ClaudeCodeHarness = class {
2513
3171
  };
2514
3172
 
2515
3173
  // src/harness/pty/session.ts
2516
- import { mkdtemp, mkdir as mkdir2, rm, stat } from "fs/promises";
2517
- import { tmpdir } from "os";
3174
+ import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4, stat as stat3 } from "fs/promises";
3175
+ import { tmpdir as tmpdir4 } from "os";
2518
3176
  import { join as join4, dirname } from "path";
2519
3177
 
2520
3178
  // src/harness/pty/event-queue.ts
@@ -2783,8 +3441,8 @@ function mapTranscriptRecord(raw) {
2783
3441
  // src/harness/pty/jsonl-tailer.ts
2784
3442
  var POLL_INTERVAL_MS = 25;
2785
3443
  var JsonlTailer = class {
2786
- constructor(path, onEvent) {
2787
- this.path = path;
3444
+ constructor(path4, onEvent) {
3445
+ this.path = path4;
2788
3446
  this.onEvent = onEvent;
2789
3447
  }
2790
3448
  path;
@@ -2981,7 +3639,7 @@ var PtyOutputCoalescer = class {
2981
3639
 
2982
3640
  // src/harness/pty/tool-server.ts
2983
3641
  import { createServer as createServer2 } from "http";
2984
- import { writeFile as writeFile2 } from "fs/promises";
3642
+ import { writeFile as writeFile3 } from "fs/promises";
2985
3643
  import { join as join3 } from "path";
2986
3644
  import { randomBytes } from "crypto";
2987
3645
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -3088,7 +3746,7 @@ async function startToolServers(mcpServers, tempDir) {
3088
3746
  }
3089
3747
  if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
3090
3748
  const mcpConfigPath = join3(tempDir, "mcp-config.json");
3091
- await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
3749
+ await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
3092
3750
  return { servers, mcpConfigPath };
3093
3751
  }
3094
3752
 
@@ -3126,14 +3784,14 @@ var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
3126
3784
  var SUBMIT_NUDGE_MAX_PRESSES = 5;
3127
3785
  var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
3128
3786
  var SUBMIT_NUDGE_WINDOW_MS = 9e4;
3129
- function sleep2(ms) {
3787
+ function sleep3(ms) {
3130
3788
  return new Promise((resolve) => {
3131
3789
  setTimeout(resolve, ms);
3132
3790
  });
3133
3791
  }
3134
- async function transcriptSize(path) {
3792
+ async function transcriptSize(path4) {
3135
3793
  try {
3136
- return (await stat(path)).size;
3794
+ return (await stat3(path4)).size;
3137
3795
  } catch {
3138
3796
  return 0;
3139
3797
  }
@@ -3223,7 +3881,7 @@ var PtySession = class {
3223
3881
  return;
3224
3882
  }
3225
3883
  try {
3226
- this.tempDir = await mkdtemp(join4(tmpdir(), "conveyor-pty-"));
3884
+ this.tempDir = await mkdtemp2(join4(tmpdir4(), "conveyor-pty-"));
3227
3885
  const socketPath = join4(this.tempDir, "hook.sock");
3228
3886
  this.socket = new HookSocketServer(
3229
3887
  socketPath,
@@ -3323,7 +3981,7 @@ var PtySession = class {
3323
3981
  this.mcpConfigPath = null;
3324
3982
  this.queue.close();
3325
3983
  if (this.tempDir) {
3326
- await rm(this.tempDir, { recursive: true, force: true });
3984
+ await rm4(this.tempDir, { recursive: true, force: true });
3327
3985
  this.tempDir = "";
3328
3986
  }
3329
3987
  }
@@ -3383,7 +4041,7 @@ var PtySession = class {
3383
4041
  async deliverPrompt(text) {
3384
4042
  this.writeStdin(buildPromptBytes(text));
3385
4043
  if (this.options.promptDelivery === "prefill") return;
3386
- await sleep2(SUBMIT_SETTLE_MS);
4044
+ await sleep3(SUBMIT_SETTLE_MS);
3387
4045
  if (this._toreDown) return;
3388
4046
  this.writeStdin("\r");
3389
4047
  this.armSubmitNudge();
@@ -3531,7 +4189,7 @@ var PtySession = class {
3531
4189
  };
3532
4190
 
3533
4191
  // src/harness/pty/credentials.ts
3534
- import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm2, writeFile as writeFile3 } from "fs/promises";
4192
+ import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm5, writeFile as writeFile4 } from "fs/promises";
3535
4193
  import { homedir as homedir2 } from "os";
3536
4194
  import { join as join5 } from "path";
3537
4195
  var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
@@ -3582,26 +4240,26 @@ function planCredentialsWrite(input) {
3582
4240
  if (fresh) return { action: "skip", reason: "current" };
3583
4241
  return { action: "write", contents };
3584
4242
  }
3585
- async function readRaw(path) {
4243
+ async function readRaw(path4) {
3586
4244
  try {
3587
- return await readFile(path, "utf8");
4245
+ return await readFile(path4, "utf8");
3588
4246
  } catch {
3589
4247
  return null;
3590
4248
  }
3591
4249
  }
3592
4250
  async function ensureClaudeCredentials(env = process.env) {
3593
4251
  try {
3594
- const path = claudeCredentialsPath();
4252
+ const path4 = claudeCredentialsPath();
3595
4253
  const plan = planCredentialsWrite({
3596
4254
  isCloud: isConveyorCloudEnv(env),
3597
4255
  token: env.CLAUDE_CODE_OAUTH_TOKEN,
3598
- existingRaw: await readRaw(path),
4256
+ existingRaw: await readRaw(path4),
3599
4257
  now: Date.now()
3600
4258
  });
3601
4259
  if (plan.action === "skip") return;
3602
4260
  await mkdir3(claudeConfigHome(), { recursive: true });
3603
- await writeFile3(path, plan.contents, { encoding: "utf8", mode: 384 });
3604
- await chmod2(path, 384).catch(() => {
4261
+ await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
4262
+ await chmod2(path4, 384).catch(() => {
3605
4263
  });
3606
4264
  } catch (err) {
3607
4265
  const message = err instanceof Error ? err.message : String(err);
@@ -3657,11 +4315,11 @@ function planClaudeJsonSeed(existingRaw, trustCwd) {
3657
4315
  async function ensureClaudeOnboarding(env = process.env, trustCwd) {
3658
4316
  try {
3659
4317
  if (!isConveyorCloudEnv(env)) return;
3660
- const path = claudeJsonPath();
3661
- const contents = planClaudeJsonSeed(await readRaw(path), trustCwd);
4318
+ const path4 = claudeJsonPath();
4319
+ const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
3662
4320
  if (contents === null) return;
3663
- await writeFile3(path, contents, "utf8");
3664
- const verify = await readRaw(path);
4321
+ await writeFile4(path4, contents, "utf8");
4322
+ const verify = await readRaw(path4);
3665
4323
  if (verify === contents) {
3666
4324
  process.stderr.write(
3667
4325
  `[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
@@ -3669,7 +4327,7 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
3669
4327
  );
3670
4328
  } else {
3671
4329
  process.stderr.write(
3672
- `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4330
+ `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
3673
4331
  `
3674
4332
  );
3675
4333
  }
@@ -3682,12 +4340,12 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
3682
4340
  async function removeConveyorCredentials(env = process.env) {
3683
4341
  try {
3684
4342
  if (!isConveyorCloudEnv(env)) return;
3685
- const path = claudeCredentialsPath();
3686
- const existing = parseClaudeAiOauth(await readRaw(path));
4343
+ const path4 = claudeCredentialsPath();
4344
+ const existing = parseClaudeAiOauth(await readRaw(path4));
3687
4345
  if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
3688
4346
  return;
3689
4347
  }
3690
- await rm2(path, { force: true });
4348
+ await rm5(path4, { force: true });
3691
4349
  } catch (err) {
3692
4350
  const message = err instanceof Error ? err.message : String(err);
3693
4351
  process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
@@ -4009,7 +4667,7 @@ function formatIncidents(incidents) {
4009
4667
  }
4010
4668
 
4011
4669
  // src/execution/tag-context-resolver.ts
4012
- import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
4670
+ import { readFile as readFile2, readdir, stat as stat4 } from "fs/promises";
4013
4671
  var TYPE_PRIORITY = { rule: 0, file: 1, folder: 2, doc: 3 };
4014
4672
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
4015
4673
  ".png",
@@ -4059,7 +4717,7 @@ async function readFileContent(filePath, maxChars) {
4059
4717
  let rawContent;
4060
4718
  let mtimeMs;
4061
4719
  try {
4062
- const st = await stat2(filePath);
4720
+ const st = await stat4(filePath);
4063
4721
  mtimeMs = st.mtimeMs;
4064
4722
  } catch {
4065
4723
  return null;
@@ -4086,7 +4744,7 @@ async function readFolderListing(folderPath) {
4086
4744
  try {
4087
4745
  let mtimeMs;
4088
4746
  try {
4089
- const st = await stat2(folderPath);
4747
+ const st = await stat4(folderPath);
4090
4748
  mtimeMs = st.mtimeMs;
4091
4749
  } catch {
4092
4750
  return null;
@@ -5634,7 +6292,7 @@ function buildMutationTools(connection, config) {
5634
6292
  }
5635
6293
 
5636
6294
  // src/tools/attachment-tools.ts
5637
- import { readFile as readFile3, stat as stat3 } from "fs/promises";
6295
+ import { readFile as readFile3, stat as stat5 } from "fs/promises";
5638
6296
  import { basename, extname, isAbsolute, join as join6 } from "path";
5639
6297
  import { z as z6 } from "zod";
5640
6298
  var IMAGE_MIME_BY_EXT = {
@@ -5652,16 +6310,16 @@ function buildUploadAttachmentTool(connection, config) {
5652
6310
  path: z6.string().describe("Path to the image file \u2014 absolute, or relative to the workspace root"),
5653
6311
  title: z6.string().optional().describe("Short caption posted with the image (defaults to the file name)")
5654
6312
  },
5655
- async ({ path, title }) => {
6313
+ async ({ path: path4, title }) => {
5656
6314
  try {
5657
- const filePath = isAbsolute(path) ? path : join6(config.workspaceDir, path);
6315
+ const filePath = isAbsolute(path4) ? path4 : join6(config.workspaceDir, path4);
5658
6316
  const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
5659
6317
  if (!mimeType) {
5660
6318
  return textResult(
5661
6319
  `Unsupported file type "${extname(filePath) || "(none)"}". Supported: ${Object.keys(IMAGE_MIME_BY_EXT).join(", ")}`
5662
6320
  );
5663
6321
  }
5664
- const info = await stat3(filePath).catch(() => null);
6322
+ const info = await stat5(filePath).catch(() => null);
5665
6323
  if (!info?.isFile()) {
5666
6324
  return textResult(`File not found: ${filePath}`);
5667
6325
  }
@@ -6568,14 +7226,14 @@ function flushPendingToolCalls(host, turnToolCalls) {
6568
7226
  }
6569
7227
  const outputsByTool = /* @__PURE__ */ new Map();
6570
7228
  for (const entry of host.pendingToolOutputs) {
6571
- const list = outputsByTool.get(entry.tool) ?? [];
6572
- list.push(entry.output);
6573
- outputsByTool.set(entry.tool, list);
7229
+ const list2 = outputsByTool.get(entry.tool) ?? [];
7230
+ list2.push(entry.output);
7231
+ outputsByTool.set(entry.tool, list2);
6574
7232
  }
6575
7233
  for (const call of turnToolCalls) {
6576
- const list = outputsByTool.get(call.tool);
6577
- if (list && list.length > 0) {
6578
- call.output = list.shift();
7234
+ const list2 = outputsByTool.get(call.tool);
7235
+ if (list2 && list2.length > 0) {
7236
+ call.output = list2.shift();
6579
7237
  }
6580
7238
  }
6581
7239
  host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
@@ -7063,10 +7721,10 @@ function hasExistingSessionFile(taskId, cwd, lineage) {
7063
7721
  const key = sessionLineageKey(taskId, lineage.agentMode, lineage.runnerMode);
7064
7722
  return sessionFileExists(taskIdToSessionUuid(key), cwd);
7065
7723
  }
7066
- function repairTornSessionFile(path) {
7724
+ function repairTornSessionFile(path4) {
7067
7725
  try {
7068
- if (!existsSync(path)) return false;
7069
- const content = readFileSync2(path, "utf8");
7726
+ if (!existsSync(path4)) return false;
7727
+ const content = readFileSync2(path4, "utf8");
7070
7728
  if (content.length === 0) return false;
7071
7729
  let keepEnd = content.length;
7072
7730
  if (!content.endsWith("\n")) {
@@ -7085,9 +7743,9 @@ function repairTornSessionFile(path) {
7085
7743
  keepEnd = prevNewline + 1;
7086
7744
  }
7087
7745
  if (keepEnd === content.length) return false;
7088
- truncateSync(path, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
7746
+ truncateSync(path4, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
7089
7747
  logger2.warn("Repaired torn transcript before resume", {
7090
- path,
7748
+ path: path4,
7091
7749
  trimmedBytes: content.length - keepEnd
7092
7750
  });
7093
7751
  return true;
@@ -7412,12 +8070,14 @@ async function handleAuthError(context, host, options) {
7412
8070
  host.connection.postChatMessage("Authentication expired. Re-bootstrapping credentials...");
7413
8071
  const refreshed = await host.connection.refreshAuthToken();
7414
8072
  if (!refreshed) {
7415
- host.connection.postChatMessage("Failed to refresh authentication. Agent will restart.");
8073
+ host.connection.postChatMessage(
8074
+ "\u26A0\uFE0F No Claude credential is configured for this project \u2014 the agent can't run and is now idle. Add a Claude Code OAuth token (or Anthropic API key) in Project Settings, then resume this task."
8075
+ );
7416
8076
  host.connection.sendEvent({
7417
8077
  type: "error",
7418
- message: "Auth re-bootstrap failed, exiting for restart"
8078
+ message: "No Claude credential available \u2014 agent idle (dormant)"
7419
8079
  });
7420
- process.exit(1);
8080
+ return;
7421
8081
  }
7422
8082
  context.claudeSessionId = null;
7423
8083
  host.connection.storeSessionId("");
@@ -7901,8 +8561,195 @@ function readAgentVersion() {
7901
8561
  return null;
7902
8562
  }
7903
8563
 
8564
+ // src/runner/port-discovery.ts
8565
+ import { readFile as readFile4 } from "fs/promises";
8566
+ import { execFile } from "child_process";
8567
+ var PROC_TCP_LISTEN_STATE = "0A";
8568
+ function parseProcNetTcpListeners(content) {
8569
+ const ports = [];
8570
+ const lines = content.split("\n");
8571
+ for (let i = 1; i < lines.length; i++) {
8572
+ const line = lines[i];
8573
+ if (!line) continue;
8574
+ const cols = line.trim().split(/\s+/);
8575
+ if (cols.length < 4 || cols[3] !== PROC_TCP_LISTEN_STATE) continue;
8576
+ const local = cols[1];
8577
+ if (!local) continue;
8578
+ const portHex = local.split(":").pop();
8579
+ if (!portHex) continue;
8580
+ const port = Number.parseInt(portHex, 16);
8581
+ if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.push(port);
8582
+ }
8583
+ return ports;
8584
+ }
8585
+ var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
8586
+ async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
8587
+ const ports = /* @__PURE__ */ new Set();
8588
+ let readable = false;
8589
+ for (const path4 of procPaths) {
8590
+ try {
8591
+ const content = await readFile4(path4, "utf8");
8592
+ readable = true;
8593
+ for (const port of parseProcNetTcpListeners(content)) ports.add(port);
8594
+ } catch {
8595
+ }
8596
+ }
8597
+ return readable ? ports : null;
8598
+ }
8599
+ async function readNetstatListeningPorts() {
8600
+ const output = await new Promise((resolve) => {
8601
+ execFile("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
8602
+ resolve(err ? null : stdout);
8603
+ });
8604
+ });
8605
+ if (output === null) return null;
8606
+ const ports = /* @__PURE__ */ new Set();
8607
+ for (const line of output.split("\n")) {
8608
+ if (!line.includes("LISTEN")) continue;
8609
+ const cols = line.trim().split(/\s+/);
8610
+ const local = cols[3];
8611
+ if (!local) continue;
8612
+ const portStr = local.split(".").pop();
8613
+ const port = Number(portStr);
8614
+ if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.add(port);
8615
+ }
8616
+ return ports;
8617
+ }
8618
+ async function readListeningPorts() {
8619
+ const proc = await readProcListeningPorts();
8620
+ if (proc !== null) return proc;
8621
+ if (process.platform !== "linux") return readNetstatListeningPorts();
8622
+ return null;
8623
+ }
8624
+ var DEFAULT_EXCLUDED_PORTS = [2222, 5432, 6379, 9200];
8625
+ var DEFAULT_EPHEMERAL_PORT_MIN = 32768;
8626
+ var DEFAULT_INTERVAL_MS = 15e3;
8627
+ var DEFAULT_MAX_PORTS = 16;
8628
+ var CONFIRM_SCANS = 2;
8629
+ var PortDiscovery = class {
8630
+ opts;
8631
+ intervalMs;
8632
+ maxPorts;
8633
+ excluded;
8634
+ ephemeralPortMin;
8635
+ scan;
8636
+ now;
8637
+ log;
8638
+ baseline = null;
8639
+ tracked = /* @__PURE__ */ new Map();
8640
+ timer = null;
8641
+ ticking = false;
8642
+ disabled = false;
8643
+ /** Set when the confirmed set changed (or a report failed) — cleared only
8644
+ * after a successful report, so transient RPC failures retry next tick. */
8645
+ reportPending = false;
8646
+ lastReportedKey = "";
8647
+ constructor(options) {
8648
+ this.opts = options;
8649
+ this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
8650
+ this.maxPorts = options.maxPorts ?? DEFAULT_MAX_PORTS;
8651
+ this.excluded = new Set(options.excludedPorts ?? DEFAULT_EXCLUDED_PORTS);
8652
+ this.ephemeralPortMin = options.ephemeralPortMin ?? DEFAULT_EPHEMERAL_PORT_MIN;
8653
+ this.scan = options.scan ?? readListeningPorts;
8654
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
8655
+ this.log = options.log ?? ((m) => process.stderr.write(`[conveyor-agent] ${m}
8656
+ `));
8657
+ }
8658
+ /** Take the baseline scan and start polling. Safe to call once. */
8659
+ async start() {
8660
+ if (this.timer || this.disabled) return;
8661
+ const baseline = await this.scanSafe();
8662
+ if (baseline === null) {
8663
+ this.disabled = true;
8664
+ this.log("Port discovery disabled: no listening-socket source available");
8665
+ return;
8666
+ }
8667
+ this.baseline = baseline;
8668
+ this.timer = setInterval(() => void this.tick(), this.intervalMs);
8669
+ this.timer.unref?.();
8670
+ }
8671
+ stop() {
8672
+ if (this.timer) {
8673
+ clearInterval(this.timer);
8674
+ this.timer = null;
8675
+ }
8676
+ }
8677
+ /** One poll cycle. Exposed for tests (deterministic, no timers needed). */
8678
+ async tick() {
8679
+ if (this.ticking || this.disabled || !this.baseline) return;
8680
+ this.ticking = true;
8681
+ try {
8682
+ const current = await this.scanSafe();
8683
+ if (current === null) return;
8684
+ this.updateTracking(current);
8685
+ if (this.reportPending) await this.flushReport();
8686
+ } finally {
8687
+ this.ticking = false;
8688
+ }
8689
+ }
8690
+ async scanSafe() {
8691
+ try {
8692
+ return await this.scan();
8693
+ } catch {
8694
+ return null;
8695
+ }
8696
+ }
8697
+ isCandidate(port) {
8698
+ if (this.baseline?.has(port)) return false;
8699
+ if (this.excluded.has(port)) return false;
8700
+ if (port >= this.ephemeralPortMin) return false;
8701
+ return true;
8702
+ }
8703
+ updateTracking(current) {
8704
+ for (const port of current) {
8705
+ if (!this.isCandidate(port)) continue;
8706
+ const entry = this.tracked.get(port);
8707
+ if (!entry) {
8708
+ this.tracked.set(port, { seen: 1, missed: 0, confirmed: false, detectedAt: "" });
8709
+ continue;
8710
+ }
8711
+ entry.seen += 1;
8712
+ entry.missed = 0;
8713
+ if (!entry.confirmed && entry.seen >= CONFIRM_SCANS) {
8714
+ entry.confirmed = true;
8715
+ entry.detectedAt = this.now().toISOString();
8716
+ }
8717
+ }
8718
+ for (const [port, entry] of this.tracked) {
8719
+ if (current.has(port)) continue;
8720
+ entry.missed += 1;
8721
+ entry.seen = 0;
8722
+ if (entry.missed >= CONFIRM_SCANS || !entry.confirmed) this.tracked.delete(port);
8723
+ }
8724
+ const key = this.confirmedKey();
8725
+ if (key !== this.lastReportedKey) this.reportPending = true;
8726
+ }
8727
+ confirmedPorts() {
8728
+ const confirmed = [...this.tracked.entries()].filter(([, entry]) => entry.confirmed).sort(([a], [b]) => a - b).slice(0, this.maxPorts);
8729
+ return confirmed.map(([port, entry]) => ({
8730
+ port,
8731
+ protocol: "tcp",
8732
+ detectedAt: entry.detectedAt
8733
+ }));
8734
+ }
8735
+ confirmedKey() {
8736
+ return this.confirmedPorts().map(({ port }) => port).join(",");
8737
+ }
8738
+ async flushReport() {
8739
+ const ports = this.confirmedPorts();
8740
+ const key = ports.map(({ port }) => port).join(",");
8741
+ try {
8742
+ await this.opts.report(ports);
8743
+ this.lastReportedKey = key;
8744
+ this.reportPending = false;
8745
+ this.log(`Discovered preview ports: [${key || "none"}]`);
8746
+ } catch {
8747
+ }
8748
+ }
8749
+ };
8750
+
7904
8751
  // src/runner/parent-pull-handler.ts
7905
- import { execSync as execSync2 } from "child_process";
8752
+ import { execSync as execSync4 } from "child_process";
7906
8753
  function handlePullBranch(workDir, branch) {
7907
8754
  if (!branch) return;
7908
8755
  const current = getCurrentBranch(workDir);
@@ -7921,14 +8768,14 @@ function handlePullBranch(workDir, branch) {
7921
8768
  return;
7922
8769
  }
7923
8770
  try {
7924
- execSync2(`git fetch origin ${branch}`, { cwd: workDir, stdio: "ignore", timeout: 6e4 });
8771
+ execSync4(`git fetch origin ${branch}`, { cwd: workDir, stdio: "ignore", timeout: 6e4 });
7925
8772
  } catch {
7926
8773
  process.stderr.write(`[conveyor-agent] pull_branch: fetch failed for ${branch}
7927
8774
  `);
7928
8775
  return;
7929
8776
  }
7930
8777
  try {
7931
- execSync2(`git pull --ff-only origin ${branch}`, {
8778
+ execSync4(`git pull --ff-only origin ${branch}`, {
7932
8779
  cwd: workDir,
7933
8780
  stdio: "ignore",
7934
8781
  timeout: 6e4
@@ -7980,6 +8827,8 @@ var SessionRunner = class _SessionRunner {
7980
8827
  agentSpokeThisTurn = false;
7981
8828
  /** Guards overlapping runs of the periodic git flush. */
7982
8829
  periodicFlushInFlight = false;
8830
+ /** Runtime preview-port poller (v3 dynamic port discovery). */
8831
+ portDiscovery = null;
7983
8832
  constructor(config, callbacks) {
7984
8833
  this.config = config;
7985
8834
  this.callbacks = callbacks;
@@ -8036,7 +8885,6 @@ var SessionRunner = class _SessionRunner {
8036
8885
  this.wireConnectionCallbacks();
8037
8886
  this.lifecycle.startHeartbeat();
8038
8887
  this.lifecycle.startTokenRefresh();
8039
- this.lifecycle.startGitFlush();
8040
8888
  const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
8041
8889
  sessionId: this.sessionId
8042
8890
  });
@@ -8045,6 +8893,11 @@ var SessionRunner = class _SessionRunner {
8045
8893
  this.pendingMessages.push({ content: msg.content, userId: msg.userId });
8046
8894
  }
8047
8895
  }
8896
+ this.portDiscovery = new PortDiscovery({
8897
+ report: (ports) => this.connection.reportDiscoveredPorts(ports)
8898
+ });
8899
+ void this.portDiscovery.start().catch(() => {
8900
+ });
8048
8901
  const agentVersion = readAgentVersion();
8049
8902
  if (agentVersion) {
8050
8903
  this.connection.call("notifyAgentVersion", { sessionId: this.sessionId, agentVersion }).catch(() => {
@@ -8079,6 +8932,17 @@ var SessionRunner = class _SessionRunner {
8079
8932
  await this.shutdown("error");
8080
8933
  return;
8081
8934
  }
8935
+ const gitState = await awaitGitReady({
8936
+ onLog: (m) => process.stderr.write(`[conveyor-agent] ${m}
8937
+ `)
8938
+ });
8939
+ if (gitState === "failed" || gitState === "timeout") {
8940
+ const message = gitState === "failed" ? "Workspace git preparation failed (see pod logs)" : "Workspace git preparation timed out (see pod logs)";
8941
+ this.connection.sendEvent({ type: "error", message });
8942
+ await this.callbacks.onEvent({ type: "error", message });
8943
+ await this.shutdown("error");
8944
+ return;
8945
+ }
8082
8946
  if (process.env.CONVEYOR_GIT_READY !== "1") {
8083
8947
  if (this.fullContext?.githubBranch) {
8084
8948
  ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
@@ -8087,11 +8951,31 @@ var SessionRunner = class _SessionRunner {
8087
8951
  syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
8088
8952
  }
8089
8953
  }
8954
+ if (!this.stopped) {
8955
+ this.lifecycle.startGitFlush();
8956
+ }
8090
8957
  if (this.fullContext?.githubBranch) {
8091
- const restored = restoreWipSnapshot(this.config.workspaceDir, this.fullContext.githubBranch);
8092
- if (restored !== "none") {
8093
- process.stderr.write(`[conveyor-agent] WIP snapshot restore: ${restored}
8958
+ const snapshotUrl = process.env.CONVEYOR_SNAPSHOT_URL;
8959
+ if (snapshotUrl) {
8960
+ const restore = await restoreOnBoot(
8961
+ { snapshotUrl, gitPlan: { branch: this.fullContext.githubBranch } },
8962
+ this.config.workspaceDir
8963
+ );
8964
+ if (restore.source !== "clean") {
8965
+ process.stderr.write(
8966
+ `[conveyor-agent] WorkPreservation restore: source=${restore.source}${restore.fileCount === void 0 ? "" : ` files=${restore.fileCount}`}
8967
+ `
8968
+ );
8969
+ }
8970
+ } else {
8971
+ const restored = restoreWipSnapshot(
8972
+ this.config.workspaceDir,
8973
+ this.fullContext.githubBranch
8974
+ );
8975
+ if (restored !== "none") {
8976
+ process.stderr.write(`[conveyor-agent] WIP snapshot restore: ${restored}
8094
8977
  `);
8978
+ }
8095
8979
  }
8096
8980
  }
8097
8981
  this.mode.applyServerMode(this.fullContext?.agentMode, this.fullContext?.isAuto);
@@ -8421,11 +9305,28 @@ var SessionRunner = class _SessionRunner {
8421
9305
  `
8422
9306
  );
8423
9307
  }
9308
+ await this.uploadGcsSnapshotIfConfigured();
8424
9309
  } catch {
8425
9310
  } finally {
8426
9311
  this.periodicFlushInFlight = false;
8427
9312
  }
8428
9313
  }
9314
+ /** PUT the WorkPreservation snapshot tar to the bundle's capability URL
9315
+ * when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
9316
+ async uploadGcsSnapshotIfConfigured() {
9317
+ const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
9318
+ if (!uploadUrl) return;
9319
+ try {
9320
+ const gcs = await uploadSnapshotToGcs(this.config.workspaceDir, uploadUrl);
9321
+ if (gcs.uploaded) {
9322
+ process.stderr.write(
9323
+ `[conveyor-agent] WorkPreservation GCS snapshot: files=${gcs.fileCount} bytes=${gcs.sizeBytes}
9324
+ `
9325
+ );
9326
+ }
9327
+ } catch {
9328
+ }
9329
+ }
8429
9330
  /** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
8430
9331
  * when a claudespace pod is killed. Must be called BEFORE stop() so the
8431
9332
  * connection is still alive for token refresh. Never throws. */
@@ -8450,6 +9351,7 @@ var SessionRunner = class _SessionRunner {
8450
9351
  `
8451
9352
  );
8452
9353
  }
9354
+ await this.uploadGcsSnapshotIfConfigured();
8453
9355
  } catch (err) {
8454
9356
  const msg = err instanceof Error ? err.message : String(err);
8455
9357
  process.stderr.write(`[conveyor-agent] Shutdown git flush failed: ${msg}
@@ -8459,6 +9361,7 @@ var SessionRunner = class _SessionRunner {
8459
9361
  stop() {
8460
9362
  this.stopped = true;
8461
9363
  this.queryBridge?.stop();
9364
+ this.portDiscovery?.stop();
8462
9365
  this.lifecycle.destroy();
8463
9366
  this.connection.disconnect();
8464
9367
  if (this.inputResolver) {
@@ -8684,6 +9587,38 @@ var SessionRunner = class _SessionRunner {
8684
9587
  this.connection.onPullBranch(({ branch }) => {
8685
9588
  handlePullBranch(this.config.workspaceDir, branch);
8686
9589
  });
9590
+ this.connection.onFinalizeSnapshot(() => void this.finalizeSnapshotNow());
9591
+ }
9592
+ /** Eager finalize snapshot triggered by the reconciler's sleep signal
9593
+ * (session:finalizeSnapshot). Runs pg_dump + a full capture and uploads it
9594
+ * NOW so the sleep confirms on this snapshot instead of waiting for the
9595
+ * ~2min periodic flush — and so sidecar DB state (which ONLY finalizeForSleep
9596
+ * captures via pg_dump) actually lands. Best-effort: on failure the
9597
+ * periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
9598
+ async finalizeSnapshotNow() {
9599
+ const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
9600
+ if (!uploadUrl || this.stopped) return;
9601
+ try {
9602
+ await finalizeForSleep({
9603
+ cwd: this.config.workspaceDir,
9604
+ branch: this.fullContext?.githubBranch ?? "",
9605
+ snapshotUploadUrl: uploadUrl,
9606
+ refreshToken: async () => {
9607
+ try {
9608
+ const res = await this.connection.call("refreshGithubToken", {
9609
+ sessionId: this.connection.sessionId
9610
+ });
9611
+ return res.token;
9612
+ } catch {
9613
+ return void 0;
9614
+ }
9615
+ }
9616
+ });
9617
+ process.stderr.write(
9618
+ "[conveyor-agent] Finalize snapshot (pg_dump) uploaded on sleep signal\n"
9619
+ );
9620
+ } catch {
9621
+ }
8687
9622
  }
8688
9623
  /** Proactively refresh the GitHub token before the 1-hour expiry. */
8689
9624
  async refreshGithubToken() {
@@ -8727,6 +9662,7 @@ var SessionRunner = class _SessionRunner {
8727
9662
  process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
8728
9663
  `);
8729
9664
  this.connection.sendEvent({ type: "shutdown", reason: finalState });
9665
+ this.portDiscovery?.stop();
8730
9666
  this.lifecycle.destroy();
8731
9667
  try {
8732
9668
  await this.setState(finalState);
@@ -8771,13 +9707,13 @@ var SessionRunner = class _SessionRunner {
8771
9707
  };
8772
9708
 
8773
9709
  // src/setup/config.ts
8774
- import { readFile as readFile4 } from "fs/promises";
9710
+ import { readFile as readFile5 } from "fs/promises";
8775
9711
  import { join as join8 } from "path";
8776
9712
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
8777
9713
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
8778
9714
  async function loadForwardPorts(workspaceDir) {
8779
9715
  try {
8780
- const raw = await readFile4(join8(workspaceDir, DEVCONTAINER_PATH), "utf-8");
9716
+ const raw = await readFile5(join8(workspaceDir, DEVCONTAINER_PATH), "utf-8");
8781
9717
  const parsed = JSON.parse(raw);
8782
9718
  const ports = (parsed.forwardPorts ?? []).filter(
8783
9719
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -8821,7 +9757,7 @@ function loadConveyorConfig() {
8821
9757
  }
8822
9758
 
8823
9759
  // src/setup/commands.ts
8824
- import { spawn, execSync as execSync3 } from "child_process";
9760
+ import { spawn, execSync as execSync5 } from "child_process";
8825
9761
  function runSetupCommand(cmd, cwd, onOutput) {
8826
9762
  return new Promise((resolve, reject) => {
8827
9763
  const child = spawn("sh", ["-c", cmd], {
@@ -8850,7 +9786,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
8850
9786
  var AUTH_TOKEN_TIMEOUT_MS = 3e4;
8851
9787
  function runAuthTokenCommand(cmd, userEmail, cwd) {
8852
9788
  try {
8853
- const output = execSync3(`${cmd} ${JSON.stringify(userEmail)}`, {
9789
+ const output = execSync5(`${cmd} ${JSON.stringify(userEmail)}`, {
8854
9790
  cwd,
8855
9791
  timeout: AUTH_TOKEN_TIMEOUT_MS,
8856
9792
  stdio: ["ignore", "pipe", "ignore"],
@@ -8880,10 +9816,10 @@ function runStartCommand(cmd, cwd, onOutput) {
8880
9816
  }
8881
9817
 
8882
9818
  // src/setup/codespace.ts
8883
- import { execSync as execSync4 } from "child_process";
9819
+ import { execSync as execSync6 } from "child_process";
8884
9820
  function unshallowRepo(workspaceDir) {
8885
9821
  try {
8886
- execSync4("git fetch --unshallow", {
9822
+ execSync6("git fetch --unshallow", {
8887
9823
  cwd: workspaceDir,
8888
9824
  timeout: 6e4,
8889
9825
  stdio: "ignore"
@@ -8896,6 +9832,8 @@ export {
8896
9832
  fetchBootstrap,
8897
9833
  applyBootstrapToEnv,
8898
9834
  AgentConnection,
9835
+ DEFAULT_LIFECYCLE_CONFIG,
9836
+ Lifecycle,
8899
9837
  createServiceLogger,
8900
9838
  hasUncommittedChanges,
8901
9839
  getCurrentBranch,
@@ -8905,6 +9843,13 @@ export {
8905
9843
  flushPendingChanges,
8906
9844
  pushToOrigin,
8907
9845
  PlanSync,
9846
+ awaitGitReady,
9847
+ uploadSnapshotToGcs,
9848
+ captureSnapshot,
9849
+ startPeriodic,
9850
+ stop,
9851
+ finalizeForSleep,
9852
+ restoreOnBoot,
8908
9853
  SessionRunner,
8909
9854
  loadForwardPorts,
8910
9855
  buildSessionPreviewPorts,
@@ -8914,4 +9859,4 @@ export {
8914
9859
  runStartCommand,
8915
9860
  unshallowRepo
8916
9861
  };
8917
- //# sourceMappingURL=chunk-34NA4BCK.js.map
9862
+ //# sourceMappingURL=chunk-QKRPJ7DB.js.map