@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.
- package/dist/chunk-7TQO4ZF4.js +60 -0
- package/dist/chunk-7TQO4ZF4.js.map +1 -0
- package/dist/{chunk-53ZLFIAI.js → chunk-DEMRCBJN.js} +940 -859
- package/dist/chunk-DEMRCBJN.js.map +1 -0
- package/dist/cli.js +87 -5
- package/dist/cli.js.map +1 -1
- package/dist/heartbeat-worker.d.ts +2 -0
- package/dist/heartbeat-worker.js +47 -0
- package/dist/heartbeat-worker.js.map +1 -0
- package/dist/index.d.ts +50 -42
- package/dist/index.js +19 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-53ZLFIAI.js.map +0 -1
|
@@ -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(
|
|
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 {
|
|
1127
|
+
import { execFile } from "child_process";
|
|
1044
1128
|
import { realpathSync } from "fs";
|
|
1045
1129
|
import { promisify } from "util";
|
|
1046
|
-
var
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1125
|
-
cwd,
|
|
1126
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1127
|
-
});
|
|
1209
|
+
await git(cwd, ["rev-parse", `origin/${currentBranch}`]);
|
|
1128
1210
|
} catch {
|
|
1129
1211
|
try {
|
|
1130
|
-
|
|
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 =
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1148
|
-
if (!hasUncommittedChanges(cwd)) return null;
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
|
1243
|
+
await git(cwd, ["push", ...noVerify, "origin", branch], 3e4);
|
|
1162
1244
|
return true;
|
|
1163
1245
|
} catch {
|
|
1164
1246
|
try {
|
|
1165
|
-
await
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
|
|
1208
|
-
const sha =
|
|
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
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
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 =
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
1336
|
+
await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
|
|
1272
1337
|
return "applied";
|
|
1273
1338
|
} catch {
|
|
1274
1339
|
try {
|
|
1275
|
-
|
|
1340
|
+
await git(cwd, ["reset", "--merge"], GIT_SLOW_TIMEOUT_MS);
|
|
1276
1341
|
} catch {
|
|
1277
1342
|
}
|
|
1278
1343
|
return "stale";
|
|
1279
1344
|
}
|
|
1280
1345
|
}
|
|
1281
|
-
|
|
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 =
|
|
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
|
-
|
|
1382
|
-
|
|
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 =
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
)
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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 {
|
|
1915
|
-
import { createReadStream, createWriteStream
|
|
1916
|
-
import {
|
|
1917
|
-
import { tmpdir
|
|
1918
|
-
import
|
|
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(
|
|
1925
|
-
const include =
|
|
1583
|
+
function planSnapshotFiles(git2) {
|
|
1584
|
+
const include = git2.trackedAndUntracked();
|
|
1926
1585
|
const includeSet = new Set(include);
|
|
1927
|
-
const deletions = [...new Set(
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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: () =>
|
|
1970
|
-
deletedSinceHead: () =>
|
|
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
|
-
|
|
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
|
|
1981
|
-
const staging = await mkdtemp(
|
|
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
|
|
1656
|
+
await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
|
|
1998
1657
|
const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
|
|
1999
|
-
|
|
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 =
|
|
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 ||
|
|
2027
|
-
const root =
|
|
2028
|
-
const abs =
|
|
2029
|
-
return abs !== root && abs.startsWith(root +
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
|
2093
|
-
const
|
|
2094
|
-
if (!
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
|
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
|
|
2129
|
-
import
|
|
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 =
|
|
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
|
|
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
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
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
|
|
3337
|
-
import { join as
|
|
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
|
-
|
|
3756
|
-
|
|
3757
|
-
const
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
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
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
4019
|
+
function isRecord3(value) {
|
|
4033
4020
|
return typeof value === "object" && value !== null;
|
|
4034
4021
|
}
|
|
4035
4022
|
function extractSpawn(mod) {
|
|
4036
|
-
if (!
|
|
4023
|
+
if (!isRecord3(mod)) return null;
|
|
4037
4024
|
if (typeof mod.spawn === "function") return mod.spawn;
|
|
4038
4025
|
const def = mod.default;
|
|
4039
|
-
if (
|
|
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 (!
|
|
4074
|
+
if (!isRecord3(entry)) continue;
|
|
4078
4075
|
if (typeof entry.question !== "string") continue;
|
|
4079
|
-
const options = Array.isArray(entry.options) ? entry.options.filter(
|
|
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(
|
|
4328
|
-
const socketPath =
|
|
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
|
-
|
|
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 :
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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,
|
|
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\`,
|
|
5783
|
-
`1. \`bun run
|
|
5784
|
-
`2. \`bun run
|
|
5785
|
-
`
|
|
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
|
|
5892
|
-
`-
|
|
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,
|
|
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-
|
|
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
|
|
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 :
|
|
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
|
|
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 (!
|
|
8385
|
-
const content =
|
|
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 (
|
|
8608
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9240
|
-
import { dirname as dirname2, join as
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
9542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10223
|
-
*
|
|
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
|
-
*
|
|
10227
|
-
* post-turn, so the TUI is no longer
|
|
10228
|
-
*
|
|
10229
|
-
*
|
|
10230
|
-
*
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
|
10377
|
+
await this.callbacks.onEvent({ type: "pr_nudge", prompt });
|
|
10290
10378
|
if (this.interrupted || this.stopped) return;
|
|
10291
|
-
await this.executeQuery(
|
|
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
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
10435
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
10744
|
+
import { execSync as execSync2 } from "child_process";
|
|
10663
10745
|
function unshallowRepo(workspaceDir) {
|
|
10664
10746
|
try {
|
|
10665
|
-
|
|
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-
|
|
10790
|
+
//# sourceMappingURL=chunk-DEMRCBJN.js.map
|