@rallycry/conveyor-agent 10.2.2 → 10.2.4

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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ LoopLagMonitor
3
+ } from "./chunk-7TQO4ZF4.js";
4
+
1
5
  // src/setup/bootstrap.ts
2
6
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
3
7
  var RETRY_DELAYS_MS = [5e3, 1e4, 2e4];
@@ -115,6 +119,9 @@ function applyBootstrapToEnv(config) {
115
119
  }
116
120
 
117
121
  // src/connection/agent-connection.ts
122
+ import { existsSync } from "fs";
123
+ import { fileURLToPath } from "url";
124
+ import { Worker } from "worker_threads";
118
125
  import { io } from "socket.io-client";
119
126
 
120
127
  // src/setup/bootstrap-poll.ts
@@ -451,8 +458,12 @@ var AgentConnection = class _AgentConnection {
451
458
  void this.refreshTaskTokenFromBootstrap().catch(() => {
452
459
  });
453
460
  });
454
- this.socket.io.on("reconnect", () => {
455
- process.stderr.write("[conveyor-agent] Reconnected\n");
461
+ this.socket.io.on("reconnect", (reconnectAttempts) => {
462
+ process.stderr.write(
463
+ `[conveyor-agent] Reconnected (attempts: ${reconnectAttempts}, ${(/* @__PURE__ */ new Date()).toISOString()})
464
+ `
465
+ );
466
+ this.sendHeartbeat();
456
467
  void this.reconnectToSession();
457
468
  });
458
469
  this.socket.io.on("reconnect_attempt", () => {
@@ -462,6 +473,7 @@ var AgentConnection = class _AgentConnection {
462
473
  disconnect() {
463
474
  this.stopProactiveTokenRefresh();
464
475
  this.clearSocketRecycle();
476
+ this.stopHeartbeatWorker();
465
477
  void this.flushEvents();
466
478
  if (this.socket) {
467
479
  this.socket.io.reconnection(false);
@@ -668,6 +680,19 @@ var AgentConnection = class _AgentConnection {
668
680
  }).catch(() => {
669
681
  });
670
682
  }
683
+ /**
684
+ * Forward one compact chat-proxy event derived from the transcript JSONL to
685
+ * the relay (fire-and-forget). Feeds the experimental chat PTY proxy ring.
686
+ * Old servers that don't know the method reject harmlessly.
687
+ */
688
+ sendPtyChatEvent(event) {
689
+ if (!this.socket) return;
690
+ void this.call("ptyChatEvent", {
691
+ sessionId: this.config.sessionId,
692
+ event
693
+ }).catch(() => {
694
+ });
695
+ }
671
696
  /**
672
697
  * Report that the interactive CLI process for this session has died and no
673
698
  * respawn is imminent (fire-and-forget). The server clears the scrollback
@@ -777,7 +802,7 @@ var AgentConnection = class _AgentConnection {
777
802
  if (this.recentMessages.length > 3) this.recentMessages.shift();
778
803
  return { duplicate: false };
779
804
  }
780
- sendHeartbeat() {
805
+ sendHeartbeat(loopLagMs) {
781
806
  if (!this.socket) return;
782
807
  const statusMap = {
783
808
  running: "active",
@@ -790,10 +815,67 @@ var AgentConnection = class _AgentConnection {
790
815
  void this.call("heartbeat", {
791
816
  sessionId: this.config.sessionId,
792
817
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
793
- status: heartbeatStatus
818
+ status: heartbeatStatus,
819
+ ...loopLagMs !== void 0 && loopLagMs > 0 ? { loopLagMs: Math.round(loopLagMs) } : {}
794
820
  }).catch(() => {
795
821
  });
796
822
  }
823
+ // ── Starvation-proof heartbeat worker ────────────────────────────────
824
+ //
825
+ // A worker thread with its own event loop + Socket.IO connection keeps the
826
+ // v3 session lease renewed even when the MAIN loop is stalled (the failure
827
+ // mode where a heavy gate got the session declared stranded and restarted
828
+ // mid-run). Best-effort by design: any spawn/runtime failure just degrades
829
+ // heartbeats to main-loop-only. See heartbeat-worker.ts for the policy.
830
+ heartbeatWorker = null;
831
+ startHeartbeatWorker(sharedBuffer, intervalMs = 3e4) {
832
+ if (this.heartbeatWorker) return;
833
+ try {
834
+ const workerUrl = new URL("./heartbeat-worker.js", import.meta.url);
835
+ if (!existsSync(fileURLToPath(workerUrl))) {
836
+ process.stderr.write(
837
+ "[conveyor-agent] heartbeat worker bundle not found \u2014 main-loop heartbeat only\n"
838
+ );
839
+ return;
840
+ }
841
+ const worker = new Worker(workerUrl, {
842
+ workerData: {
843
+ apiUrl: this.config.apiUrl,
844
+ taskToken: this.config.taskToken,
845
+ sessionId: this.config.sessionId,
846
+ runnerMode: this.config.runnerMode ?? "task",
847
+ sharedBuffer,
848
+ intervalMs
849
+ }
850
+ });
851
+ worker.unref();
852
+ worker.on("error", (err) => {
853
+ process.stderr.write(`[conveyor-agent] heartbeat worker error: ${err.message}
854
+ `);
855
+ this.heartbeatWorker = null;
856
+ });
857
+ worker.on("exit", (code) => {
858
+ if (code !== 0) {
859
+ process.stderr.write(`[conveyor-agent] heartbeat worker exited (code ${code})
860
+ `);
861
+ }
862
+ this.heartbeatWorker = null;
863
+ });
864
+ this.heartbeatWorker = worker;
865
+ process.stderr.write("[conveyor-agent] heartbeat worker started\n");
866
+ } catch (err) {
867
+ process.stderr.write(
868
+ `[conveyor-agent] heartbeat worker failed to start: ${err instanceof Error ? err.message : String(err)}
869
+ `
870
+ );
871
+ this.heartbeatWorker = null;
872
+ }
873
+ }
874
+ stopHeartbeatWorker() {
875
+ const worker = this.heartbeatWorker;
876
+ this.heartbeatWorker = null;
877
+ if (worker) void worker.terminate();
878
+ }
797
879
  emitModeChanged(agentMode) {
798
880
  this.sendEvent({ type: "mode_changed", agentMode });
799
881
  }
@@ -927,6 +1009,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
927
1009
  auth.taskToken = result.config.taskToken;
928
1010
  }
929
1011
  }
1012
+ this.heartbeatWorker?.postMessage({ taskToken: result.config.taskToken });
930
1013
  }
931
1014
  const refreshedClaude = Boolean(result.config.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
932
1015
  return { refreshedClaude, refreshedTaskToken };
@@ -959,6 +1042,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
959
1042
  auth.taskToken = bundle.sessionJwt;
960
1043
  }
961
1044
  }
1045
+ this.heartbeatWorker?.postMessage({ taskToken: bundle.sessionJwt });
962
1046
  }
963
1047
  const refreshedClaude = Boolean(bundle.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
964
1048
  return { refreshedClaude, refreshedTaskToken };
@@ -1040,14 +1124,25 @@ function createServiceLogger(service) {
1040
1124
  }
1041
1125
 
1042
1126
  // src/runner/git-utils.ts
1043
- import { execSync, exec } from "child_process";
1127
+ import { execFile } from "child_process";
1044
1128
  import { realpathSync } from "fs";
1045
1129
  import { promisify } from "util";
1046
- var execAsync = promisify(exec);
1047
- function syncWithBaseBranch(cwd, baseBranch) {
1130
+ var execFileAsync = promisify(execFile);
1131
+ var GIT_TIMEOUT_MS = 6e4;
1132
+ var GIT_SLOW_TIMEOUT_MS = 12e4;
1133
+ var GIT_MAX_BUFFER = 16 * 1024 * 1024;
1134
+ async function git(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
1135
+ const { stdout } = await execFileAsync("git", args, {
1136
+ cwd,
1137
+ timeout: timeoutMs,
1138
+ maxBuffer: GIT_MAX_BUFFER
1139
+ });
1140
+ return stdout.toString().trim();
1141
+ }
1142
+ async function syncWithBaseBranch(cwd, baseBranch) {
1048
1143
  if (!baseBranch) return true;
1049
1144
  try {
1050
- execSync(`git fetch origin ${baseBranch}`, { cwd, stdio: "ignore", timeout: 6e4 });
1145
+ await git(cwd, ["fetch", "origin", baseBranch]);
1051
1146
  } catch {
1052
1147
  process.stderr.write(
1053
1148
  `[conveyor-agent] Warning: git fetch origin ${baseBranch} failed, continuing with current base
@@ -1056,14 +1151,14 @@ function syncWithBaseBranch(cwd, baseBranch) {
1056
1151
  return false;
1057
1152
  }
1058
1153
  try {
1059
- execSync(`git merge origin/${baseBranch} --no-edit`, { cwd, stdio: "ignore", timeout: 3e4 });
1154
+ await git(cwd, ["merge", `origin/${baseBranch}`, "--no-edit"], 3e4);
1060
1155
  } catch {
1061
1156
  process.stderr.write(
1062
1157
  `[conveyor-agent] Warning: merge origin/${baseBranch} failed, aborting merge and continuing
1063
1158
  `
1064
1159
  );
1065
1160
  try {
1066
- execSync("git merge --abort", { cwd, stdio: "ignore" });
1161
+ await git(cwd, ["merge", "--abort"], 3e4);
1067
1162
  } catch {
1068
1163
  }
1069
1164
  return false;
@@ -1072,23 +1167,19 @@ function syncWithBaseBranch(cwd, baseBranch) {
1072
1167
  `);
1073
1168
  return true;
1074
1169
  }
1075
- function ensureOnTaskBranch(cwd, taskBranch) {
1170
+ async function ensureOnTaskBranch(cwd, taskBranch) {
1076
1171
  if (!taskBranch) return true;
1077
- const current = getCurrentBranch(cwd);
1172
+ const current = await getCurrentBranch(cwd);
1078
1173
  if (current === taskBranch) return true;
1079
1174
  try {
1080
- execSync(`git fetch origin ${taskBranch}`, { cwd, stdio: "ignore", timeout: 6e4 });
1175
+ await git(cwd, ["fetch", "origin", taskBranch]);
1081
1176
  } catch {
1082
1177
  process.stderr.write(`[conveyor-agent] Warning: git fetch origin ${taskBranch} failed
1083
1178
  `);
1084
1179
  return false;
1085
1180
  }
1086
1181
  try {
1087
- execSync(`git checkout -B ${taskBranch} origin/${taskBranch}`, {
1088
- cwd,
1089
- stdio: "ignore",
1090
- timeout: 3e4
1091
- });
1182
+ await git(cwd, ["checkout", "-B", taskBranch, `origin/${taskBranch}`], 3e4);
1092
1183
  } catch {
1093
1184
  process.stderr.write(`[conveyor-agent] Warning: git checkout ${taskBranch} failed
1094
1185
  `);
@@ -1098,74 +1189,62 @@ function ensureOnTaskBranch(cwd, taskBranch) {
1098
1189
  `);
1099
1190
  return true;
1100
1191
  }
1101
- function hasUncommittedChanges(cwd) {
1102
- const status = execSync("git status --porcelain", {
1103
- cwd,
1104
- stdio: ["ignore", "pipe", "ignore"]
1105
- }).toString().trim();
1192
+ async function hasUncommittedChanges(cwd) {
1193
+ const status = await git(cwd, ["status", "--porcelain"], GIT_SLOW_TIMEOUT_MS);
1106
1194
  return status.length > 0;
1107
1195
  }
1108
- function getCurrentBranch(cwd) {
1196
+ async function getCurrentBranch(cwd) {
1109
1197
  try {
1110
- const branch = execSync("git branch --show-current", {
1111
- cwd,
1112
- stdio: ["ignore", "pipe", "ignore"]
1113
- }).toString().trim();
1198
+ const branch = await git(cwd, ["branch", "--show-current"]);
1114
1199
  return branch || null;
1115
1200
  } catch {
1116
1201
  return null;
1117
1202
  }
1118
1203
  }
1119
- function hasUnpushedCommits(cwd) {
1204
+ async function hasUnpushedCommits(cwd) {
1120
1205
  try {
1121
- const currentBranch = getCurrentBranch(cwd);
1206
+ const currentBranch = await getCurrentBranch(cwd);
1122
1207
  if (!currentBranch) return false;
1123
1208
  try {
1124
- execSync(`git rev-parse origin/${currentBranch}`, {
1125
- cwd,
1126
- stdio: ["ignore", "pipe", "ignore"]
1127
- });
1209
+ await git(cwd, ["rev-parse", `origin/${currentBranch}`]);
1128
1210
  } catch {
1129
1211
  try {
1130
- execSync("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] });
1212
+ await git(cwd, ["rev-parse", "HEAD"]);
1131
1213
  return true;
1132
1214
  } catch {
1133
1215
  return false;
1134
1216
  }
1135
1217
  }
1136
- const ahead = execSync(`git rev-list --count HEAD --not origin/${currentBranch}`, {
1137
- cwd,
1138
- stdio: ["ignore", "pipe", "ignore"]
1139
- }).toString().trim();
1218
+ const ahead = await git(cwd, [
1219
+ "rev-list",
1220
+ "--count",
1221
+ "HEAD",
1222
+ "--not",
1223
+ `origin/${currentBranch}`
1224
+ ]);
1140
1225
  return parseInt(ahead, 10) > 0;
1141
1226
  } catch {
1142
1227
  return false;
1143
1228
  }
1144
1229
  }
1145
- function stageAndCommit(cwd, message) {
1230
+ async function stageAndCommit(cwd, message) {
1146
1231
  try {
1147
- execSync("git add -A", { cwd, stdio: ["ignore", "pipe", "ignore"] });
1148
- if (!hasUncommittedChanges(cwd)) return null;
1149
- execSync(`git commit -m ${JSON.stringify(message)}`, {
1150
- cwd,
1151
- stdio: ["ignore", "pipe", "ignore"]
1152
- });
1153
- return execSync("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
1232
+ await git(cwd, ["add", "-A"], GIT_SLOW_TIMEOUT_MS);
1233
+ if (!await hasUncommittedChanges(cwd)) return null;
1234
+ await git(cwd, ["commit", "-m", message], GIT_SLOW_TIMEOUT_MS);
1235
+ return await git(cwd, ["rev-parse", "HEAD"]);
1154
1236
  } catch {
1155
1237
  return null;
1156
1238
  }
1157
1239
  }
1158
1240
  async function tryPush(cwd, branch, skipVerify = false) {
1159
- const noVerify = skipVerify ? "--no-verify " : "";
1241
+ const noVerify = skipVerify ? ["--no-verify"] : [];
1160
1242
  try {
1161
- await execAsync(`git push ${noVerify}origin ${branch}`, { cwd, timeout: 3e4 });
1243
+ await git(cwd, ["push", ...noVerify, "origin", branch], 3e4);
1162
1244
  return true;
1163
1245
  } catch {
1164
1246
  try {
1165
- await execAsync(`git push ${noVerify}--force-with-lease origin ${branch}`, {
1166
- cwd,
1167
- timeout: 3e4
1168
- });
1247
+ await git(cwd, ["push", ...noVerify, "--force-with-lease", "origin", branch], 3e4);
1169
1248
  return true;
1170
1249
  } catch {
1171
1250
  return false;
@@ -1174,7 +1253,7 @@ async function tryPush(cwd, branch, skipVerify = false) {
1174
1253
  }
1175
1254
  async function isAuthError(cwd) {
1176
1255
  try {
1177
- await execAsync("git push --dry-run", { cwd, timeout: 3e4 });
1256
+ await git(cwd, ["push", "--dry-run"], 3e4);
1178
1257
  return false;
1179
1258
  } catch (err) {
1180
1259
  if (err.killed) return true;
@@ -1184,16 +1263,18 @@ async function isAuthError(cwd) {
1184
1263
  return /authentication|authorization|403|401|token/i.test(msg);
1185
1264
  }
1186
1265
  }
1187
- function updateRemoteToken(cwd, token) {
1266
+ async function updateRemoteToken(cwd, token) {
1188
1267
  try {
1189
- const url = execSync("git remote get-url origin", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
1268
+ const url = await git(cwd, ["remote", "get-url", "origin"]);
1190
1269
  const match = url.match(/github\.com[/:]([^/]+\/[^/.]+)/);
1191
1270
  if (match) {
1192
1271
  const repo = match[1].replace(/\.git$/, "");
1193
- execSync(
1194
- `git remote set-url origin "https://x-access-token:${token}@github.com/${repo}.git"`,
1195
- { cwd, stdio: ["ignore", "pipe", "ignore"] }
1196
- );
1272
+ await git(cwd, [
1273
+ "remote",
1274
+ "set-url",
1275
+ "origin",
1276
+ `https://x-access-token:${token}@github.com/${repo}.git`
1277
+ ]);
1197
1278
  }
1198
1279
  } catch {
1199
1280
  }
@@ -1202,30 +1283,24 @@ function wipRefForBranch(branch) {
1202
1283
  return `conveyor-wip/${branch}`;
1203
1284
  }
1204
1285
  var wipRefPushed = /* @__PURE__ */ new Set();
1205
- function createWipSnapshot(cwd, message) {
1286
+ async function createWipSnapshot(cwd, message) {
1206
1287
  try {
1207
- execSync("git add -A", { cwd, stdio: ["ignore", "pipe", "ignore"] });
1208
- const sha = execSync(`git stash create ${JSON.stringify(message)}`, {
1209
- cwd,
1210
- stdio: ["ignore", "pipe", "ignore"]
1211
- }).toString().trim();
1288
+ await git(cwd, ["add", "-A"], GIT_SLOW_TIMEOUT_MS);
1289
+ const sha = await git(cwd, ["stash", "create", message], GIT_SLOW_TIMEOUT_MS);
1212
1290
  return sha || null;
1213
1291
  } catch {
1214
1292
  return null;
1215
1293
  } finally {
1216
1294
  try {
1217
- execSync("git reset -q", { cwd, stdio: ["ignore", "pipe", "ignore"] });
1295
+ await git(cwd, ["reset", "-q"], GIT_SLOW_TIMEOUT_MS);
1218
1296
  } catch {
1219
1297
  }
1220
1298
  }
1221
1299
  }
1222
- function tryPushRefspec(cwd, refspec, force = false) {
1300
+ async function tryPushRefspec(cwd, refspec, force = false) {
1223
1301
  try {
1224
- execSync(`git push --no-verify ${force ? "--force " : ""}origin ${JSON.stringify(refspec)}`, {
1225
- cwd,
1226
- stdio: ["ignore", "pipe", "pipe"],
1227
- timeout: 3e4
1228
- });
1302
+ const forceArgs = force ? ["--force"] : [];
1303
+ await git(cwd, ["push", "--no-verify", ...forceArgs, "origin", refspec], 3e4);
1229
1304
  return true;
1230
1305
  } catch {
1231
1306
  return false;
@@ -1236,49 +1311,39 @@ async function refreshRemoteToken(cwd, refreshToken) {
1236
1311
  try {
1237
1312
  const token = await refreshToken();
1238
1313
  if (token) {
1239
- updateRemoteToken(cwd, token);
1314
+ await updateRemoteToken(cwd, token);
1240
1315
  process.env.GITHUB_TOKEN = token;
1241
1316
  process.env.GH_TOKEN = token;
1242
1317
  }
1243
1318
  } catch {
1244
1319
  }
1245
1320
  }
1246
- function restoreWipSnapshot(cwd, branch) {
1321
+ async function restoreWipSnapshot(cwd, branch) {
1247
1322
  if (!branch) return "none";
1248
1323
  const ref = wipRefForBranch(branch);
1249
1324
  try {
1250
- execSync(`git fetch origin +refs/heads/${ref}:refs/remotes/origin/${ref}`, {
1251
- cwd,
1252
- stdio: "ignore",
1253
- timeout: 6e4
1254
- });
1325
+ await git(cwd, ["fetch", "origin", `+refs/heads/${ref}:refs/remotes/origin/${ref}`]);
1255
1326
  } catch {
1256
1327
  return "none";
1257
1328
  }
1258
1329
  wipRefPushed.add(cwd);
1259
1330
  try {
1260
- const sha = execSync(`git rev-parse refs/remotes/origin/${ref}`, {
1261
- cwd,
1262
- stdio: ["ignore", "pipe", "ignore"]
1263
- }).toString().trim();
1264
- const parent = execSync(`git rev-parse "${sha}^"`, {
1265
- cwd,
1266
- stdio: ["ignore", "pipe", "ignore"]
1267
- }).toString().trim();
1268
- const head = execSync("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
1331
+ const sha = await git(cwd, ["rev-parse", `refs/remotes/origin/${ref}`]);
1332
+ const parent = await git(cwd, ["rev-parse", `${sha}^`]);
1333
+ const head = await git(cwd, ["rev-parse", "HEAD"]);
1269
1334
  if (parent !== head) {
1270
1335
  try {
1271
- execSync(`git stash apply ${sha}`, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1336
+ await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
1272
1337
  return "applied";
1273
1338
  } catch {
1274
1339
  try {
1275
- execSync("git reset --merge", { cwd, stdio: "ignore" });
1340
+ await git(cwd, ["reset", "--merge"], GIT_SLOW_TIMEOUT_MS);
1276
1341
  } catch {
1277
1342
  }
1278
1343
  return "stale";
1279
1344
  }
1280
1345
  }
1281
- execSync(`git stash apply ${sha}`, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1346
+ await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
1282
1347
  return "applied";
1283
1348
  } catch {
1284
1349
  return "failed";
@@ -1289,10 +1354,10 @@ async function flushPendingChanges(cwd, opts) {
1289
1354
  let pushed = false;
1290
1355
  let hadWork = false;
1291
1356
  try {
1292
- const branch = getCurrentBranch(cwd);
1357
+ const branch = await getCurrentBranch(cwd);
1293
1358
  if (!branch) return { committed, pushed, hadWork };
1294
- const dirty = hasUncommittedChanges(cwd);
1295
- const unpushed = hasUnpushedCommits(cwd);
1359
+ const dirty = await hasUncommittedChanges(cwd);
1360
+ const unpushed = await hasUnpushedCommits(cwd);
1296
1361
  if (!dirty && !unpushed) {
1297
1362
  await dropStaleWipRef(cwd, branch, opts?.refreshToken);
1298
1363
  return { committed, pushed, hadWork };
@@ -1304,9 +1369,9 @@ async function flushPendingChanges(cwd, opts) {
1304
1369
  }
1305
1370
  if (dirty) {
1306
1371
  const message = opts?.wipMessage ?? "WIP: conveyor-agent snapshot";
1307
- const sha = createWipSnapshot(cwd, message);
1372
+ const sha = await createWipSnapshot(cwd, message);
1308
1373
  if (sha) {
1309
- committed = tryPushRefspec(cwd, `${sha}:refs/heads/${wipRefForBranch(branch)}`, true);
1374
+ committed = await tryPushRefspec(cwd, `${sha}:refs/heads/${wipRefForBranch(branch)}`, true);
1310
1375
  if (committed) wipRefPushed.add(cwd);
1311
1376
  }
1312
1377
  }
@@ -1317,19 +1382,19 @@ async function flushPendingChanges(cwd, opts) {
1317
1382
  async function dropStaleWipRef(cwd, branch, refreshToken) {
1318
1383
  if (!wipRefPushed.has(cwd)) return;
1319
1384
  await refreshRemoteToken(cwd, refreshToken);
1320
- if (tryPushRefspec(cwd, `:refs/heads/${wipRefForBranch(branch)}`)) {
1385
+ if (await tryPushRefspec(cwd, `:refs/heads/${wipRefForBranch(branch)}`)) {
1321
1386
  wipRefPushed.delete(cwd);
1322
1387
  }
1323
1388
  }
1324
1389
  async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
1325
1390
  try {
1326
- const currentBranch = getCurrentBranch(cwd);
1391
+ const currentBranch = await getCurrentBranch(cwd);
1327
1392
  if (!currentBranch) return false;
1328
1393
  if (refreshToken) {
1329
1394
  try {
1330
1395
  const token = await refreshToken();
1331
1396
  if (token) {
1332
- updateRemoteToken(cwd, token);
1397
+ await updateRemoteToken(cwd, token);
1333
1398
  process.env.GITHUB_TOKEN = token;
1334
1399
  process.env.GH_TOKEN = token;
1335
1400
  }
@@ -1340,7 +1405,7 @@ async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
1340
1405
  if (refreshToken && await isAuthError(cwd)) {
1341
1406
  const token = await refreshToken();
1342
1407
  if (token) {
1343
- updateRemoteToken(cwd, token);
1408
+ await updateRemoteToken(cwd, token);
1344
1409
  process.env.GITHUB_TOKEN = token;
1345
1410
  process.env.GH_TOKEN = token;
1346
1411
  return await tryPush(cwd, currentBranch, skipVerify);
@@ -1354,12 +1419,9 @@ async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
1354
1419
  function branchBackupRef(branch) {
1355
1420
  return `conveyor-wip/branches/${branch}`;
1356
1421
  }
1357
- function listWorktrees(cwd) {
1422
+ async function listWorktrees(cwd) {
1358
1423
  try {
1359
- const out = execSync("git worktree list --porcelain", {
1360
- cwd,
1361
- stdio: ["ignore", "pipe", "ignore"]
1362
- }).toString();
1424
+ const out = await git(cwd, ["worktree", "list", "--porcelain"]);
1363
1425
  const result = [];
1364
1426
  for (const entry of out.split("\n\n")) {
1365
1427
  const lines = entry.trim().split("\n");
@@ -1376,420 +1438,79 @@ function listWorktrees(cwd) {
1376
1438
  return [];
1377
1439
  }
1378
1440
  }
1379
- function listLocalBranches(cwd) {
1441
+ async function listLocalBranches(cwd) {
1380
1442
  try {
1381
- return execSync("git for-each-ref --format='%(refname:short)' refs/heads/", {
1382
- cwd,
1383
- stdio: ["ignore", "pipe", "ignore"]
1384
- }).toString().split("\n").map((s) => s.trim()).filter(Boolean);
1443
+ const out = await git(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]);
1444
+ return out.split("\n").map((s) => s.trim()).filter(Boolean);
1385
1445
  } catch {
1386
1446
  return [];
1387
1447
  }
1388
1448
  }
1389
- function branchUnpushedCount(cwd, branch) {
1449
+ async function branchUnpushedCount(cwd, branch) {
1390
1450
  try {
1391
- const n = execSync(`git rev-list --count ${JSON.stringify(branch)} --not --remotes=origin`, {
1392
- cwd,
1393
- stdio: ["ignore", "pipe", "ignore"]
1394
- }).toString().trim();
1451
+ const n = await git(cwd, ["rev-list", "--count", branch, "--not", "--remotes=origin"]);
1395
1452
  return Number.parseInt(n, 10) || 0;
1396
1453
  } catch {
1397
1454
  return 0;
1398
1455
  }
1399
- }
1400
- function samePath(a, b) {
1401
- try {
1402
- return realpathSync(a) === realpathSync(b);
1403
- } catch {
1404
- return a === b;
1405
- }
1406
- }
1407
- async function flushAllPendingWork(cwd, opts) {
1408
- try {
1409
- const primary = await flushPendingChanges(cwd, opts);
1410
- await refreshRemoteToken(cwd, opts?.refreshToken);
1411
- const currentBranch = getCurrentBranch(cwd);
1412
- const worktreesSnapshotted = snapshotOtherWorktrees(cwd, opts?.wipMessage);
1413
- const branchesBackedUp = backupOtherBranches(cwd, currentBranch);
1414
- return {
1415
- hadWork: primary.hadWork || worktreesSnapshotted > 0 || branchesBackedUp > 0,
1416
- branchesBackedUp,
1417
- worktreesSnapshotted
1418
- };
1419
- } catch {
1420
- return { hadWork: false, branchesBackedUp: 0, worktreesSnapshotted: 0 };
1421
- }
1422
- }
1423
- function snapshotOtherWorktrees(cwd, wipMessage) {
1424
- let count = 0;
1425
- for (const wt of listWorktrees(cwd)) {
1426
- if (samePath(wt.path, cwd) || !wt.branch) continue;
1427
- try {
1428
- if (!hasUncommittedChanges(wt.path)) continue;
1429
- const sha = createWipSnapshot(wt.path, wipMessage ?? "WIP: conveyor-agent snapshot");
1430
- if (sha && tryPushRefspec(wt.path, `${sha}:refs/heads/${wipRefForBranch(wt.branch)}`, true)) {
1431
- wipRefPushed.add(wt.path);
1432
- count++;
1433
- }
1434
- } catch {
1435
- }
1436
- }
1437
- return count;
1438
- }
1439
- function backupOtherBranches(cwd, currentBranch) {
1440
- let count = 0;
1441
- for (const branch of listLocalBranches(cwd)) {
1442
- if (branch === currentBranch || branch.startsWith("conveyor-wip/")) continue;
1443
- try {
1444
- if (branchUnpushedCount(cwd, branch) === 0) continue;
1445
- if (tryPushRefspec(cwd, `refs/heads/${branch}:refs/heads/${branchBackupRef(branch)}`, true)) {
1446
- count++;
1447
- }
1448
- } catch {
1449
- }
1450
- }
1451
- return count;
1452
- }
1453
-
1454
- // src/runner/plan-sync.ts
1455
- import { readdirSync, statSync, readFileSync } from "fs";
1456
- import { join as join2 } from "path";
1457
-
1458
- // src/harness/pty/settings.ts
1459
- import { mkdir, writeFile, chmod } from "fs/promises";
1460
- import { homedir } from "os";
1461
- import { join } from "path";
1462
- function claudeConfigHome() {
1463
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
1464
- }
1465
- function projectSlug(cwd) {
1466
- return cwd.replace(/\//g, "-");
1467
- }
1468
- function sessionTranscriptPath(cwd, sessionId) {
1469
- return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
1470
- }
1471
- function configHomePlansDir() {
1472
- return join(claudeConfigHome(), "plans");
1473
- }
1474
- var ALLOW_RULES = [
1475
- "Bash",
1476
- "BashOutput",
1477
- "Edit",
1478
- "Write",
1479
- "Read",
1480
- "Glob",
1481
- "Grep",
1482
- "WebFetch",
1483
- "WebSearch",
1484
- "NotebookEdit",
1485
- "Task",
1486
- "TodoWrite",
1487
- "ToolSearch",
1488
- "KillShell",
1489
- "SlashCommand",
1490
- "Skill",
1491
- "mcp__conveyor__*"
1492
- ];
1493
- var HOOK_HELPER_SOURCE = `"use strict";
1494
- const net = require("node:net");
1495
- const crypto = require("node:crypto");
1496
-
1497
- const PRE_TOOL_USE_TIMEOUT_MS = 110000;
1498
- const ASK_USER_QUESTION_TIMEOUT_MS = 5000;
1499
-
1500
- let raw = "";
1501
- process.stdin.setEncoding("utf8");
1502
- process.stdin.on("data", (chunk) => {
1503
- raw += chunk;
1504
- });
1505
- process.stdin.on("end", () => {
1506
- let payload = {};
1507
- try {
1508
- const parsed = JSON.parse(raw);
1509
- if (parsed && typeof parsed === "object") payload = parsed;
1510
- } catch {
1511
- payload = {};
1512
- }
1513
- if (payload.hook_event_name === "PreToolUse") {
1514
- preToolUse(payload);
1515
- } else {
1516
- relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
1517
- }
1518
- });
1519
-
1520
- function relayProgress(toolName) {
1521
- const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
1522
- let done = false;
1523
- const finish = () => {
1524
- if (done) return;
1525
- done = true;
1526
- process.stdout.write(JSON.stringify({ continue: true }));
1527
- process.exit(0);
1528
- };
1529
- const fallback = setTimeout(finish, 250);
1530
- if (!socketPath) {
1531
- clearTimeout(fallback);
1532
- finish();
1533
- return;
1534
- }
1535
- const client = net.connect(socketPath, () => {
1536
- // PostToolUse does not supply tool duration, so elapsed_time_seconds is
1537
- // omitted rather than reported as a misleading 0 (the field is optional).
1538
- const line = JSON.stringify({ tool_name: toolName }) + "\\n";
1539
- client.write(line, () => {
1540
- client.end();
1541
- });
1542
- });
1543
- client.on("close", () => {
1544
- clearTimeout(fallback);
1545
- finish();
1546
- });
1547
- client.on("error", () => {
1548
- clearTimeout(fallback);
1549
- finish();
1550
- });
1551
- }
1552
-
1553
- function preToolUse(payload) {
1554
- const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
1555
- let done = false;
1556
- const respond = (decision, reason) => {
1557
- if (done) return;
1558
- done = true;
1559
- process.stdout.write(
1560
- JSON.stringify({
1561
- hookSpecificOutput: {
1562
- hookEventName: "PreToolUse",
1563
- permissionDecision: decision,
1564
- ...(reason ? { permissionDecisionReason: reason } : {}),
1565
- },
1566
- }),
1567
- );
1568
- process.exit(0);
1569
- };
1570
- // MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
1571
- // Plan mode prompts for them despite permissions.allow (the CLI can't
1572
- // classify MCP tools as read-only), and the pod is the sandbox.
1573
- if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
1574
- respond("allow");
1575
- return;
1576
- }
1577
- // AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
1578
- // missing/slow socket never denies the questionnaire. Everything else
1579
- // (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
1580
- const failOpen = payload.tool_name === "AskUserQuestion";
1581
- const respondUnavailable = () =>
1582
- failOpen
1583
- ? respond("allow")
1584
- : respond(
1585
- "deny",
1586
- "Conveyor validation is unavailable right now. Wait a moment and call the tool again.",
1587
- );
1588
- if (!socketPath) {
1589
- respondUnavailable();
1590
- return;
1591
- }
1592
- const id = crypto.randomBytes(8).toString("hex");
1593
- const timer = setTimeout(
1594
- respondUnavailable,
1595
- failOpen ? ASK_USER_QUESTION_TIMEOUT_MS : PRE_TOOL_USE_TIMEOUT_MS,
1596
- );
1597
- const client = net.connect(socketPath, () => {
1598
- client.write(
1599
- JSON.stringify({
1600
- kind: "pre_tool_use",
1601
- id,
1602
- tool_name: typeof payload.tool_name === "string" ? payload.tool_name : "",
1603
- tool_input:
1604
- payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {},
1605
- }) + "\\n",
1606
- );
1607
- });
1608
- let buffer = "";
1609
- client.on("data", (chunk) => {
1610
- buffer += chunk.toString("utf8");
1611
- let index = buffer.indexOf("\\n");
1612
- while (index >= 0) {
1613
- const line = buffer.slice(0, index);
1614
- buffer = buffer.slice(index + 1);
1615
- try {
1616
- const verdict = JSON.parse(line);
1617
- if (verdict && verdict.id === id) {
1618
- clearTimeout(timer);
1619
- client.end();
1620
- respond(verdict.decision === "allow" ? "allow" : "deny", verdict.reason);
1621
- return;
1622
- }
1623
- } catch {
1624
- /* keep scanning */
1625
- }
1626
- index = buffer.indexOf("\\n");
1627
- }
1628
- });
1629
- client.on("error", () => {
1630
- clearTimeout(timer);
1631
- respondUnavailable();
1632
- });
1633
- client.on("close", () => {
1634
- clearTimeout(timer);
1635
- respondUnavailable();
1636
- });
1637
- }
1638
- `;
1639
- async function writeHookSettings(dir) {
1640
- const helperPath = join(dir, "hook-helper.cjs");
1641
- const settingsPath = join(dir, "settings.json");
1642
- await mkdir(dir, { recursive: true });
1643
- await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
1644
- await chmod(helperPath, 493);
1645
- const settings = {
1646
- // Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
1647
- // spawns pass `--dangerously-skip-permissions`; on a real PTY the CLI otherwise
1648
- // parks on the interactive "Yes, I accept / No, exit" dialog whose default focus
1649
- // is "No, exit" — so it cannot be auto-dismissed by an Enter nudge and the run
1650
- // stalls until the auto-nudge watchdog shuts it down. The CLI reads this key
1651
- // from the `--settings` (flagSettings) layer via its bypass-mode gate, so
1652
- // setting it here suppresses the dialog deterministically on every spawn —
1653
- // independent of the `bypassPermissionsModeAccepted` seed in credentials.ts,
1654
- // which the CLI's one-time migration consumes and which is subject to
1655
- // persistence races on the codespace user-home.
1656
- skipDangerousModePermissionPrompt: true,
1657
- // Auto-approve tool calls so the interactive (PTY) agent never stops to
1658
- // prompt the viewer. This only suppresses per-tool approval prompts — it
1659
- // does NOT relax the planning gate: in discovery/plan mode the spawn passes
1660
- // `--permission-mode plan`, which keeps the agent read-only (no edits/commits
1661
- // until it exits plan mode) regardless of this allow-list. In build mode the
1662
- // spawn already bypasses prompts via `--dangerously-skip-permissions`.
1663
- // NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
1664
- // a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
1665
- permissions: {
1666
- allow: ALLOW_RULES
1667
- },
1668
- hooks: {
1669
- PreToolUse: [
1670
- {
1671
- matcher: "ExitPlanMode",
1672
- hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 120 }]
1673
- },
1674
- {
1675
- // Observe-only: lets the session report waiting_for_input the moment
1676
- // the planning questionnaire renders. The helper fails open for this
1677
- // tool, so the questionnaire is never blocked by Conveyor.
1678
- matcher: "AskUserQuestion",
1679
- hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
1680
- },
1681
- {
1682
- // Plan-permission spawns prompt for MCP tools even when they're in
1683
- // permissions.allow — the CLI can't classify them as read-only. A
1684
- // hook allow bypasses the permission engine in every mode. Loose by
1685
- // design: the pod is the sandbox, and planning agents must call
1686
- // update_task etc. The helper answers locally (no socket).
1687
- matcher: "mcp__.*",
1688
- hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
1689
- }
1690
- ],
1691
- PostToolUse: [
1692
- {
1693
- matcher: "*",
1694
- hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
1695
- }
1696
- ]
1697
- }
1698
- };
1699
- await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
1700
- return { settingsPath, helperPath };
1701
- }
1702
-
1703
- // src/runner/plan-sync.ts
1704
- var PlanSync = class {
1705
- planFileSnapshot = /* @__PURE__ */ new Map();
1706
- lockedPlanFile = null;
1707
- workspaceDir;
1708
- connection;
1709
- constructor(workspaceDir, connection) {
1710
- this.workspaceDir = workspaceDir;
1711
- this.connection = connection;
1712
- }
1713
- updateWorkspaceDir(workspaceDir) {
1714
- this.workspaceDir = workspaceDir;
1715
- }
1716
- getPlanDirs() {
1717
- return [join2(this.workspaceDir, ".claude", "plans"), configHomePlansDir()];
1718
- }
1719
- snapshotPlanFiles() {
1720
- this.planFileSnapshot.clear();
1721
- this.lockedPlanFile = null;
1722
- for (const plansDir of this.getPlanDirs()) {
1723
- try {
1724
- for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
1725
- try {
1726
- const fullPath = join2(plansDir, file);
1727
- const stat6 = statSync(fullPath);
1728
- this.planFileSnapshot.set(fullPath, stat6.mtimeMs);
1729
- } catch {
1730
- continue;
1731
- }
1732
- }
1733
- } catch {
1734
- }
1735
- }
1736
- }
1737
- findNewestPlanFile() {
1738
- let newest = null;
1739
- for (const plansDir of this.getPlanDirs()) {
1740
- let files;
1741
- try {
1742
- files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1743
- } catch {
1744
- continue;
1745
- }
1746
- for (const file of files) {
1747
- const fullPath = join2(plansDir, file);
1748
- try {
1749
- const stat6 = statSync(fullPath);
1750
- const prevMtime = this.planFileSnapshot.get(fullPath);
1751
- const isNew = prevMtime === void 0 || stat6.mtimeMs > prevMtime;
1752
- if (isNew && (!newest || stat6.mtimeMs > newest.mtime)) {
1753
- newest = { path: fullPath, mtime: stat6.mtimeMs };
1754
- }
1755
- } catch {
1756
- continue;
1757
- }
1456
+ }
1457
+ function samePath(a, b) {
1458
+ try {
1459
+ return realpathSync(a) === realpathSync(b);
1460
+ } catch {
1461
+ return a === b;
1462
+ }
1463
+ }
1464
+ async function flushAllPendingWork(cwd, opts) {
1465
+ try {
1466
+ const primary = await flushPendingChanges(cwd, opts);
1467
+ await refreshRemoteToken(cwd, opts?.refreshToken);
1468
+ const currentBranch = await getCurrentBranch(cwd);
1469
+ const worktreesSnapshotted = await snapshotOtherWorktrees(cwd, opts?.wipMessage);
1470
+ const branchesBackedUp = await backupOtherBranches(cwd, currentBranch);
1471
+ return {
1472
+ hadWork: primary.hadWork || worktreesSnapshotted > 0 || branchesBackedUp > 0,
1473
+ branchesBackedUp,
1474
+ worktreesSnapshotted
1475
+ };
1476
+ } catch {
1477
+ return { hadWork: false, branchesBackedUp: 0, worktreesSnapshotted: 0 };
1478
+ }
1479
+ }
1480
+ async function snapshotOtherWorktrees(cwd, wipMessage) {
1481
+ let count = 0;
1482
+ for (const wt of await listWorktrees(cwd)) {
1483
+ if (samePath(wt.path, cwd) || !wt.branch) continue;
1484
+ try {
1485
+ if (!await hasUncommittedChanges(wt.path)) continue;
1486
+ const sha = await createWipSnapshot(wt.path, wipMessage ?? "WIP: conveyor-agent snapshot");
1487
+ if (sha && await tryPushRefspec(wt.path, `${sha}:refs/heads/${wipRefForBranch(wt.branch)}`, true)) {
1488
+ wipRefPushed.add(wt.path);
1489
+ count++;
1758
1490
  }
1491
+ } catch {
1759
1492
  }
1760
- return newest;
1761
1493
  }
1762
- async syncPlanFile() {
1763
- const isInitialDetection = this.lockedPlanFile === null;
1764
- const target = this.lockedPlanFile ?? this.findNewestPlanFile()?.path ?? null;
1765
- if (!target) return;
1766
- if (isInitialDetection) {
1767
- this.lockedPlanFile = target;
1768
- }
1769
- const fileName = target.split("/").pop() ?? "plan";
1770
- let content;
1494
+ return count;
1495
+ }
1496
+ async function backupOtherBranches(cwd, currentBranch) {
1497
+ let count = 0;
1498
+ for (const branch of await listLocalBranches(cwd)) {
1499
+ if (branch === currentBranch || branch.startsWith("conveyor-wip/")) continue;
1771
1500
  try {
1772
- content = readFileSync(target, "utf-8").trim();
1773
- } catch (err) {
1774
- const reason = err instanceof Error ? err.message : String(err);
1775
- this.connection.postChatMessage(
1776
- `Plan sync warning: could not read local plan file (${fileName}): ${reason}. The task plan was not updated.`
1777
- );
1778
- return;
1779
- }
1780
- if (!content) return;
1781
- const result = await this.connection.updateTaskFields({ plan: content });
1782
- if (!result.ok) {
1783
- this.connection.postChatMessage(
1784
- `Plan sync failed: the local plan file (${fileName}) was not persisted to the task (${result.error ?? "unknown error"}). Retry update_task_plan or re-run ExitPlanMode.`
1785
- );
1786
- return;
1501
+ if (await branchUnpushedCount(cwd, branch) === 0) continue;
1502
+ if (await tryPushRefspec(
1503
+ cwd,
1504
+ `refs/heads/${branch}:refs/heads/${branchBackupRef(branch)}`,
1505
+ true
1506
+ )) {
1507
+ count++;
1508
+ }
1509
+ } catch {
1787
1510
  }
1788
- const verb = isInitialDetection ? "Detected local plan file" : "Synced local plan file";
1789
- const suffix = isInitialDetection ? " and synced it to the task plan." : " to the task plan.";
1790
- this.connection.postChatMessage(`${verb} (${fileName})${suffix}`);
1791
1511
  }
1792
- };
1512
+ return count;
1513
+ }
1793
1514
 
1794
1515
  // src/setup/git-ready.ts
1795
1516
  import { access, stat } from "fs/promises";
@@ -1840,69 +1561,6 @@ async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog)
1840
1561
  // src/runner/work-preservation/index.ts
1841
1562
  import { rm as rm3 } from "fs/promises";
1842
1563
 
1843
- // src/runner/work-preservation/pg-dump.ts
1844
- import { execSync as execSync2 } from "child_process";
1845
- import { randomUUID } from "crypto";
1846
- import { tmpdir } from "os";
1847
- import path from "path";
1848
- var PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
1849
- function connectionDefaults(opts) {
1850
- return {
1851
- host: opts.host ?? "localhost",
1852
- port: opts.port ?? 5432,
1853
- db: opts.db ?? "conveyor"
1854
- };
1855
- }
1856
- function buildPgDumpCommand(opts) {
1857
- const { host, port, db } = connectionDefaults(opts);
1858
- return `pg_dump -h ${host} -p ${port} -U postgres ${db} -f ${JSON.stringify(
1859
- opts.outputPath
1860
- )} --no-owner --no-privileges`;
1861
- }
1862
- function buildPsqlRestoreCommand(opts) {
1863
- const { host, port, db } = connectionDefaults(opts);
1864
- return `psql -h ${host} -p ${port} -U postgres -d ${db} -f ${JSON.stringify(opts.dumpPath)}`;
1865
- }
1866
- function hasPostgresDep() {
1867
- return (process.env.CONVEYOR_DEPS ?? "").split(",").map((dep) => dep.trim()).includes("postgresql");
1868
- }
1869
- function execErrorDetail(err) {
1870
- const withStreams = err;
1871
- const stderr = withStreams.stderr instanceof Buffer ? withStreams.stderr.toString("utf8").trim() : typeof withStreams.stderr === "string" ? withStreams.stderr.trim() : "";
1872
- const message = withStreams.message ?? String(err);
1873
- return stderr ? `${message} \u2014 stderr: ${stderr}` : message;
1874
- }
1875
- function dumpPostgresIfPresent(exec2 = execSync2) {
1876
- if (!hasPostgresDep()) return Promise.resolve({ dumped: false });
1877
- const outputPath = path.join(tmpdir(), `conveyor-pgdump-${randomUUID()}.sql`);
1878
- try {
1879
- exec2(buildPgDumpCommand({ outputPath }), {
1880
- stdio: ["ignore", "pipe", "pipe"],
1881
- timeout: 6e4
1882
- });
1883
- return Promise.resolve({ dumped: true, path: outputPath });
1884
- } catch (err) {
1885
- process.stderr.write(`[conveyor-agent] pg_dump failed: ${execErrorDetail(err)}
1886
- `);
1887
- return Promise.resolve({ dumped: false });
1888
- }
1889
- }
1890
- function applyPgDumpIfConfigured(dumpPath, exec2 = execSync2) {
1891
- if (!hasPostgresDep()) return false;
1892
- try {
1893
- exec2(buildPsqlRestoreCommand({ dumpPath }), {
1894
- stdio: ["ignore", "pipe", "pipe"],
1895
- timeout: 12e4
1896
- });
1897
- process.stderr.write("[conveyor-agent] Restored postgres state from snapshot pg dump\n");
1898
- return true;
1899
- } catch (err) {
1900
- process.stderr.write(`[conveyor-agent] psql dump restore failed: ${execErrorDetail(err)}
1901
- `);
1902
- return false;
1903
- }
1904
- }
1905
-
1906
1564
  // src/runner/work-preservation/restore-precedence.ts
1907
1565
  function selectRestoreSource(probe) {
1908
1566
  if (probe.gcsAvailable) return "gcs";
@@ -1911,25 +1569,27 @@ function selectRestoreSource(probe) {
1911
1569
  }
1912
1570
 
1913
1571
  // src/runner/work-preservation/snapshot-tar.ts
1914
- import { execSync as execSync3 } from "child_process";
1915
- import { createReadStream, createWriteStream, mkdtempSync } from "fs";
1916
- import { copyFile, mkdtemp, rm, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1917
- import { tmpdir as tmpdir2 } from "os";
1918
- import path2 from "path";
1572
+ import { execFile as execFile2 } from "child_process";
1573
+ import { createReadStream, createWriteStream } from "fs";
1574
+ import { mkdtemp, rm, stat as stat2, writeFile } from "fs/promises";
1575
+ import { tmpdir } from "os";
1576
+ import path from "path";
1919
1577
  import { pipeline } from "stream/promises";
1578
+ import { promisify as promisify2 } from "util";
1920
1579
  import { createGzip } from "zlib";
1921
1580
  import * as tar from "tar";
1922
1581
 
1923
1582
  // src/runner/work-preservation/snapshot-artifact.ts
1924
- function planSnapshotFiles(git) {
1925
- const include = git.trackedAndUntracked();
1583
+ function planSnapshotFiles(git2) {
1584
+ const include = git2.trackedAndUntracked();
1926
1585
  const includeSet = new Set(include);
1927
- const deletions = [...new Set(git.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
1586
+ const deletions = [...new Set(git2.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
1928
1587
  return { include, deletions };
1929
1588
  }
1930
1589
 
1931
1590
  // src/runner/work-preservation/snapshot-tar.ts
1932
1591
  var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
1592
+ var LEGACY_PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
1933
1593
  var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
1934
1594
  function parseStatusPorcelainZ(raw) {
1935
1595
  const includes = [];
@@ -1952,60 +1612,55 @@ function parseStatusPorcelainZ(raw) {
1952
1612
  }
1953
1613
  return { includes, deletions };
1954
1614
  }
1955
- function realGitSurface(cwd) {
1956
- let cached = null;
1957
- const load = () => {
1958
- if (!cached) {
1959
- const out = execSync3("git status --porcelain=v1 -z -uall", {
1960
- cwd,
1961
- stdio: ["ignore", "pipe", "ignore"],
1962
- maxBuffer: STATUS_MAX_BUFFER
1963
- }).toString("utf8");
1964
- cached = parseStatusPorcelainZ(out);
1965
- }
1966
- return cached;
1967
- };
1615
+ var execFileAsync2 = promisify2(execFile2);
1616
+ var GIT_TIMEOUT_MS2 = 12e4;
1617
+ async function realGitSurface(cwd) {
1618
+ const { stdout } = await execFileAsync2("git", ["status", "--porcelain=v1", "-z", "-uall"], {
1619
+ cwd,
1620
+ timeout: GIT_TIMEOUT_MS2,
1621
+ maxBuffer: STATUS_MAX_BUFFER
1622
+ });
1623
+ const cached = parseStatusPorcelainZ(stdout.toString());
1968
1624
  return {
1969
- trackedAndUntracked: () => load().includes,
1970
- deletedSinceHead: () => load().deletions
1625
+ trackedAndUntracked: () => cached.includes,
1626
+ deletedSinceHead: () => cached.deletions
1971
1627
  };
1972
1628
  }
1973
- function gitHead(cwd) {
1629
+ async function gitHead(cwd) {
1974
1630
  try {
1975
- return execSync3("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
1631
+ const { stdout } = await execFileAsync2("git", ["rev-parse", "HEAD"], {
1632
+ cwd,
1633
+ timeout: GIT_TIMEOUT_MS2
1634
+ });
1635
+ return stdout.toString().trim() || null;
1976
1636
  } catch {
1977
1637
  return null;
1978
1638
  }
1979
1639
  }
1980
- async function buildSnapshotTar(cwd, opts) {
1981
- const staging = await mkdtemp(path2.join(tmpdir2(), "conveyor-snapshot-"));
1640
+ async function buildSnapshotTar(cwd) {
1641
+ const staging = await mkdtemp(path.join(tmpdir(), "conveyor-snapshot-"));
1982
1642
  const cleanup = async () => {
1983
1643
  await rm(staging, { recursive: true, force: true }).catch(() => {
1984
1644
  });
1985
1645
  };
1986
1646
  try {
1987
- const plan = planSnapshotFiles(realGitSurface(cwd));
1988
- const head = gitHead(cwd);
1647
+ const plan = planSnapshotFiles(await realGitSurface(cwd));
1648
+ const head = await gitHead(cwd);
1989
1649
  if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
1990
1650
  const capturedAt = Date.now();
1991
1651
  const manifest = {
1992
1652
  head,
1993
1653
  deletions: plan.deletions,
1994
- capturedAt,
1995
- pgDump: Boolean(opts?.pgDumpPath)
1654
+ capturedAt
1996
1655
  };
1997
- await writeFile2(path2.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
1656
+ await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
1998
1657
  const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
1999
- if (opts?.pgDumpPath) {
2000
- await copyFile(opts.pgDumpPath, path2.join(staging, PG_DUMP_FILENAME));
2001
- stagedEntries.push(PG_DUMP_FILENAME);
2002
- }
2003
- const rawTarPath = path2.join(staging, "snapshot.tar");
1658
+ const rawTarPath = path.join(staging, "snapshot.tar");
2004
1659
  await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
2005
1660
  if (plan.include.length > 0) {
2006
1661
  await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
2007
1662
  }
2008
- const tarPath = path2.join(staging, "snapshot.tar.gz");
1663
+ const tarPath = path.join(staging, "snapshot.tar.gz");
2009
1664
  await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
2010
1665
  await rm(rawTarPath, { force: true });
2011
1666
  const { size } = await stat2(tarPath);
@@ -2023,20 +1678,13 @@ async function buildSnapshotTar(cwd, opts) {
2023
1678
  }
2024
1679
  }
2025
1680
  function isWithinWorkspace(cwd, rel) {
2026
- if (!rel || path2.isAbsolute(rel)) return false;
2027
- const root = path2.resolve(cwd);
2028
- const abs = path2.resolve(root, rel);
2029
- return abs !== root && abs.startsWith(root + path2.sep);
1681
+ if (!rel || path.isAbsolute(rel)) return false;
1682
+ const root = path.resolve(cwd);
1683
+ const abs = path.resolve(root, rel);
1684
+ return abs !== root && abs.startsWith(root + path.sep);
2030
1685
  }
2031
1686
  async function readSnapshotArchive(tarPath) {
2032
1687
  let manifestRaw = null;
2033
- let pgDumpDir = null;
2034
- let pgDumpPath;
2035
- let pgDumpWritten = Promise.resolve();
2036
- const cleanup = async () => {
2037
- if (pgDumpDir) await rm(pgDumpDir, { recursive: true, force: true }).catch(() => {
2038
- });
2039
- };
2040
1688
  try {
2041
1689
  await tar.list({
2042
1690
  file: tarPath,
@@ -2047,86 +1695,59 @@ async function readSnapshotArchive(tarPath) {
2047
1695
  entry.on("end", () => {
2048
1696
  manifestRaw = Buffer.concat(chunks);
2049
1697
  });
2050
- } else if (entry.path === PG_DUMP_FILENAME) {
2051
- pgDumpDir = mkdtempSync(path2.join(tmpdir2(), "conveyor-pgdump-restore-"));
2052
- pgDumpPath = path2.join(pgDumpDir, "dump.sql");
2053
- const dest = createWriteStream(pgDumpPath);
2054
- pgDumpWritten = new Promise((resolve, reject) => {
2055
- dest.on("finish", () => resolve());
2056
- dest.on("error", reject);
2057
- });
2058
- entry.pipe(dest);
2059
1698
  }
2060
1699
  }
2061
1700
  });
2062
- await pgDumpWritten;
2063
1701
  } catch {
2064
- await cleanup();
2065
- return null;
2066
- }
2067
- if (!manifestRaw) {
2068
- await cleanup();
2069
1702
  return null;
2070
1703
  }
1704
+ if (!manifestRaw) return null;
2071
1705
  try {
2072
1706
  const parsed = JSON.parse(manifestRaw.toString("utf8"));
2073
- if (typeof parsed.head !== "string" || !parsed.head) {
2074
- await cleanup();
2075
- return null;
2076
- }
1707
+ if (typeof parsed.head !== "string" || !parsed.head) return null;
2077
1708
  return {
2078
- manifest: {
2079
- head: parsed.head,
2080
- deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
2081
- capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0,
2082
- pgDump: parsed.pgDump === true
2083
- },
2084
- ...pgDumpPath ? { pgDumpPath } : {},
2085
- cleanup
1709
+ head: parsed.head,
1710
+ deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
1711
+ capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0
2086
1712
  };
2087
1713
  } catch {
2088
- await cleanup();
2089
1714
  return null;
2090
1715
  }
2091
1716
  }
2092
- async function extractSnapshotTar(cwd, tarPath, opts) {
2093
- const read = await readSnapshotArchive(tarPath);
2094
- if (!read) return { status: "invalid" };
2095
- try {
2096
- const { manifest, pgDumpPath } = read;
2097
- const currentHead = gitHead(cwd);
2098
- if (manifest.head !== currentHead) {
2099
- return { status: "stale-head", snapshotHead: manifest.head, currentHead };
2100
- }
2101
- let filesExtracted = 0;
2102
- await tar.extract({
2103
- file: tarPath,
2104
- cwd,
2105
- filter: (entryPath) => {
2106
- if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === PG_DUMP_FILENAME) return false;
2107
- filesExtracted++;
2108
- return true;
1717
+ async function extractSnapshotTar(cwd, tarPath) {
1718
+ const manifest = await readSnapshotArchive(tarPath);
1719
+ if (!manifest) return { status: "invalid" };
1720
+ const currentHead = await gitHead(cwd);
1721
+ if (manifest.head !== currentHead) {
1722
+ return { status: "stale-head", snapshotHead: manifest.head, currentHead };
1723
+ }
1724
+ let filesExtracted = 0;
1725
+ await tar.extract({
1726
+ file: tarPath,
1727
+ cwd,
1728
+ filter: (entryPath) => {
1729
+ if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === LEGACY_PG_DUMP_FILENAME) {
1730
+ return false;
2109
1731
  }
2110
- });
2111
- let deletionsApplied = 0;
2112
- for (const rel of manifest.deletions) {
2113
- if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
2114
- await rm(path2.resolve(cwd, rel), { recursive: true, force: true });
2115
- deletionsApplied++;
2116
- }
2117
- const pgDumpApplied = manifest.pgDump && pgDumpPath ? applyPgDumpIfConfigured(pgDumpPath, opts?.pgExec) : false;
2118
- return { status: "extracted", filesExtracted, deletionsApplied, pgDumpApplied };
2119
- } finally {
2120
- await read.cleanup();
1732
+ filesExtracted++;
1733
+ return true;
1734
+ }
1735
+ });
1736
+ let deletionsApplied = 0;
1737
+ for (const rel of manifest.deletions) {
1738
+ if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
1739
+ await rm(path.resolve(cwd, rel), { recursive: true, force: true });
1740
+ deletionsApplied++;
2121
1741
  }
1742
+ return { status: "extracted", filesExtracted, deletionsApplied };
2122
1743
  }
2123
1744
 
2124
1745
  // src/runner/work-preservation/snapshot-transfer.ts
2125
- import { randomUUID as randomUUID2 } from "crypto";
1746
+ import { randomUUID } from "crypto";
2126
1747
  import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
2127
1748
  import { rm as rm2 } from "fs/promises";
2128
- import { tmpdir as tmpdir3 } from "os";
2129
- import path3 from "path";
1749
+ import { tmpdir as tmpdir2 } from "os";
1750
+ import path2 from "path";
2130
1751
  import { Readable } from "stream";
2131
1752
  import { pipeline as pipeline2 } from "stream/promises";
2132
1753
  var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
@@ -2151,7 +1772,7 @@ async function putSnapshotToGcs(url, tarResult) {
2151
1772
  }
2152
1773
  }
2153
1774
  async function downloadSnapshotToTemp(url) {
2154
- const tmpTar = path3.join(tmpdir3(), `conveyor-snapshot-restore-${randomUUID2()}.tar.gz`);
1775
+ const tmpTar = path2.join(tmpdir2(), `conveyor-snapshot-restore-${randomUUID()}.tar.gz`);
2155
1776
  try {
2156
1777
  const res = await fetch(url, {
2157
1778
  method: "GET",
@@ -2178,13 +1799,12 @@ var EMPTY_GCS_RESULT = {
2178
1799
  uploaded: false,
2179
1800
  fileCount: 0,
2180
1801
  deletionCount: 0,
2181
- sizeBytes: 0,
2182
- pgDumped: false
1802
+ sizeBytes: 0
2183
1803
  };
2184
1804
  async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
2185
1805
  let tarResult;
2186
1806
  try {
2187
- tarResult = await buildSnapshotTar(cwd, { pgDumpPath: opts?.pgDumpPath });
1807
+ tarResult = await buildSnapshotTar(cwd);
2188
1808
  } catch {
2189
1809
  return EMPTY_GCS_RESULT;
2190
1810
  }
@@ -2194,8 +1814,7 @@ async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
2194
1814
  uploaded,
2195
1815
  fileCount: tarResult.fileCount,
2196
1816
  deletionCount: tarResult.deletionCount,
2197
- sizeBytes: tarResult.sizeBytes,
2198
- pgDumped: Boolean(opts?.pgDumpPath)
1817
+ sizeBytes: tarResult.sizeBytes
2199
1818
  };
2200
1819
  } finally {
2201
1820
  await tarResult.cleanup();
@@ -2217,7 +1836,6 @@ async function uploadSnapshotToGcs(cwd, uploadUrl, opts) {
2217
1836
  }
2218
1837
  async function captureSnapshot(ctx) {
2219
1838
  const gcs = await uploadSnapshotToGcs(ctx.cwd, ctx.snapshotUploadUrl, {
2220
- pgDumpPath: ctx.pgDumpPath,
2221
1839
  forceUpload: ctx.forceUpload
2222
1840
  });
2223
1841
  const wipResult = await flushPendingChanges(ctx.cwd, {
@@ -2229,8 +1847,7 @@ async function captureSnapshot(ctx) {
2229
1847
  deletionCount: gcs.deletionCount,
2230
1848
  sizeBytes: gcs.sizeBytes,
2231
1849
  gcsUploaded: gcs.uploaded,
2232
- wipPushed: wipResult.committed || wipResult.pushed,
2233
- pgDumped: gcs.pgDumped
1850
+ wipPushed: wipResult.committed || wipResult.pushed
2234
1851
  };
2235
1852
  }
2236
1853
  function startPeriodic(intervalMs, ctx) {
@@ -2255,21 +1872,13 @@ async function finalizeForSleep(ctx) {
2255
1872
  stop();
2256
1873
  if (inFlightCapture) await inFlightCapture.catch(() => {
2257
1874
  });
2258
- const dump = await dumpPostgresIfPresent();
2259
- try {
2260
- const artifact = await captureSnapshot({
2261
- ...ctx,
2262
- pgDumpPath: dump.path,
2263
- wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation sleep finalize snapshot",
2264
- // Always stamp snapshotAt on sleep — even an empty workspace must confirm
2265
- // the finalize so teardown doesn't wait out the reconciler's cap.
2266
- forceUpload: true
2267
- });
2268
- return { ...artifact, pgDumped: dump.dumped };
2269
- } finally {
2270
- if (dump.path) await rm3(dump.path, { force: true }).catch(() => {
2271
- });
2272
- }
1875
+ return captureSnapshot({
1876
+ ...ctx,
1877
+ wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation sleep finalize snapshot",
1878
+ // Always stamp snapshotAt on sleep — even an empty workspace must confirm
1879
+ // the finalize so teardown doesn't wait out the reconciler's cap.
1880
+ forceUpload: true
1881
+ });
2273
1882
  }
2274
1883
  async function restoreOnBoot(bundle, cwd) {
2275
1884
  let gcsAvailable = false;
@@ -2291,7 +1900,7 @@ async function restoreOnBoot(bundle, cwd) {
2291
1900
  }
2292
1901
  }
2293
1902
  }
2294
- const wipRestoreResult = gcsAvailable ? "none" : restoreWipSnapshot(cwd, bundle.gitPlan.branch);
1903
+ const wipRestoreResult = gcsAvailable ? "none" : await restoreWipSnapshot(cwd, bundle.gitPlan.branch);
2295
1904
  const source = selectRestoreSource({ gcsAvailable, wipRestoreResult });
2296
1905
  return source === "gcs" ? { source, fileCount: gcsFileCount } : { source };
2297
1906
  }
@@ -2308,7 +1917,9 @@ var AgentHeartbeatSchema = z.object({
2308
1917
  sessionId: z.string().optional(),
2309
1918
  timestamp: z.string(),
2310
1919
  status: z.enum(["active", "idle", "building"]),
2311
- currentAction: z.string().optional()
1920
+ currentAction: z.string().optional(),
1921
+ /** Sender-observed main event-loop lag (ms) — see AgentHeartbeat.loopLagMs. */
1922
+ loopLagMs: z.number().nonnegative().optional()
2312
1923
  });
2313
1924
  var CreatePRInputSchema = z.object({
2314
1925
  title: z.string().min(1),
@@ -2590,6 +2201,10 @@ var ListActivePtySessionsRequestSchema = z.object({
2590
2201
  var SendSoftStopRequestSchema = z.object({
2591
2202
  taskId: z.string()
2592
2203
  });
2204
+ var StopTaskSessionRequestSchema = z.object({
2205
+ taskId: z.string(),
2206
+ sessionId: z.string()
2207
+ });
2593
2208
  var FlushTaskQueueRequestSchema = z.object({
2594
2209
  taskId: z.string(),
2595
2210
  softStop: z.boolean().optional()
@@ -2669,6 +2284,29 @@ var PtyResizeRequestSchema = z.object({
2669
2284
  var PtyAttachRequestSchema = z.object({
2670
2285
  sessionId: z.string()
2671
2286
  });
2287
+ var PtyChatEventPayloadSchema = z.discriminatedUnion("kind", [
2288
+ z.object({
2289
+ kind: z.literal("init"),
2290
+ model: z.string().max(200),
2291
+ claudeSessionId: z.string().max(100).optional()
2292
+ }),
2293
+ z.object({ kind: z.literal("user_text"), text: z.string().max(16384) }),
2294
+ z.object({ kind: z.literal("assistant_text"), text: z.string().max(16384) }),
2295
+ z.object({
2296
+ kind: z.literal("tool_use"),
2297
+ name: z.string().max(200),
2298
+ // Compact preview: JSON.stringify(input) truncated agent-side.
2299
+ input: z.string().max(2e3)
2300
+ }),
2301
+ z.object({ kind: z.literal("turn_end") })
2302
+ ]);
2303
+ var PtyChatEventRequestSchema = z.object({
2304
+ sessionId: z.string(),
2305
+ event: PtyChatEventPayloadSchema
2306
+ });
2307
+ var PtyChatAttachRequestSchema = z.object({
2308
+ sessionId: z.string()
2309
+ });
2672
2310
  var CreatePRResponseSchema = z.object({
2673
2311
  prNumber: z.number().int().positive(),
2674
2312
  prUrl: z.string().url()
@@ -2809,6 +2447,13 @@ var ListMyLiveSessionsRequestSchema = z2.object({
2809
2447
  var StartAdhocSessionRequestSchema = z2.object({
2810
2448
  projectId: z2.string(),
2811
2449
  label: z2.string().max(200).optional(),
2450
+ /**
2451
+ * Session role. Constrained: other task-less modes fall through to the pm
2452
+ * runner in the pod entrypoint, and "review" would crash without a task.
2453
+ */
2454
+ mode: z2.enum(["adhoc", "pm"]).optional(),
2455
+ /** Base branch to check out (defaults to the project's dev branch). */
2456
+ branch: z2.string().max(300).optional(),
2812
2457
  requestingUserId: z2.string().optional()
2813
2458
  });
2814
2459
  var StopAdhocSessionRequestSchema = z2.object({
@@ -3333,8 +2978,8 @@ var ClaudeCodeHarness = class {
3333
2978
 
3334
2979
  // src/harness/pty/session.ts
3335
2980
  import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4 } from "fs/promises";
3336
- import { tmpdir as tmpdir4 } from "os";
3337
- import { join as join4, dirname } from "path";
2981
+ import { tmpdir as tmpdir3 } from "os";
2982
+ import { join as join3, dirname } from "path";
3338
2983
 
3339
2984
  // src/harness/pty/event-queue.ts
3340
2985
  var AsyncEventQueue = class {
@@ -3615,12 +3260,14 @@ function mapTranscriptRecord(raw) {
3615
3260
  // src/harness/pty/jsonl-tailer.ts
3616
3261
  var POLL_INTERVAL_MS = 25;
3617
3262
  var JsonlTailer = class {
3618
- constructor(path4, onEvent) {
3263
+ constructor(path4, onEvent, onRawRecord) {
3619
3264
  this.path = path4;
3620
3265
  this.onEvent = onEvent;
3266
+ this.onRawRecord = onRawRecord;
3621
3267
  }
3622
3268
  path;
3623
3269
  onEvent;
3270
+ onRawRecord;
3624
3271
  offset = 0;
3625
3272
  buffer = "";
3626
3273
  timer = null;
@@ -3692,11 +3339,109 @@ var JsonlTailer = class {
3692
3339
  } catch {
3693
3340
  return;
3694
3341
  }
3342
+ this.onRawRecord?.(parsed);
3695
3343
  const event = mapTranscriptRecord(parsed);
3696
3344
  if (event) this.onEvent(event);
3697
3345
  }
3698
3346
  };
3699
3347
 
3348
+ // src/harness/pty/chat-record-mapper.ts
3349
+ var TEXT_MAX = 16e3;
3350
+ var TOOL_INPUT_MAX = 1900;
3351
+ function isRecord2(value) {
3352
+ return typeof value === "object" && value !== null;
3353
+ }
3354
+ function isUnknownArray2(value) {
3355
+ return Array.isArray(value);
3356
+ }
3357
+ function stringField2(record, ...keys) {
3358
+ for (const key of keys) {
3359
+ const value = record[key];
3360
+ if (typeof value === "string") return value;
3361
+ }
3362
+ return void 0;
3363
+ }
3364
+ function truncate(text, max) {
3365
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3366
+ }
3367
+ function isCommandWrapperText(text) {
3368
+ const trimmed = text.trimStart();
3369
+ return trimmed.startsWith("<command-name>") || trimmed.startsWith("<local-command-");
3370
+ }
3371
+ function mapSystem2(record) {
3372
+ if (record.subtype === "init") {
3373
+ const event = {
3374
+ kind: "init",
3375
+ model: stringField2(record, "model") ?? ""
3376
+ };
3377
+ const sessionId = stringField2(record, "session_id", "sessionId");
3378
+ if (sessionId !== void 0) event.claudeSessionId = sessionId;
3379
+ return [event];
3380
+ }
3381
+ if (record.subtype === "turn_duration") return [{ kind: "turn_end" }];
3382
+ return [];
3383
+ }
3384
+ function mapAssistant2(record) {
3385
+ const message = record.message;
3386
+ if (!isRecord2(message)) return [];
3387
+ const content = isUnknownArray2(message.content) ? message.content : [];
3388
+ const events = [];
3389
+ for (const raw of content) {
3390
+ if (!isRecord2(raw)) continue;
3391
+ if (raw.type === "text") {
3392
+ const text = stringField2(raw, "text");
3393
+ if (text && text.length > 0) {
3394
+ events.push({ kind: "assistant_text", text: truncate(text, TEXT_MAX) });
3395
+ }
3396
+ } else if (raw.type === "tool_use") {
3397
+ const name = stringField2(raw, "name");
3398
+ if (name) {
3399
+ const input = "input" in raw ? raw.input : void 0;
3400
+ events.push({
3401
+ kind: "tool_use",
3402
+ name: truncate(name, 200),
3403
+ input: JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX)
3404
+ });
3405
+ }
3406
+ }
3407
+ }
3408
+ return events;
3409
+ }
3410
+ function mapUser(record) {
3411
+ const message = record.message;
3412
+ if (!isRecord2(message)) return [];
3413
+ const content = message.content;
3414
+ let text;
3415
+ if (typeof content === "string") {
3416
+ text = content;
3417
+ } else if (isUnknownArray2(content)) {
3418
+ const hasToolResult = content.some((b) => isRecord2(b) && b.type === "tool_result");
3419
+ if (hasToolResult) return [];
3420
+ text = content.filter((b) => isRecord2(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
3421
+ } else {
3422
+ return [];
3423
+ }
3424
+ const trimmed = text.trim();
3425
+ if (trimmed.length === 0 || isCommandWrapperText(trimmed)) return [];
3426
+ return [{ kind: "user_text", text: truncate(trimmed, TEXT_MAX) }];
3427
+ }
3428
+ function mapChatRecords(raw) {
3429
+ if (!isRecord2(raw)) return [];
3430
+ if (raw.isSidechain === true || raw.isMeta === true) return [];
3431
+ switch (raw.type) {
3432
+ case "system":
3433
+ return mapSystem2(raw);
3434
+ case "assistant":
3435
+ return mapAssistant2(raw);
3436
+ case "user":
3437
+ return mapUser(raw);
3438
+ case "result":
3439
+ return [{ kind: "turn_end" }];
3440
+ default:
3441
+ return [];
3442
+ }
3443
+ }
3444
+
3700
3445
  // src/harness/pty/spawn-args.ts
3701
3446
  function resolveClaudeBinary() {
3702
3447
  return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
@@ -3745,26 +3490,268 @@ function cleanTerminalOutput(raw, maxChars = 1200) {
3745
3490
  else if (code < 32 || code === 127) continue;
3746
3491
  else out += ch;
3747
3492
  }
3748
- const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
3749
- const text = lines.join("\n").trim();
3750
- return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
3751
- }
3752
- function isMissingBinaryFailure(tail) {
3753
- return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
3493
+ const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
3494
+ const text = lines.join("\n").trim();
3495
+ return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
3496
+ }
3497
+ function isMissingBinaryFailure(tail) {
3498
+ return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
3499
+ }
3500
+ function buildExitErrors(exitCode, rawOutput, binary) {
3501
+ const errors = [`claude exited (code ${exitCode}) without a result`];
3502
+ const tail = cleanTerminalOutput(rawOutput);
3503
+ if (isMissingBinaryFailure(tail)) {
3504
+ errors.push(
3505
+ `The \`${binary}\` CLI could not be started \u2014 it is not installed or not on PATH. Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) or set CONVEYOR_CLAUDE_BIN to its absolute path.`
3506
+ );
3507
+ }
3508
+ if (tail) {
3509
+ errors.push(`Last terminal output before exit:
3510
+ ${tail}`);
3511
+ }
3512
+ return errors;
3513
+ }
3514
+
3515
+ // src/harness/pty/settings.ts
3516
+ import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
3517
+ import { homedir } from "os";
3518
+ import { join } from "path";
3519
+ function claudeConfigHome() {
3520
+ return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
3521
+ }
3522
+ function projectSlug(cwd) {
3523
+ return cwd.replace(/\//g, "-");
3524
+ }
3525
+ function sessionTranscriptPath(cwd, sessionId) {
3526
+ return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
3527
+ }
3528
+ var ALLOW_RULES = [
3529
+ "Bash",
3530
+ "BashOutput",
3531
+ "Edit",
3532
+ "Write",
3533
+ "Read",
3534
+ "Glob",
3535
+ "Grep",
3536
+ "WebFetch",
3537
+ "WebSearch",
3538
+ "NotebookEdit",
3539
+ "Task",
3540
+ "TodoWrite",
3541
+ "ToolSearch",
3542
+ "KillShell",
3543
+ "SlashCommand",
3544
+ "Skill",
3545
+ "mcp__conveyor__*"
3546
+ ];
3547
+ var HOOK_HELPER_SOURCE = `"use strict";
3548
+ const net = require("node:net");
3549
+ const crypto = require("node:crypto");
3550
+
3551
+ const PRE_TOOL_USE_TIMEOUT_MS = 110000;
3552
+ const ASK_USER_QUESTION_TIMEOUT_MS = 5000;
3553
+
3554
+ let raw = "";
3555
+ process.stdin.setEncoding("utf8");
3556
+ process.stdin.on("data", (chunk) => {
3557
+ raw += chunk;
3558
+ });
3559
+ process.stdin.on("end", () => {
3560
+ let payload = {};
3561
+ try {
3562
+ const parsed = JSON.parse(raw);
3563
+ if (parsed && typeof parsed === "object") payload = parsed;
3564
+ } catch {
3565
+ payload = {};
3566
+ }
3567
+ if (payload.hook_event_name === "PreToolUse") {
3568
+ preToolUse(payload);
3569
+ } else {
3570
+ relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
3571
+ }
3572
+ });
3573
+
3574
+ function relayProgress(toolName) {
3575
+ const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
3576
+ let done = false;
3577
+ const finish = () => {
3578
+ if (done) return;
3579
+ done = true;
3580
+ process.stdout.write(JSON.stringify({ continue: true }));
3581
+ process.exit(0);
3582
+ };
3583
+ const fallback = setTimeout(finish, 250);
3584
+ if (!socketPath) {
3585
+ clearTimeout(fallback);
3586
+ finish();
3587
+ return;
3588
+ }
3589
+ const client = net.connect(socketPath, () => {
3590
+ // PostToolUse does not supply tool duration, so elapsed_time_seconds is
3591
+ // omitted rather than reported as a misleading 0 (the field is optional).
3592
+ const line = JSON.stringify({ tool_name: toolName }) + "\\n";
3593
+ client.write(line, () => {
3594
+ client.end();
3595
+ });
3596
+ });
3597
+ client.on("close", () => {
3598
+ clearTimeout(fallback);
3599
+ finish();
3600
+ });
3601
+ client.on("error", () => {
3602
+ clearTimeout(fallback);
3603
+ finish();
3604
+ });
3754
3605
  }
3755
- function buildExitErrors(exitCode, rawOutput, binary) {
3756
- const errors = [`claude exited (code ${exitCode}) without a result`];
3757
- const tail = cleanTerminalOutput(rawOutput);
3758
- if (isMissingBinaryFailure(tail)) {
3759
- errors.push(
3760
- `The \`${binary}\` CLI could not be started \u2014 it is not installed or not on PATH. Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) or set CONVEYOR_CLAUDE_BIN to its absolute path.`
3606
+
3607
+ function preToolUse(payload) {
3608
+ const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
3609
+ let done = false;
3610
+ const respond = (decision, reason) => {
3611
+ if (done) return;
3612
+ done = true;
3613
+ process.stdout.write(
3614
+ JSON.stringify({
3615
+ hookSpecificOutput: {
3616
+ hookEventName: "PreToolUse",
3617
+ permissionDecision: decision,
3618
+ ...(reason ? { permissionDecisionReason: reason } : {}),
3619
+ },
3620
+ }),
3761
3621
  );
3622
+ process.exit(0);
3623
+ };
3624
+ // MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
3625
+ // Plan mode prompts for them despite permissions.allow (the CLI can't
3626
+ // classify MCP tools as read-only), and the pod is the sandbox.
3627
+ if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
3628
+ respond("allow");
3629
+ return;
3762
3630
  }
3763
- if (tail) {
3764
- errors.push(`Last terminal output before exit:
3765
- ${tail}`);
3631
+ // AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
3632
+ // missing/slow socket never denies the questionnaire. Everything else
3633
+ // (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
3634
+ const failOpen = payload.tool_name === "AskUserQuestion";
3635
+ const respondUnavailable = () =>
3636
+ failOpen
3637
+ ? respond("allow")
3638
+ : respond(
3639
+ "deny",
3640
+ "Conveyor validation is unavailable right now. Wait a moment and call the tool again.",
3641
+ );
3642
+ if (!socketPath) {
3643
+ respondUnavailable();
3644
+ return;
3766
3645
  }
3767
- return errors;
3646
+ const id = crypto.randomBytes(8).toString("hex");
3647
+ const timer = setTimeout(
3648
+ respondUnavailable,
3649
+ failOpen ? ASK_USER_QUESTION_TIMEOUT_MS : PRE_TOOL_USE_TIMEOUT_MS,
3650
+ );
3651
+ const client = net.connect(socketPath, () => {
3652
+ client.write(
3653
+ JSON.stringify({
3654
+ kind: "pre_tool_use",
3655
+ id,
3656
+ tool_name: typeof payload.tool_name === "string" ? payload.tool_name : "",
3657
+ tool_input:
3658
+ payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {},
3659
+ }) + "\\n",
3660
+ );
3661
+ });
3662
+ let buffer = "";
3663
+ client.on("data", (chunk) => {
3664
+ buffer += chunk.toString("utf8");
3665
+ let index = buffer.indexOf("\\n");
3666
+ while (index >= 0) {
3667
+ const line = buffer.slice(0, index);
3668
+ buffer = buffer.slice(index + 1);
3669
+ try {
3670
+ const verdict = JSON.parse(line);
3671
+ if (verdict && verdict.id === id) {
3672
+ clearTimeout(timer);
3673
+ client.end();
3674
+ respond(verdict.decision === "allow" ? "allow" : "deny", verdict.reason);
3675
+ return;
3676
+ }
3677
+ } catch {
3678
+ /* keep scanning */
3679
+ }
3680
+ index = buffer.indexOf("\\n");
3681
+ }
3682
+ });
3683
+ client.on("error", () => {
3684
+ clearTimeout(timer);
3685
+ respondUnavailable();
3686
+ });
3687
+ client.on("close", () => {
3688
+ clearTimeout(timer);
3689
+ respondUnavailable();
3690
+ });
3691
+ }
3692
+ `;
3693
+ async function writeHookSettings(dir) {
3694
+ const helperPath = join(dir, "hook-helper.cjs");
3695
+ const settingsPath = join(dir, "settings.json");
3696
+ await mkdir(dir, { recursive: true });
3697
+ await writeFile2(helperPath, HOOK_HELPER_SOURCE, "utf8");
3698
+ await chmod(helperPath, 493);
3699
+ const settings = {
3700
+ // Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
3701
+ // spawns pass `--dangerously-skip-permissions`; on a real PTY the CLI otherwise
3702
+ // parks on the interactive "Yes, I accept / No, exit" dialog whose default focus
3703
+ // is "No, exit" — so it cannot be auto-dismissed by an Enter nudge and the run
3704
+ // stalls until the auto-nudge watchdog shuts it down. The CLI reads this key
3705
+ // from the `--settings` (flagSettings) layer via its bypass-mode gate, so
3706
+ // setting it here suppresses the dialog deterministically on every spawn —
3707
+ // independent of the `bypassPermissionsModeAccepted` seed in credentials.ts,
3708
+ // which the CLI's one-time migration consumes and which is subject to
3709
+ // persistence races on the codespace user-home.
3710
+ skipDangerousModePermissionPrompt: true,
3711
+ // Auto-approve tool calls so the interactive (PTY) agent never stops to
3712
+ // prompt the viewer. This only suppresses per-tool approval prompts — it
3713
+ // does NOT relax the planning gate: in discovery/plan mode the spawn passes
3714
+ // `--permission-mode plan`, which keeps the agent read-only (no edits/commits
3715
+ // until it exits plan mode) regardless of this allow-list. In build mode the
3716
+ // spawn already bypasses prompts via `--dangerously-skip-permissions`.
3717
+ // NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
3718
+ // a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
3719
+ permissions: {
3720
+ allow: ALLOW_RULES
3721
+ },
3722
+ hooks: {
3723
+ PreToolUse: [
3724
+ {
3725
+ matcher: "ExitPlanMode",
3726
+ hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 120 }]
3727
+ },
3728
+ {
3729
+ // Observe-only: lets the session report waiting_for_input the moment
3730
+ // the planning questionnaire renders. The helper fails open for this
3731
+ // tool, so the questionnaire is never blocked by Conveyor.
3732
+ matcher: "AskUserQuestion",
3733
+ hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
3734
+ },
3735
+ {
3736
+ // Plan-permission spawns prompt for MCP tools even when they're in
3737
+ // permissions.allow — the CLI can't classify them as read-only. A
3738
+ // hook allow bypasses the permission engine in every mode. Loose by
3739
+ // design: the pod is the sandbox, and planning agents must call
3740
+ // update_task etc. The helper answers locally (no socket).
3741
+ matcher: "mcp__.*",
3742
+ hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
3743
+ }
3744
+ ],
3745
+ PostToolUse: [
3746
+ {
3747
+ matcher: "*",
3748
+ hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
3749
+ }
3750
+ ]
3751
+ }
3752
+ };
3753
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf8");
3754
+ return { settingsPath, helperPath };
3768
3755
  }
3769
3756
 
3770
3757
  // src/harness/pty/output-coalescer.ts
@@ -3822,7 +3809,7 @@ var PtyOutputCoalescer = class {
3822
3809
  // src/harness/pty/tool-server.ts
3823
3810
  import { createServer as createServer2 } from "http";
3824
3811
  import { writeFile as writeFile3 } from "fs/promises";
3825
- import { join as join3 } from "path";
3812
+ import { join as join2 } from "path";
3826
3813
  import { randomBytes } from "crypto";
3827
3814
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3828
3815
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -3990,7 +3977,7 @@ async function startToolServers(mcpServers, tempDir) {
3990
3977
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
3991
3978
  }
3992
3979
  if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
3993
- const mcpConfigPath = join3(tempDir, "mcp-config.json");
3980
+ const mcpConfigPath = join2(tempDir, "mcp-config.json");
3994
3981
  await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
3995
3982
  return { servers, mcpConfigPath };
3996
3983
  }
@@ -4029,14 +4016,14 @@ function turnOptionsFrom(options) {
4029
4016
  abortController: options.abortController
4030
4017
  };
4031
4018
  }
4032
- function isRecord2(value) {
4019
+ function isRecord3(value) {
4033
4020
  return typeof value === "object" && value !== null;
4034
4021
  }
4035
4022
  function extractSpawn(mod) {
4036
- if (!isRecord2(mod)) return null;
4023
+ if (!isRecord3(mod)) return null;
4037
4024
  if (typeof mod.spawn === "function") return mod.spawn;
4038
4025
  const def = mod.default;
4039
- if (isRecord2(def) && typeof def.spawn === "function") return def.spawn;
4026
+ if (isRecord3(def) && typeof def.spawn === "function") return def.spawn;
4040
4027
  return null;
4041
4028
  }
4042
4029
  async function loadPtySpawn() {
@@ -4058,6 +4045,16 @@ function inheritedEnv(socketPath) {
4058
4045
  function buildPromptBytes(text) {
4059
4046
  return `\x1B[200~${text}\x1B[201~`;
4060
4047
  }
4048
+ function renderPromptContentText(content) {
4049
+ return content.map((block) => {
4050
+ const b = block;
4051
+ if (b?.type === "text" && typeof b.text === "string") return b.text;
4052
+ if (b?.type === "image") {
4053
+ return `[Image attachment \u2014 use list_task_files / get_attachment to view]`;
4054
+ }
4055
+ return JSON.stringify(block);
4056
+ }).join("\n\n");
4057
+ }
4061
4058
  function sleep3(ms) {
4062
4059
  return new Promise((resolve) => {
4063
4060
  setTimeout(resolve, ms);
@@ -4074,9 +4071,9 @@ function parseUserQuestions(input) {
4074
4071
  if (!Array.isArray(input.questions)) return [];
4075
4072
  const questions = [];
4076
4073
  for (const entry of input.questions) {
4077
- if (!isRecord2(entry)) continue;
4074
+ if (!isRecord3(entry)) continue;
4078
4075
  if (typeof entry.question !== "string") continue;
4079
- const options = Array.isArray(entry.options) ? entry.options.filter(isRecord2).filter((o) => typeof o.label === "string").map((o) => ({
4076
+ const options = Array.isArray(entry.options) ? entry.options.filter(isRecord3).filter((o) => typeof o.label === "string").map((o) => ({
4080
4077
  label: o.label,
4081
4078
  description: typeof o.description === "string" ? o.description : ""
4082
4079
  })) : [];
@@ -4324,8 +4321,8 @@ var PtySession = class {
4324
4321
  return;
4325
4322
  }
4326
4323
  try {
4327
- this.tempDir = await mkdtemp2(join4(tmpdir4(), "conveyor-pty-"));
4328
- const socketPath = join4(this.tempDir, "hook.sock");
4324
+ this.tempDir = await mkdtemp2(join3(tmpdir3(), "conveyor-pty-"));
4325
+ const socketPath = join3(this.tempDir, "hook.sock");
4329
4326
  this.socket = new HookSocketServer(
4330
4327
  socketPath,
4331
4328
  (progress) => this.handleProgress(progress),
@@ -4337,7 +4334,14 @@ var PtySession = class {
4337
4334
  const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
4338
4335
  await mkdir2(dirname(transcriptPath), { recursive: true });
4339
4336
  const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
4340
- this.tailer = new JsonlTailer(transcriptPath, (event) => this.handleTranscriptEvent(event));
4337
+ const sendChat = this.bridge?.sendChatEvent?.bind(this.bridge);
4338
+ this.tailer = new JsonlTailer(
4339
+ transcriptPath,
4340
+ (event) => this.handleTranscriptEvent(event),
4341
+ sendChat ? (raw) => {
4342
+ for (const chatEvent of mapChatRecords(raw)) sendChat(chatEvent);
4343
+ } : void 0
4344
+ );
4341
4345
  this.tailer.start(startOffset);
4342
4346
  await this.spawn(settingsPath, socketPath);
4343
4347
  if (signal) {
@@ -4497,7 +4501,7 @@ var PtySession = class {
4497
4501
  }
4498
4502
  for await (const message of this.turnPrompt) {
4499
4503
  const content = message.message.content;
4500
- const text = typeof content === "string" ? content : JSON.stringify(content);
4504
+ const text = typeof content === "string" ? content : renderPromptContentText(content);
4501
4505
  await this.deliverPrompt(text);
4502
4506
  }
4503
4507
  }
@@ -4661,11 +4665,11 @@ var PtySession = class {
4661
4665
  // src/harness/pty/credentials.ts
4662
4666
  import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm5, writeFile as writeFile4 } from "fs/promises";
4663
4667
  import { homedir as homedir2 } from "os";
4664
- import { join as join5 } from "path";
4668
+ import { join as join4 } from "path";
4665
4669
  var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
4666
4670
  var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
4667
4671
  function claudeCredentialsPath() {
4668
- return join5(claudeConfigHome(), ".credentials.json");
4672
+ return join4(claudeConfigHome(), ".credentials.json");
4669
4673
  }
4670
4674
  function isConveyorCloudEnv(env = process.env) {
4671
4675
  return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
@@ -4739,7 +4743,7 @@ async function ensureClaudeCredentials(env = process.env) {
4739
4743
  }
4740
4744
  function claudeJsonPath() {
4741
4745
  const configDir = process.env.CLAUDE_CONFIG_DIR;
4742
- return configDir ? join5(configDir, ".claude.json") : join5(homedir2(), ".claude.json");
4746
+ return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
4743
4747
  }
4744
4748
  function asRecord(value) {
4745
4749
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
@@ -4995,7 +4999,7 @@ function createHarness(kind = "sdk", ptyBridge) {
4995
4999
 
4996
5000
  // src/execution/query-executor.ts
4997
5001
  import { createHash } from "crypto";
4998
- import { existsSync, readFileSync as readFileSync2, truncateSync } from "fs";
5002
+ import { existsSync as existsSync2, readFileSync, truncateSync } from "fs";
4999
5003
 
5000
5004
  // src/execution/pack-runner-prompt.ts
5001
5005
  function findLastAgentMessageIndex(history) {
@@ -5150,8 +5154,9 @@ function formatChatFile(file) {
5150
5154
  return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]: ${file.downloadUrl}`];
5151
5155
  }
5152
5156
  if (file.content && file.contentEncoding === "base64") {
5157
+ const link = file.downloadUrl ? ` or download: ${file.downloadUrl}` : "";
5153
5158
  return [
5154
- `[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_attachment("${file.fileId}") to view]`
5159
+ `[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_attachment("${file.fileId}") to view${link}]`
5155
5160
  ];
5156
5161
  }
5157
5162
  return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`];
@@ -5163,8 +5168,9 @@ function formatTaskFile(file) {
5163
5168
  }
5164
5169
  if (file.content && file.contentEncoding === "base64") {
5165
5170
  const size = formatFileSize(file.fileSize);
5171
+ const link = file.downloadUrl ? ` or download: ${file.downloadUrl}` : "";
5166
5172
  return [
5167
- `- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_attachment("${file.fileId}") to view]`
5173
+ `- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_attachment("${file.fileId}") to view${link}]`
5168
5174
  ];
5169
5175
  }
5170
5176
  if (!file.content) {
@@ -5647,7 +5653,7 @@ function buildDiscoveryPrompt(context, runnerMode) {
5647
5653
  ## Mode: Discovery`,
5648
5654
  `You are in Discovery mode \u2014 helping plan and scope this task.`,
5649
5655
  `- You have read-only codebase access (can read files, run git commands, search code)`,
5650
- `- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
5656
+ `- You can write plan files in .claude/plans/ only \u2014 no other file writes. Plan files on disk are NOT synced to the task; save the plan via update_task_plan`,
5651
5657
  `- Do NOT attempt to edit, write, or modify source code files \u2014 these operations will be denied`,
5652
5658
  `- If you identify code changes needed, describe them in the plan instead of implementing them`,
5653
5659
  `- You can create and manage subtasks`,
@@ -5697,7 +5703,7 @@ function buildDiscoveryPrompt(context, runnerMode) {
5697
5703
  ``,
5698
5704
  `### Plan Verification Requirements`,
5699
5705
  `Every plan MUST include a **Testing / Verification** section so the build agent has a clear definition of "done". Enumerate:`,
5700
- `- The quality gates the build agent should run: \`bun run lint\`, \`bun run typecheck\`, \`bun run test\` (or scoped equivalents)`,
5706
+ `- The scoped verification for the change: \`bun run check\` (lint + typecheck) plus \`bun run test:affected\` \u2014 name the specific package suites the diff will touch. Docs-only plans should state that no local gates are needed (CI validates on the PR).`,
5701
5707
  `- Any task-specific end-to-end checks (manual UI walk-through, API smoke test, migration dry-run, etc.)`,
5702
5708
  `You are NOT expected to run these gates yourself \u2014 discovery is read-only. Just describe them in the plan.`,
5703
5709
  ``,
@@ -5776,14 +5782,14 @@ function buildModePrompt(agentMode, context, runnerMode) {
5776
5782
  `- Goal: coordinate child task execution and ensure all children complete successfully`
5777
5783
  ] : [
5778
5784
  `- If this is a leaf task (no children): execute the plan directly`,
5779
- `- Goal: implement the plan, verify quality gates, open a PR when done`,
5785
+ `- Goal: implement the plan, run scoped verification, open a PR when done`,
5780
5786
  ``,
5781
5787
  `### Pre-PR Verification Checklist`,
5782
- `Before calling \`mcp__conveyor__create_pull_request\`, verify ALL of the following pass:`,
5783
- `1. \`bun run lint\` \u2014 no new lint errors`,
5784
- `2. \`bun run typecheck\` \u2014 no new type errors`,
5785
- `3. \`bun run test\` \u2014 relevant suites pass (scope to the affected package if the full run is prohibitively slow, and state the scope in the PR description)`,
5786
- `If a gate fails, fix it before opening the PR. Do NOT open PRs with known failing gates.`,
5788
+ `CI runs the FULL suite (lint, typecheck, all test shards) on every PR \u2014 do not duplicate it locally. Before calling \`mcp__conveyor__create_pull_request\`, scope verification to your diff:`,
5789
+ `1. \`bun run check\` \u2014 lint + typecheck (fast; run whenever you changed code)`,
5790
+ `2. \`bun run test:affected\` \u2014 runs only the tests your diff can affect (docs-only diffs run nothing; apps/api diffs run unit tests only since CI covers the int shards; shared/db diffs escalate to the full suite automatically)`,
5791
+ `Docs/markdown/.claude-only changes need NO local gates \u2014 open the PR and let CI validate.`,
5792
+ `If a gate fails, fix it before opening the PR. Do NOT open PRs with known failing gates. Never run the full \`bun run test\` for a diff confined to one package.`,
5787
5793
  `For refactors: also run \`git diff ${context?.baseBranch ?? "dev"}..HEAD\` and confirm the public API surface (exports, function signatures) has no unintended breaking changes.`
5788
5794
  ]
5789
5795
  ];
@@ -5884,12 +5890,13 @@ function buildPmPreamble(context) {
5884
5890
  `
5885
5891
  Environment (ready, no setup required):`,
5886
5892
  `- Repository is cloned at your current working directory.`,
5893
+ `- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`); never spend a tool call on a bare \`cd\`.`,
5887
5894
  `- You can read files and run git commands to understand the codebase before writing task plans.`,
5888
5895
  `- Check the dev branch (e.g. run: git fetch && git checkout dev || git checkout main) to understand the current state of the codebase that agents will branch off of.`,
5889
5896
  `
5890
5897
  Workflow:`,
5891
- `- You can draft and iterate on plans in .claude/plans/*.md \u2014 these files are automatically synced to the task.`,
5892
- `- You can also use update_task_plan directly to save the plan to the task.`,
5898
+ `- You can draft and iterate on plans in .claude/plans/*.md, but files on disk are NOT synced to the task.`,
5899
+ `- Save the plan to the task with update_task_plan \u2014 this is the only way the plan is persisted.`,
5893
5900
  `- After saving the plan, call post_to_chat with a short summary for the team (your turn output is NOT posted to chat \u2014 they only see what you post), then end your turn. Do NOT attempt to execute the plan yourself.`,
5894
5901
  `- A separate task agent will handle execution after the team reviews and approves your plan.`
5895
5902
  ];
@@ -5922,6 +5929,7 @@ Environment (ready, no setup required):`,
5922
5929
  `- Repository is cloned at your current working directory.`,
5923
5930
  `- You can read, write, and edit files directly.`,
5924
5931
  `- You can run shell commands including git, build tools, and test runners.`,
5932
+ `- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`, \`bun run --cwd\`); never spend a tool call on a bare \`cd\`.`,
5925
5933
  context.githubBranch ? `- You are working on branch: \`${context.githubBranch}\`` : "",
5926
5934
  `
5927
5935
  Safety rules:`,
@@ -5946,6 +5954,10 @@ Environment (fully ready \u2014 do NOT verify or set up):`,
5946
5954
  `- Branch \`${context.githubBranch}\` is already checked out.`,
5947
5955
  `- All dependencies are installed, database is migrated, and the dev server is running.`,
5948
5956
  `- Git is configured. Commit and push directly to this branch.`,
5957
+ `- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`, \`bun run --cwd\`); never spend a tool call on a bare \`cd\`.`,
5958
+ `- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output. Don't poll background jobs with sleep/ps/tail loops.`,
5959
+ `- The \`gh\` CLI may not be installed \u2014 use the mcp__conveyor__* tools for PR and task state.`,
5960
+ `- Read a file before your first Write/Edit to it, and batch multiple changes to the same file into a single call instead of many sequential edits.`,
5949
5961
  `
5950
5962
  IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the following:`,
5951
5963
  `- bun/npm install, pip install, or any dependency installation`,
@@ -6208,7 +6220,7 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
6208
6220
  `Your FIRST action should be reading the relevant source files mentioned in the plan, then writing code. Do NOT run install, build, lint, test, or dev server commands first \u2014 the environment is already set up.`,
6209
6221
  `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
6210
6222
  `Use post_to_chat to keep the team informed (your turn output is NOT shown in chat \u2014 post_to_chat is the only way they see it): briefly post what you're doing when you begin meaningful implementation, and again when the PR is ready.`,
6211
- `Before opening the PR, run the quality gates: \`bun run lint && bun run typecheck && bun run test\`. Fix any failures. Do NOT open a PR with known failing gates \u2014 scope the test run to affected packages if the full suite is prohibitively slow, and state the scope in the PR body.`,
6223
+ `Before opening the PR, verify with \`bun run check\` (lint + typecheck) and \`bun run test:affected\` (tests scoped to your diff \u2014 docs-only changes need no local gates). Fix any failures. Do NOT open a PR with known failing gates. CI runs the full suite on the PR \u2014 never run the full \`bun run test\` for a diff confined to one package.`,
6212
6224
  `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
6213
6225
  ];
6214
6226
  if (isAutoMode) {
@@ -6280,7 +6292,7 @@ New messages since your last run:`,
6280
6292
  ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
6281
6293
  `
6282
6294
  Address the requested changes directly. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 go straight to implementing the feedback.`,
6283
- `Before pushing or opening a PR, re-run the quality gates: \`bun run lint && bun run typecheck && bun run test\`. Fix any failures \u2014 relaunches are the most common place verification gets skipped.`,
6295
+ `Before pushing or opening a PR, re-verify with \`bun run check\` and \`bun run test:affected\`. Fix any failures \u2014 relaunches are the most common place verification gets skipped. CI runs the full suite on the PR; keep local runs scoped to your diff.`,
6284
6296
  `Implement your updates and open a PR when finished.`
6285
6297
  ];
6286
6298
  if (context.githubPRUrl) {
@@ -6723,11 +6735,11 @@ function buildCreatePullRequestTool(connection, config) {
6723
6735
  async ({ title, body, branch, baseBranch, commitMessage, skipVerify }) => {
6724
6736
  try {
6725
6737
  const cwd = config.workspaceDir;
6726
- if (hasUncommittedChanges(cwd)) {
6738
+ if (await hasUncommittedChanges(cwd)) {
6727
6739
  const message = commitMessage || `${title}
6728
6740
 
6729
6741
  Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
6730
- const commitHash = stageAndCommit(cwd, message);
6742
+ const commitHash = await stageAndCommit(cwd, message);
6731
6743
  if (commitHash) {
6732
6744
  connection.sendEvent({
6733
6745
  type: "message",
@@ -6739,7 +6751,7 @@ Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
6739
6751
  );
6740
6752
  }
6741
6753
  }
6742
- if (hasUnpushedCommits(cwd)) {
6754
+ if (await hasUnpushedCommits(cwd)) {
6743
6755
  const pushSuccess = await pushToOrigin(
6744
6756
  cwd,
6745
6757
  async () => {
@@ -6938,7 +6950,7 @@ function buildMutationTools(connection, config) {
6938
6950
 
6939
6951
  // src/tools/attachment-tools.ts
6940
6952
  import { readFile as readFile3, stat as stat5 } from "fs/promises";
6941
- import { basename, extname, isAbsolute, join as join6 } from "path";
6953
+ import { basename, extname, isAbsolute, join as join5 } from "path";
6942
6954
  import { z as z6 } from "zod";
6943
6955
  var IMAGE_MIME_BY_EXT = {
6944
6956
  ".png": "image/png",
@@ -6957,7 +6969,7 @@ function buildUploadAttachmentTool(connection, config) {
6957
6969
  },
6958
6970
  async ({ path: path4, title }) => {
6959
6971
  try {
6960
- const filePath = isAbsolute(path4) ? path4 : join6(config.workspaceDir, path4);
6972
+ const filePath = isAbsolute(path4) ? path4 : join5(config.workspaceDir, path4);
6961
6973
  const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
6962
6974
  if (!mimeType) {
6963
6975
  return textResult(
@@ -8092,7 +8104,6 @@ async function handleExitPlanMode(host, input) {
8092
8104
  return { behavior: "allow", updatedInput: input };
8093
8105
  }
8094
8106
  try {
8095
- await host.syncPlanFile();
8096
8107
  const taskProps = await host.connection.getTaskProperties();
8097
8108
  const missingProps = collectMissingProps(taskProps);
8098
8109
  if (host.isParentTask) {
@@ -8362,7 +8373,7 @@ function sessionLineageKey(taskId, agentMode, runnerMode) {
8362
8373
  }
8363
8374
  function sessionFileExists(sessionUuid, cwd) {
8364
8375
  try {
8365
- return existsSync(sessionTranscriptPath(cwd, sessionUuid));
8376
+ return existsSync2(sessionTranscriptPath(cwd, sessionUuid));
8366
8377
  } catch {
8367
8378
  return false;
8368
8379
  }
@@ -8381,8 +8392,8 @@ function resolveSessionStart(lineageKey, cwd) {
8381
8392
  }
8382
8393
  function repairTornSessionFile(path4) {
8383
8394
  try {
8384
- if (!existsSync(path4)) return false;
8385
- const content = readFileSync2(path4, "utf8");
8395
+ if (!existsSync2(path4)) return false;
8396
+ const content = readFileSync(path4, "utf8");
8386
8397
  if (content.length === 0) return false;
8387
8398
  let keepEnd = content.length;
8388
8399
  if (!content.endsWith("\n")) {
@@ -8526,14 +8537,21 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
8526
8537
 
8527
8538
  The team says:
8528
8539
  ${followUpText}` : followUpText;
8540
+ const isPty = host.harnessKind === "pty";
8529
8541
  if (isPmMode) {
8530
- const prompt = buildMultimodalPrompt(textPrompt, context);
8542
+ const prompt = buildMultimodalPrompt(textPrompt, context, isPty);
8531
8543
  if (followUpImages.length > 0 && Array.isArray(prompt)) {
8532
8544
  prompt.push(...followUpImages);
8533
8545
  }
8534
8546
  return prompt;
8535
8547
  }
8536
8548
  if (followUpImages.length > 0) {
8549
+ if (isPty) {
8550
+ const refs = followUpImages.map(
8551
+ () => `[Image attachment \u2014 use list_task_files / get_attachment to view]`
8552
+ );
8553
+ return [textPrompt, ...refs].join("\n");
8554
+ }
8537
8555
  return [{ type: "text", text: textPrompt }, ...followUpImages];
8538
8556
  }
8539
8557
  return textPrompt;
@@ -8574,10 +8592,6 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
8574
8592
  if (host.isStopped()) return;
8575
8593
  const mode = host.agentMode;
8576
8594
  const isDiscoveryLike = mode === "discovery" || mode === "help";
8577
- const needsPlanSync = isDiscoveryLike || mode === "auto" && !host.hasExitedPlanMode;
8578
- if (needsPlanSync) {
8579
- host.snapshotPlanFiles();
8580
- }
8581
8595
  const sessionStart = resolveSessionStart(
8582
8596
  sessionLineageKey(context.taskId, mode, host.config.mode),
8583
8597
  host.config.workspaceDir
@@ -8599,14 +8613,12 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
8599
8613
  const resume = sessionStart.resume;
8600
8614
  if (followUpContent) {
8601
8615
  await runFollowUpQuery(host, context, options, resume, followUpContent);
8602
- } else if (isDiscoveryLike && promptDelivery !== "prefill") {
8603
8616
  return;
8604
- } else {
8605
- await runInitialQuery(host, context, options, resume, promptDelivery);
8606
8617
  }
8607
- if (needsPlanSync) {
8608
- await host.syncPlanFile();
8618
+ if (isDiscoveryLike && promptDelivery !== "prefill") {
8619
+ return;
8609
8620
  }
8621
+ await runInitialQuery(host, context, options, resume, promptDelivery);
8610
8622
  }
8611
8623
  async function runFollowUpQuery(host, context, options, resume, followUpContent) {
8612
8624
  if (options.promptDelivery === "prefill") {
@@ -8675,7 +8687,7 @@ function latestUserMessageText(context) {
8675
8687
  }
8676
8688
  return null;
8677
8689
  }
8678
- function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAppendSystemPrompt) {
8690
+ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAppendSystemPrompt, harnessKind) {
8679
8691
  if (promptDelivery === "prefill") {
8680
8692
  return {
8681
8693
  prompt: latestUserMessageText(context) ?? "",
@@ -8683,7 +8695,9 @@ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAp
8683
8695
  };
8684
8696
  }
8685
8697
  return {
8686
- prompt: buildMultimodalPrompt(initialPrompt, context),
8698
+ // On the PTY harness image blocks would be pasted into the TUI as text —
8699
+ // the prompt body already links each image (get_attachment / downloadUrl).
8700
+ prompt: buildMultimodalPrompt(initialPrompt, context, harnessKind === "pty"),
8687
8701
  appendSystemPrompt: baseAppendSystemPrompt
8688
8702
  };
8689
8703
  }
@@ -8698,7 +8712,8 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
8698
8712
  promptDelivery,
8699
8713
  initialPrompt,
8700
8714
  context,
8701
- options.appendSystemPrompt
8715
+ options.appendSystemPrompt,
8716
+ host.harnessKind
8702
8717
  );
8703
8718
  const queryOptions = { ...options, appendSystemPrompt };
8704
8719
  let agentQuery = host.harness.executeQuery({
@@ -8723,7 +8738,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
8723
8738
  const retryPrompt = buildMultimodalPrompt(
8724
8739
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
8725
8740
  context,
8726
- lastErrorWasImage
8741
+ lastErrorWasImage || host.harnessKind === "pty"
8727
8742
  );
8728
8743
  return host.harness.executeQuery({
8729
8744
  prompt: host.createInputStream(retryPrompt),
@@ -8751,7 +8766,8 @@ async function handleAuthError(context, host, options) {
8751
8766
  host.connection.storeSessionId("");
8752
8767
  const freshPrompt = buildMultimodalPrompt(
8753
8768
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
8754
- context
8769
+ context,
8770
+ host.harnessKind === "pty"
8755
8771
  );
8756
8772
  const freshQuery = host.harness.executeQuery({
8757
8773
  prompt: host.createInputStream(freshPrompt),
@@ -8765,7 +8781,8 @@ async function handleStaleSession(context, host, options) {
8765
8781
  host.connection.storeSessionId("");
8766
8782
  const freshPrompt = buildMultimodalPrompt(
8767
8783
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
8768
- context
8784
+ context,
8785
+ host.harnessKind === "pty"
8769
8786
  );
8770
8787
  const freshQuery = host.harness.executeQuery({
8771
8788
  prompt: host.createInputStream(freshPrompt),
@@ -8991,13 +9008,14 @@ function resolveHarnessKind() {
8991
9008
  function buildPtyBridge(connection) {
8992
9009
  return {
8993
9010
  sendOutput: (data, dims) => connection.sendPtyOutput(data, dims),
9011
+ sendChatEvent: (event) => connection.sendPtyChatEvent(event),
8994
9012
  sendEnded: () => connection.sendPtyEnded(),
8995
9013
  onInput: (handler) => connection.onPtyInput(handler),
8996
9014
  onResize: (handler) => connection.onPtyResize(handler)
8997
9015
  };
8998
9016
  }
8999
9017
  var QueryBridge = class {
9000
- constructor(connection, mode, runnerConfig, callbacks, workspaceDir) {
9018
+ constructor(connection, mode, runnerConfig, callbacks) {
9001
9019
  this.connection = connection;
9002
9020
  this.mode = mode;
9003
9021
  this.runnerConfig = runnerConfig;
@@ -9008,7 +9026,6 @@ var QueryBridge = class {
9008
9026
  harnessKind,
9009
9027
  harnessKind === "pty" ? buildPtyBridge(connection) : void 0
9010
9028
  );
9011
- this.planSync = new PlanSync(workspaceDir, connection);
9012
9029
  }
9013
9030
  connection;
9014
9031
  mode;
@@ -9018,7 +9035,6 @@ var QueryBridge = class {
9018
9035
  /** Which harness drives this bridge ("pty" task chat; "sdk" only under the
9019
9036
  * CONVEYOR_FORCE_SDK_CARDS maintainer override). */
9020
9037
  harnessKind;
9021
- planSync;
9022
9038
  sessionIds = /* @__PURE__ */ new Map();
9023
9039
  activeQuery = null;
9024
9040
  pendingToolOutputs = [];
@@ -9219,8 +9235,6 @@ var QueryBridge = class {
9219
9235
  if (bridge.onSoftStop) bridge.onSoftStop();
9220
9236
  },
9221
9237
  createInputStream: (prompt) => bridge.createInputStream(prompt),
9222
- snapshotPlanFiles: () => bridge.planSync.snapshotPlanFiles(),
9223
- syncPlanFile: () => bridge.planSync.syncPlanFile(),
9224
9238
  onModeTransition: bridge.onModeTransition
9225
9239
  };
9226
9240
  }
@@ -9236,9 +9250,9 @@ var QueryBridge = class {
9236
9250
  };
9237
9251
 
9238
9252
  // src/runner/session-runner-helpers.ts
9239
- import { readFileSync as readFileSync3 } from "fs";
9240
- import { dirname as dirname2, join as join7 } from "path";
9241
- import { fileURLToPath } from "url";
9253
+ import { readFileSync as readFileSync2 } from "fs";
9254
+ import { dirname as dirname2, join as join6 } from "path";
9255
+ import { fileURLToPath as fileURLToPath2 } from "url";
9242
9256
  function mapChatHistory(messages) {
9243
9257
  if (!messages) return [];
9244
9258
  return messages.map((m) => ({
@@ -9263,10 +9277,10 @@ function mapChatHistory(messages) {
9263
9277
  }
9264
9278
  function readAgentVersion() {
9265
9279
  try {
9266
- const here = dirname2(fileURLToPath(import.meta.url));
9280
+ const here = dirname2(fileURLToPath2(import.meta.url));
9267
9281
  for (const rel of ["../package.json", "../../package.json"]) {
9268
9282
  try {
9269
- const pkg = JSON.parse(readFileSync3(join7(here, rel), "utf-8"));
9283
+ const pkg = JSON.parse(readFileSync2(join6(here, rel), "utf-8"));
9270
9284
  if (pkg.version) return pkg.version;
9271
9285
  } catch {
9272
9286
  }
@@ -9352,7 +9366,7 @@ async function sampleKeyUsage(token) {
9352
9366
 
9353
9367
  // src/runner/port-discovery.ts
9354
9368
  import { readFile as readFile4 } from "fs/promises";
9355
- import { execFile } from "child_process";
9369
+ import { execFile as execFile3 } from "child_process";
9356
9370
  var PROC_TCP_LISTEN_STATE = "0A";
9357
9371
  function parseProcNetTcpListeners(content) {
9358
9372
  const ports = [];
@@ -9387,7 +9401,7 @@ async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
9387
9401
  }
9388
9402
  async function readNetstatListeningPorts() {
9389
9403
  const output = await new Promise((resolve) => {
9390
- execFile("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
9404
+ execFile3("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
9391
9405
  resolve(err ? null : stdout);
9392
9406
  });
9393
9407
  });
@@ -9538,10 +9552,12 @@ var PortDiscovery = class {
9538
9552
  };
9539
9553
 
9540
9554
  // src/runner/parent-pull-handler.ts
9541
- import { execSync as execSync4 } from "child_process";
9542
- function handlePullBranch(workDir, branch) {
9555
+ import { execFile as execFile4 } from "child_process";
9556
+ import { promisify as promisify3 } from "util";
9557
+ var execFileAsync3 = promisify3(execFile4);
9558
+ async function handlePullBranch(workDir, branch) {
9543
9559
  if (!branch) return;
9544
- const current = getCurrentBranch(workDir);
9560
+ const current = await getCurrentBranch(workDir);
9545
9561
  if (current !== branch) {
9546
9562
  process.stderr.write(
9547
9563
  `[conveyor-agent] pull_branch ignored \u2014 current branch ${current ?? "(detached)"} != ${branch}
@@ -9549,7 +9565,7 @@ function handlePullBranch(workDir, branch) {
9549
9565
  );
9550
9566
  return;
9551
9567
  }
9552
- if (hasUncommittedChanges(workDir)) {
9568
+ if (await hasUncommittedChanges(workDir)) {
9553
9569
  process.stderr.write(
9554
9570
  `[conveyor-agent] pull_branch ignored \u2014 uncommitted changes on ${branch}
9555
9571
  `
@@ -9557,16 +9573,15 @@ function handlePullBranch(workDir, branch) {
9557
9573
  return;
9558
9574
  }
9559
9575
  try {
9560
- execSync4(`git fetch origin ${branch}`, { cwd: workDir, stdio: "ignore", timeout: 6e4 });
9576
+ await execFileAsync3("git", ["fetch", "origin", branch], { cwd: workDir, timeout: 6e4 });
9561
9577
  } catch {
9562
9578
  process.stderr.write(`[conveyor-agent] pull_branch: fetch failed for ${branch}
9563
9579
  `);
9564
9580
  return;
9565
9581
  }
9566
9582
  try {
9567
- execSync4(`git pull --ff-only origin ${branch}`, {
9583
+ await execFileAsync3("git", ["pull", "--ff-only", "origin", branch], {
9568
9584
  cwd: workDir,
9569
- stdio: "ignore",
9570
9585
  timeout: 6e4
9571
9586
  });
9572
9587
  process.stderr.write(`[conveyor-agent] pull_branch: pulled origin/${branch}
@@ -9579,6 +9594,33 @@ function handlePullBranch(workDir, branch) {
9579
9594
  }
9580
9595
  }
9581
9596
 
9597
+ // src/runner/heavy-gate.ts
9598
+ import { readFileSync as readFileSync3 } from "fs";
9599
+ import path3 from "path";
9600
+ var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
9601
+ function runDir() {
9602
+ return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
9603
+ }
9604
+ function pidAlive(pid) {
9605
+ try {
9606
+ process.kill(pid, 0);
9607
+ return true;
9608
+ } catch {
9609
+ return false;
9610
+ }
9611
+ }
9612
+ function isHeavyGateActive() {
9613
+ for (const key of GATE_KEYS) {
9614
+ try {
9615
+ const raw = readFileSync3(path3.join(runDir(), `${key}.pid`), "utf8").trim();
9616
+ const pid = Number.parseInt(raw, 10);
9617
+ if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
9618
+ } catch {
9619
+ }
9620
+ }
9621
+ return false;
9622
+ }
9623
+
9582
9624
  // src/runner/session-runner.ts
9583
9625
  function isPostToChatTool(name) {
9584
9626
  return typeof name === "string" && (name === "post_to_chat" || name.endsWith("__post_to_chat"));
@@ -9616,8 +9658,14 @@ var SessionRunner = class _SessionRunner {
9616
9658
  agentSpokeThisTurn = false;
9617
9659
  /** Guards overlapping runs of the periodic git flush. */
9618
9660
  periodicFlushInFlight = false;
9661
+ /** Consecutive flush ticks skipped because a heavy gate was running. */
9662
+ gateSkipCount = 0;
9663
+ /** Max consecutive gate-deferred ticks (~2 min each) before flushing anyway. */
9664
+ static MAX_GATE_SKIPS = 10;
9619
9665
  /** Runtime preview-port poller (v3 dynamic port discovery). */
9620
9666
  portDiscovery = null;
9667
+ /** Main event-loop lag measurement, shared with the heartbeat worker. */
9668
+ loopLag = new LoopLagMonitor();
9621
9669
  constructor(config, callbacks) {
9622
9670
  this.config = config;
9623
9671
  this.callbacks = callbacks;
@@ -9626,7 +9674,7 @@ var SessionRunner = class _SessionRunner {
9626
9674
  this.mode = new ModeController(initialMode, config.runnerMode, config.isAuto);
9627
9675
  const lifecycleConfig = { ...DEFAULT_LIFECYCLE_CONFIG, ...config.lifecycle };
9628
9676
  this.lifecycle = new Lifecycle(lifecycleConfig, {
9629
- onHeartbeat: () => this.connection.sendHeartbeat(),
9677
+ onHeartbeat: () => this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs()),
9630
9678
  onIdleTimeout: () => {
9631
9679
  process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
9632
9680
  this.stopped = true;
@@ -9674,6 +9722,8 @@ var SessionRunner = class _SessionRunner {
9674
9722
  this.connection.sendEvent({ type: "connected", sessionId: this.sessionId });
9675
9723
  this.wireConnectionCallbacks();
9676
9724
  this.lifecycle.startHeartbeat();
9725
+ this.loopLag.start();
9726
+ this.connection.startHeartbeatWorker(this.loopLag.sharedBuffer);
9677
9727
  this.lifecycle.startTokenRefresh();
9678
9728
  this.lifecycle.startUsageSample();
9679
9729
  const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
@@ -9736,10 +9786,10 @@ var SessionRunner = class _SessionRunner {
9736
9786
  }
9737
9787
  if (process.env.CONVEYOR_GIT_READY !== "1") {
9738
9788
  if (this.fullContext?.githubBranch) {
9739
- ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
9789
+ await ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
9740
9790
  }
9741
9791
  if (this.fullContext?.baseBranch) {
9742
- syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
9792
+ await syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
9743
9793
  }
9744
9794
  }
9745
9795
  if (!this.stopped) {
@@ -9759,7 +9809,7 @@ var SessionRunner = class _SessionRunner {
9759
9809
  );
9760
9810
  }
9761
9811
  } else {
9762
- const restored = restoreWipSnapshot(
9812
+ const restored = await restoreWipSnapshot(
9763
9813
  this.config.workspaceDir,
9764
9814
  this.fullContext.githubBranch
9765
9815
  );
@@ -10102,6 +10152,14 @@ var SessionRunner = class _SessionRunner {
10102
10152
  * clean tree. Guarded so two ticks can't overlap. Never throws. */
10103
10153
  async periodicGitFlush() {
10104
10154
  if (this.periodicFlushInFlight || this.stopped) return;
10155
+ if (isHeavyGateActive() && this.gateSkipCount < _SessionRunner.MAX_GATE_SKIPS) {
10156
+ this.gateSkipCount++;
10157
+ if (this.gateSkipCount === 1) {
10158
+ process.stderr.write("[conveyor-agent] Periodic git flush deferred \u2014 heavy gate running\n");
10159
+ }
10160
+ return;
10161
+ }
10162
+ this.gateSkipCount = 0;
10105
10163
  this.periodicFlushInFlight = true;
10106
10164
  try {
10107
10165
  const result = await flushAllPendingWork(this.config.workspaceDir, {
@@ -10200,6 +10258,7 @@ var SessionRunner = class _SessionRunner {
10200
10258
  void this.queryBridge?.dispose().catch(() => {
10201
10259
  });
10202
10260
  this.portDiscovery?.stop();
10261
+ this.loopLag.stop();
10203
10262
  this.lifecycle.destroy();
10204
10263
  this.connection.disconnect();
10205
10264
  if (this.inputResolver) {
@@ -10219,23 +10278,41 @@ var SessionRunner = class _SessionRunner {
10219
10278
  }
10220
10279
  // ── Auto-mode stuck detection & nudge ───────────────────────────────
10221
10280
  static MAX_STUCK_NUDGES = 3;
10222
- /** The prompt delivered into the live TUI when an auto agent looks stuck. It
10223
- * must offer BOTH escape routes so the agent never just goes idle. */
10281
+ /** The build-phase nudge (In Progress, no PR). Offers BOTH escape routes so
10282
+ * the agent never just goes idle. */
10224
10283
  static STUCK_PROMPT = "You are in Auto mode, your task is still In Progress, and there is no open pull request. Do ONE of these right now: (1) open a pull request with the create_pull_request tool, OR (2) call post_to_chat to explain exactly where you are stuck and what you need from a human. Do not finish or go idle silently.";
10284
+ /** The planning-phase nudge (still Planning, identification/plan unfinished).
10285
+ * Same two-escape-route shape, aimed at finishing discovery instead of a PR. */
10286
+ static STUCK_PROMPT_PLANNING = "You are in Auto mode and still in the Planning phase \u2014 identification and/or the plan are not finished. Do ONE of these right now: (1) finish identifying the task (set the story points) and save the plan with the update_task_plan tool, then call ExitPlanMode to begin building, OR (2) call post_to_chat to explain exactly where you are stuck and what you need from a human. Do not finish or go idle silently.";
10225
10287
  /**
10226
- * An auto task is "stuck" when its last turn ended (the call sites run
10227
- * post-turn, so the TUI is no longer actively working), it has no open PR,
10228
- * AND the agent did not communicate in chat this turn. If the agent posted a
10229
- * message (explained itself / asked for help) it is waiting on a human, not
10230
- * silently stuck, so we leave it attached instead of nudging.
10288
+ * Classify how an auto task is "stuck" after its turn ended (call sites run
10289
+ * post-turn, so the TUI is no longer working), or null if it isn't. Shared
10290
+ * guards: only auto, not rate-limited, under the nudge cap, and the agent
10291
+ * stayed silent this turn if it posted to chat (explained itself / asked for
10292
+ * help) it's waiting on a human, not silently stuck.
10293
+ *
10294
+ * - "pr": In Progress with no open PR (build finished without shipping).
10295
+ * - "planning": still Planning without (identified + saved plan). A fresh auto
10296
+ * agent that finishes its plan turn WITHOUT ExitPlanMode would otherwise idle
10297
+ * → clean-exit → its session Ends → the workspace is reaped "agent_gone"
10298
+ * before a plan ever landed. Nudging (and, on exhaustion, going dormant)
10299
+ * keeps the session Active so the pod is never prematurely slept.
10231
10300
  */
10301
+ autoStuckKind() {
10302
+ if (!this.mode.isAuto || this.stopped) return null;
10303
+ if (this.queryBridge?.wasRateLimited) return null;
10304
+ if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) return null;
10305
+ if (this.agentSpokeThisTurn) return null;
10306
+ if (!this.taskContext) return null;
10307
+ const { status, storyPointId, plan, githubPRUrl } = this.taskContext;
10308
+ if (status === "InProgress" && !githubPRUrl) return "pr";
10309
+ if (status === "Planning" && !(storyPointId !== null && !!plan?.trim())) {
10310
+ return "planning";
10311
+ }
10312
+ return null;
10313
+ }
10232
10314
  isAutoStuck() {
10233
- if (!this.mode.isAuto || this.stopped) return false;
10234
- if (this.queryBridge?.wasRateLimited) return false;
10235
- if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) return false;
10236
- if (this.agentSpokeThisTurn) return false;
10237
- if (!this.taskContext) return false;
10238
- return this.taskContext.status === "InProgress" && !this.taskContext.githubPRUrl;
10315
+ return this.autoStuckKind() !== null;
10239
10316
  }
10240
10317
  /**
10241
10318
  * Stop driving the agent and route the core loop into dormant idle —
@@ -10271,6 +10348,16 @@ var SessionRunner = class _SessionRunner {
10271
10348
  } catch {
10272
10349
  }
10273
10350
  }
10351
+ /** Prompt + chat-message label for a stuck-nudge of the given kind. */
10352
+ stuckNudgeContent(kind) {
10353
+ if (kind === "planning") {
10354
+ return {
10355
+ prompt: _SessionRunner.STUCK_PROMPT_PLANNING,
10356
+ situation: "still Planning with no identified plan"
10357
+ };
10358
+ }
10359
+ return { prompt: _SessionRunner.STUCK_PROMPT, situation: "still no PR" };
10360
+ }
10274
10361
  async maybeHandleStuckAuto() {
10275
10362
  await this.refreshTaskContext();
10276
10363
  if (!this.isAutoStuck()) return;
@@ -10278,17 +10365,18 @@ var SessionRunner = class _SessionRunner {
10278
10365
  this.prNudgeCount++;
10279
10366
  if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) {
10280
10367
  this.enterDormantForHumanTakeover(
10281
- `Auto-mode: I couldn't open a PR or get unstuck after ${_SessionRunner.MAX_STUCK_NUDGES} attempts. Staying connected \u2014 reply here or in the terminal and I'll keep working.`
10368
+ `Auto-mode: I couldn't finish or get unstuck after ${_SessionRunner.MAX_STUCK_NUDGES} attempts. Staying connected \u2014 reply here or in the terminal and I'll keep working.`
10282
10369
  );
10283
10370
  return;
10284
10371
  }
10372
+ const { prompt, situation } = this.stuckNudgeContent(this.autoStuckKind());
10285
10373
  const attempt = this.prNudgeCount;
10286
- const chatMsg = attempt === 1 ? "Auto-nudge: still In Progress with no PR \u2014 prompting the agent to finish or explain where it's stuck..." : `Auto-nudge (attempt ${attempt}/${_SessionRunner.MAX_STUCK_NUDGES}): still no PR \u2014 prompting the agent again...`;
10374
+ const chatMsg = attempt === 1 ? `Auto-nudge: ${situation} \u2014 prompting the agent to finish or explain where it's stuck...` : `Auto-nudge (attempt ${attempt}/${_SessionRunner.MAX_STUCK_NUDGES}): ${situation} \u2014 prompting the agent again...`;
10287
10375
  this.connection.postChatMessage(chatMsg);
10288
10376
  await this.setState("running");
10289
- await this.callbacks.onEvent({ type: "pr_nudge", prompt: _SessionRunner.STUCK_PROMPT });
10377
+ await this.callbacks.onEvent({ type: "pr_nudge", prompt });
10290
10378
  if (this.interrupted || this.stopped) return;
10291
- await this.executeQuery(_SessionRunner.STUCK_PROMPT);
10379
+ await this.executeQuery(prompt);
10292
10380
  if (this.interrupted || this.stopped) return;
10293
10381
  await this.refreshTaskContext();
10294
10382
  if (this.agentSpokeThisTurn && !this.taskContext?.githubPRUrl) {
@@ -10346,32 +10434,26 @@ var SessionRunner = class _SessionRunner {
10346
10434
  mode: this.config.runnerMode,
10347
10435
  isAuto: this.config.isAuto
10348
10436
  };
10349
- const bridge = new QueryBridge(
10350
- this.connection,
10351
- this.mode,
10352
- runnerConfig,
10353
- {
10354
- onStatusChange: (status) => {
10355
- if (status === "running" && this._state === "waiting_for_input") {
10356
- this._state = "running";
10357
- this.lifecycle.cancelIdleTimer();
10358
- }
10359
- return this.callbacks.onStatusChange(status);
10360
- },
10361
- onEvent: (event) => {
10362
- const evt = event;
10363
- if (evt.type === "tool_use" && isPostToChatTool(evt.tool)) {
10364
- this.agentSpokeThisTurn = true;
10365
- }
10366
- if (event.type === "completed") {
10367
- this.completedThisTurn = true;
10368
- void this.connection.sendHeartbeat();
10369
- }
10370
- return this.callbacks.onEvent(event);
10437
+ const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
10438
+ onStatusChange: (status) => {
10439
+ if (status === "running" && this._state === "waiting_for_input") {
10440
+ this._state = "running";
10441
+ this.lifecycle.cancelIdleTimer();
10371
10442
  }
10443
+ return this.callbacks.onStatusChange(status);
10372
10444
  },
10373
- this.config.workspaceDir
10374
- );
10445
+ onEvent: (event) => {
10446
+ const evt = event;
10447
+ if (evt.type === "tool_use" && isPostToChatTool(evt.tool)) {
10448
+ this.agentSpokeThisTurn = true;
10449
+ }
10450
+ if (event.type === "completed") {
10451
+ this.completedThisTurn = true;
10452
+ void this.connection.sendHeartbeat();
10453
+ }
10454
+ return this.callbacks.onEvent(event);
10455
+ }
10456
+ });
10375
10457
  bridge.isParentTask = this.fullContext?.isParentTask ?? false;
10376
10458
  bridge.onSoftStop = () => {
10377
10459
  process.stderr.write("[conveyor-agent] Soft stop requested (discovery ExitPlanMode)\n");
@@ -10424,15 +10506,16 @@ var SessionRunner = class _SessionRunner {
10424
10506
  }
10425
10507
  });
10426
10508
  this.connection.onPullBranch(({ branch }) => {
10427
- handlePullBranch(this.config.workspaceDir, branch);
10509
+ void handlePullBranch(this.config.workspaceDir, branch);
10428
10510
  });
10429
10511
  this.connection.onFinalizeSnapshot(() => void this.finalizeSnapshotNow());
10430
10512
  }
10431
10513
  /** Eager finalize snapshot triggered by the reconciler's sleep signal
10432
- * (session:finalizeSnapshot). Runs pg_dump + a full capture and uploads it
10514
+ * (session:finalizeSnapshot). Runs a full workspace capture and uploads it
10433
10515
  * NOW so the sleep confirms on this snapshot instead of waiting for the
10434
- * ~2min periodic flush and so sidecar DB state (which ONLY finalizeForSleep
10435
- * captures via pg_dump) actually lands. Best-effort: on failure the
10516
+ * ~2min periodic flush. Sidecar DB state is intentionally NOT captured it
10517
+ * is no longer durable across sleep/wake (#2623); only the agent's
10518
+ * /workspace rides the snapshot. Best-effort: on failure the
10436
10519
  * periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
10437
10520
  async finalizeSnapshotNow() {
10438
10521
  const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
@@ -10453,9 +10536,7 @@ var SessionRunner = class _SessionRunner {
10453
10536
  }
10454
10537
  }
10455
10538
  });
10456
- process.stderr.write(
10457
- "[conveyor-agent] Finalize snapshot (pg_dump) uploaded on sleep signal\n"
10458
- );
10539
+ process.stderr.write("[conveyor-agent] Finalize snapshot uploaded on sleep signal\n");
10459
10540
  } catch {
10460
10541
  }
10461
10542
  }
@@ -10465,7 +10546,7 @@ var SessionRunner = class _SessionRunner {
10465
10546
  const res = await this.connection.call("refreshGithubToken", {
10466
10547
  sessionId: this.connection.sessionId
10467
10548
  });
10468
- updateRemoteToken(this.config.workspaceDir, res.token);
10549
+ await updateRemoteToken(this.config.workspaceDir, res.token);
10469
10550
  process.env.GITHUB_TOKEN = res.token;
10470
10551
  process.env.GH_TOKEN = res.token;
10471
10552
  process.stderr.write("[conveyor-agent] Proactively refreshed GitHub token\n");
@@ -10484,7 +10565,7 @@ var SessionRunner = class _SessionRunner {
10484
10565
  });
10485
10566
  if (ctx?.githubBranch && this.fullContext) {
10486
10567
  this.fullContext.githubBranch = ctx.githubBranch;
10487
- ensureOnTaskBranch(this.config.workspaceDir, ctx.githubBranch);
10568
+ await ensureOnTaskBranch(this.config.workspaceDir, ctx.githubBranch);
10488
10569
  }
10489
10570
  } catch {
10490
10571
  process.stderr.write(
@@ -10494,6 +10575,8 @@ var SessionRunner = class _SessionRunner {
10494
10575
  }
10495
10576
  async setState(status) {
10496
10577
  this._state = status;
10578
+ const loopStatus = status === "idle" ? "idle" : "active";
10579
+ this.loopLag.setStatus(loopStatus);
10497
10580
  await this.connection.emitStatus(status);
10498
10581
  await this.callbacks.onStatusChange(status);
10499
10582
  }
@@ -10502,6 +10585,7 @@ var SessionRunner = class _SessionRunner {
10502
10585
  `);
10503
10586
  this.connection.sendEvent({ type: "shutdown", reason: finalState });
10504
10587
  this.portDiscovery?.stop();
10588
+ this.loopLag.stop();
10505
10589
  this.lifecycle.destroy();
10506
10590
  try {
10507
10591
  await this.queryBridge?.dispose();
@@ -10551,12 +10635,12 @@ var SessionRunner = class _SessionRunner {
10551
10635
 
10552
10636
  // src/setup/config.ts
10553
10637
  import { readFile as readFile5 } from "fs/promises";
10554
- import { join as join8 } from "path";
10638
+ import { join as join7 } from "path";
10555
10639
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
10556
10640
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
10557
10641
  async function loadForwardPorts(workspaceDir) {
10558
10642
  try {
10559
- const raw = await readFile5(join8(workspaceDir, DEVCONTAINER_PATH), "utf-8");
10643
+ const raw = await readFile5(join7(workspaceDir, DEVCONTAINER_PATH), "utf-8");
10560
10644
  const parsed = JSON.parse(raw);
10561
10645
  const ports = (parsed.forwardPorts ?? []).filter(
10562
10646
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -10588,19 +10672,17 @@ function buildSessionPreviewPorts(result) {
10588
10672
  function loadConveyorConfig() {
10589
10673
  const envSetup = process.env.CONVEYOR_SETUP_COMMAND;
10590
10674
  const envStart = process.env.CONVEYOR_START_COMMAND;
10591
- const envPort = process.env.CONVEYOR_PREVIEW_PORT;
10592
10675
  if (envSetup || envStart) {
10593
10676
  return {
10594
10677
  setupCommand: envSetup,
10595
- startCommand: envStart,
10596
- previewPort: envPort ? Number(envPort) : void 0
10678
+ startCommand: envStart
10597
10679
  };
10598
10680
  }
10599
10681
  return null;
10600
10682
  }
10601
10683
 
10602
10684
  // src/setup/commands.ts
10603
- import { spawn, execSync as execSync5 } from "child_process";
10685
+ import { spawn, execSync } from "child_process";
10604
10686
  function runSetupCommand(cmd, cwd, onOutput) {
10605
10687
  return new Promise((resolve, reject) => {
10606
10688
  const child = spawn("sh", ["-c", cmd], {
@@ -10629,7 +10711,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
10629
10711
  var AUTH_TOKEN_TIMEOUT_MS = 3e4;
10630
10712
  function runAuthTokenCommand(cmd, userEmail, cwd) {
10631
10713
  try {
10632
- const output = execSync5(`${cmd} ${JSON.stringify(userEmail)}`, {
10714
+ const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {
10633
10715
  cwd,
10634
10716
  timeout: AUTH_TOKEN_TIMEOUT_MS,
10635
10717
  stdio: ["ignore", "pipe", "ignore"],
@@ -10659,10 +10741,10 @@ function runStartCommand(cmd, cwd, onOutput) {
10659
10741
  }
10660
10742
 
10661
10743
  // src/setup/codespace.ts
10662
- import { execSync as execSync6 } from "child_process";
10744
+ import { execSync as execSync2 } from "child_process";
10663
10745
  function unshallowRepo(workspaceDir) {
10664
10746
  try {
10665
- execSync6("git fetch --unshallow", {
10747
+ execSync2("git fetch --unshallow", {
10666
10748
  cwd: workspaceDir,
10667
10749
  timeout: 6e4,
10668
10750
  stdio: "ignore"
@@ -10672,10 +10754,10 @@ function unshallowRepo(workspaceDir) {
10672
10754
  }
10673
10755
 
10674
10756
  export {
10675
- DEFAULT_SONNET_MODEL,
10676
10757
  fetchBootstrap,
10677
10758
  applyBootstrapToEnv,
10678
10759
  AgentConnection,
10760
+ DEFAULT_SONNET_MODEL,
10679
10761
  DEFAULT_LIFECYCLE_CONFIG,
10680
10762
  Lifecycle,
10681
10763
  PtyHarness,
@@ -10688,7 +10770,6 @@ export {
10688
10770
  flushPendingChanges,
10689
10771
  pushToOrigin,
10690
10772
  resolveSessionStart,
10691
- PlanSync,
10692
10773
  sampleKeyUsage,
10693
10774
  awaitGitReady,
10694
10775
  uploadSnapshotToGcs,
@@ -10706,4 +10787,4 @@ export {
10706
10787
  runStartCommand,
10707
10788
  unshallowRepo
10708
10789
  };
10709
- //# sourceMappingURL=chunk-53ZLFIAI.js.map
10790
+ //# sourceMappingURL=chunk-DEMRCBJN.js.map