@rallycry/conveyor-agent 10.2.2 → 10.2.5
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-CFELRV35.js} +1405 -1233
- package/dist/chunk-CFELRV35.js.map +1 -0
- package/dist/cli.js +209 -10
- 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/runtime/entrypoint.sh +93 -4
- 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,22 +1438,17 @@ 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;
|
|
@@ -1408,9 +1465,9 @@ async function flushAllPendingWork(cwd, opts) {
|
|
|
1408
1465
|
try {
|
|
1409
1466
|
const primary = await flushPendingChanges(cwd, opts);
|
|
1410
1467
|
await refreshRemoteToken(cwd, opts?.refreshToken);
|
|
1411
|
-
const currentBranch = getCurrentBranch(cwd);
|
|
1412
|
-
const worktreesSnapshotted = snapshotOtherWorktrees(cwd, opts?.wipMessage);
|
|
1413
|
-
const branchesBackedUp = backupOtherBranches(cwd, currentBranch);
|
|
1468
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
1469
|
+
const worktreesSnapshotted = await snapshotOtherWorktrees(cwd, opts?.wipMessage);
|
|
1470
|
+
const branchesBackedUp = await backupOtherBranches(cwd, currentBranch);
|
|
1414
1471
|
return {
|
|
1415
1472
|
hadWork: primary.hadWork || worktreesSnapshotted > 0 || branchesBackedUp > 0,
|
|
1416
1473
|
branchesBackedUp,
|
|
@@ -1420,14 +1477,14 @@ async function flushAllPendingWork(cwd, opts) {
|
|
|
1420
1477
|
return { hadWork: false, branchesBackedUp: 0, worktreesSnapshotted: 0 };
|
|
1421
1478
|
}
|
|
1422
1479
|
}
|
|
1423
|
-
function snapshotOtherWorktrees(cwd, wipMessage) {
|
|
1480
|
+
async function snapshotOtherWorktrees(cwd, wipMessage) {
|
|
1424
1481
|
let count = 0;
|
|
1425
|
-
for (const wt of listWorktrees(cwd)) {
|
|
1482
|
+
for (const wt of await listWorktrees(cwd)) {
|
|
1426
1483
|
if (samePath(wt.path, cwd) || !wt.branch) continue;
|
|
1427
1484
|
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)) {
|
|
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)) {
|
|
1431
1488
|
wipRefPushed.add(wt.path);
|
|
1432
1489
|
count++;
|
|
1433
1490
|
}
|
|
@@ -1436,13 +1493,17 @@ function snapshotOtherWorktrees(cwd, wipMessage) {
|
|
|
1436
1493
|
}
|
|
1437
1494
|
return count;
|
|
1438
1495
|
}
|
|
1439
|
-
function backupOtherBranches(cwd, currentBranch) {
|
|
1496
|
+
async function backupOtherBranches(cwd, currentBranch) {
|
|
1440
1497
|
let count = 0;
|
|
1441
|
-
for (const branch of listLocalBranches(cwd)) {
|
|
1498
|
+
for (const branch of await listLocalBranches(cwd)) {
|
|
1442
1499
|
if (branch === currentBranch || branch.startsWith("conveyor-wip/")) continue;
|
|
1443
1500
|
try {
|
|
1444
|
-
if (branchUnpushedCount(cwd, branch) === 0) continue;
|
|
1445
|
-
if (tryPushRefspec(
|
|
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
|
+
)) {
|
|
1446
1507
|
count++;
|
|
1447
1508
|
}
|
|
1448
1509
|
} catch {
|
|
@@ -1451,707 +1512,267 @@ function backupOtherBranches(cwd, currentBranch) {
|
|
|
1451
1512
|
return count;
|
|
1452
1513
|
}
|
|
1453
1514
|
|
|
1454
|
-
// src/
|
|
1455
|
-
import {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1515
|
+
// src/setup/git-ready.ts
|
|
1516
|
+
import { access, stat } from "fs/promises";
|
|
1517
|
+
var DEFAULT_FAILED_PATH = "/workspaces/.conveyor-git-failed";
|
|
1518
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1519
|
+
var DEFAULT_POLL_MS = 200;
|
|
1520
|
+
async function fileExists(path4) {
|
|
1521
|
+
try {
|
|
1522
|
+
await access(path4);
|
|
1523
|
+
const s = await stat(path4);
|
|
1524
|
+
return s.isFile();
|
|
1525
|
+
} catch {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1464
1528
|
}
|
|
1465
|
-
function
|
|
1466
|
-
|
|
1529
|
+
function awaitGitReady(opts = {}) {
|
|
1530
|
+
const markerPath = opts.markerPath ?? process.env.CONVEYOR_GIT_READY_MARKER;
|
|
1531
|
+
if (!markerPath) {
|
|
1532
|
+
return Promise.resolve("not-gated");
|
|
1533
|
+
}
|
|
1534
|
+
const failedPath = opts.failedPath ?? process.env.CONVEYOR_GIT_FAILED_MARKER ?? DEFAULT_FAILED_PATH;
|
|
1535
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1536
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
1537
|
+
opts.onLog?.(`waiting for workspace git (marker=${markerPath})`);
|
|
1538
|
+
return pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, opts.onLog);
|
|
1467
1539
|
}
|
|
1468
|
-
function
|
|
1469
|
-
|
|
1540
|
+
async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog) {
|
|
1541
|
+
const deadline = Date.now() + timeoutMs;
|
|
1542
|
+
for (; ; ) {
|
|
1543
|
+
if (await fileExists(markerPath)) {
|
|
1544
|
+
onLog?.("workspace git ready");
|
|
1545
|
+
return "ready";
|
|
1546
|
+
}
|
|
1547
|
+
if (await fileExists(failedPath)) {
|
|
1548
|
+
onLog?.("workspace git preparation failed (marker present)");
|
|
1549
|
+
return "failed";
|
|
1550
|
+
}
|
|
1551
|
+
if (Date.now() >= deadline) {
|
|
1552
|
+
onLog?.(`workspace git not ready after ${timeoutMs}ms \u2014 giving up`);
|
|
1553
|
+
return "timeout";
|
|
1554
|
+
}
|
|
1555
|
+
await new Promise((resolve) => {
|
|
1556
|
+
setTimeout(resolve, pollMs);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1470
1559
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1560
|
+
|
|
1561
|
+
// src/runner/work-preservation/index.ts
|
|
1562
|
+
import { rm as rm3 } from "fs/promises";
|
|
1563
|
+
|
|
1564
|
+
// src/runner/work-preservation/restore-precedence.ts
|
|
1565
|
+
function selectRestoreSource(probe) {
|
|
1566
|
+
if (probe.gcsAvailable) return "gcs";
|
|
1567
|
+
if (probe.wipRestoreResult === "applied") return "wip";
|
|
1568
|
+
return "clean";
|
|
1473
1569
|
}
|
|
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
1570
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1571
|
+
// src/runner/work-preservation/snapshot-tar.ts
|
|
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";
|
|
1577
|
+
import { pipeline } from "stream/promises";
|
|
1578
|
+
import { promisify as promisify2 } from "util";
|
|
1579
|
+
import { createGzip } from "zlib";
|
|
1580
|
+
import * as tar from "tar";
|
|
1499
1581
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1582
|
+
// src/runner/work-preservation/snapshot-artifact.ts
|
|
1583
|
+
function planSnapshotFiles(git2) {
|
|
1584
|
+
const include = git2.trackedAndUntracked();
|
|
1585
|
+
const includeSet = new Set(include);
|
|
1586
|
+
const deletions = [...new Set(git2.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
|
|
1587
|
+
return { include, deletions };
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/runner/work-preservation/snapshot-tar.ts
|
|
1591
|
+
var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
|
|
1592
|
+
var LEGACY_PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
|
|
1593
|
+
var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1594
|
+
function parseStatusPorcelainZ(raw) {
|
|
1595
|
+
const includes = [];
|
|
1596
|
+
const deletions = [];
|
|
1597
|
+
const tokens = raw.split("\0");
|
|
1598
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1599
|
+
const token = tokens[i];
|
|
1600
|
+
if (!token || token.length < 4) continue;
|
|
1601
|
+
const x = token[0];
|
|
1602
|
+
const y = token[1];
|
|
1603
|
+
const filePath = token.slice(3);
|
|
1604
|
+
if (x === "R" || x === "C") {
|
|
1605
|
+
const origPath = tokens[++i];
|
|
1606
|
+
if (x === "R" && origPath) deletions.push(origPath);
|
|
1607
|
+
}
|
|
1608
|
+
if (x === "!") continue;
|
|
1609
|
+
const missingFromWorktree = y === "D" || x === "D" && y === " ";
|
|
1610
|
+
if (missingFromWorktree) deletions.push(filePath);
|
|
1611
|
+
else includes.push(filePath);
|
|
1612
|
+
}
|
|
1613
|
+
return { includes, deletions };
|
|
1614
|
+
}
|
|
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());
|
|
1624
|
+
return {
|
|
1625
|
+
trackedAndUntracked: () => cached.includes,
|
|
1626
|
+
deletedSinceHead: () => cached.deletions
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
async function gitHead(cwd) {
|
|
1507
1630
|
try {
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1631
|
+
const { stdout } = await execFileAsync2("git", ["rev-parse", "HEAD"], {
|
|
1632
|
+
cwd,
|
|
1633
|
+
timeout: GIT_TIMEOUT_MS2
|
|
1634
|
+
});
|
|
1635
|
+
return stdout.toString().trim() || null;
|
|
1510
1636
|
} catch {
|
|
1511
|
-
|
|
1512
|
-
}
|
|
1513
|
-
if (payload.hook_event_name === "PreToolUse") {
|
|
1514
|
-
preToolUse(payload);
|
|
1515
|
-
} else {
|
|
1516
|
-
relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
|
|
1637
|
+
return null;
|
|
1517
1638
|
}
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (done) return;
|
|
1525
|
-
done = true;
|
|
1526
|
-
process.stdout.write(JSON.stringify({ continue: true }));
|
|
1527
|
-
process.exit(0);
|
|
1639
|
+
}
|
|
1640
|
+
async function buildSnapshotTar(cwd) {
|
|
1641
|
+
const staging = await mkdtemp(path.join(tmpdir(), "conveyor-snapshot-"));
|
|
1642
|
+
const cleanup = async () => {
|
|
1643
|
+
await rm(staging, { recursive: true, force: true }).catch(() => {
|
|
1644
|
+
});
|
|
1528
1645
|
};
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1646
|
+
try {
|
|
1647
|
+
const plan = planSnapshotFiles(await realGitSurface(cwd));
|
|
1648
|
+
const head = await gitHead(cwd);
|
|
1649
|
+
if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
|
|
1650
|
+
const capturedAt = Date.now();
|
|
1651
|
+
const manifest = {
|
|
1652
|
+
head,
|
|
1653
|
+
deletions: plan.deletions,
|
|
1654
|
+
capturedAt
|
|
1655
|
+
};
|
|
1656
|
+
await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
|
|
1657
|
+
const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
|
|
1658
|
+
const rawTarPath = path.join(staging, "snapshot.tar");
|
|
1659
|
+
await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
|
|
1660
|
+
if (plan.include.length > 0) {
|
|
1661
|
+
await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
|
|
1662
|
+
}
|
|
1663
|
+
const tarPath = path.join(staging, "snapshot.tar.gz");
|
|
1664
|
+
await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
|
|
1665
|
+
await rm(rawTarPath, { force: true });
|
|
1666
|
+
const { size } = await stat2(tarPath);
|
|
1667
|
+
return {
|
|
1668
|
+
tarPath,
|
|
1669
|
+
sizeBytes: size,
|
|
1670
|
+
fileCount: plan.include.length,
|
|
1671
|
+
deletionCount: plan.deletions.length,
|
|
1672
|
+
capturedAt,
|
|
1673
|
+
cleanup
|
|
1674
|
+
};
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
await cleanup();
|
|
1677
|
+
throw err;
|
|
1534
1678
|
}
|
|
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
1679
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
});
|
|
1680
|
+
function isWithinWorkspace(cwd, rel) {
|
|
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);
|
|
1637
1685
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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)}` }]
|
|
1686
|
+
async function readSnapshotArchive(tarPath) {
|
|
1687
|
+
let manifestRaw = null;
|
|
1688
|
+
try {
|
|
1689
|
+
await tar.list({
|
|
1690
|
+
file: tarPath,
|
|
1691
|
+
onReadEntry: (entry) => {
|
|
1692
|
+
if (entry.path === SNAPSHOT_MANIFEST_NAME) {
|
|
1693
|
+
const chunks = [];
|
|
1694
|
+
entry.on("data", (chunk) => chunks.push(chunk));
|
|
1695
|
+
entry.on("end", () => {
|
|
1696
|
+
manifestRaw = Buffer.concat(chunks);
|
|
1697
|
+
});
|
|
1695
1698
|
}
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
|
|
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;
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
} catch {
|
|
1702
|
+
return null;
|
|
1715
1703
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1704
|
+
if (!manifestRaw) return null;
|
|
1705
|
+
try {
|
|
1706
|
+
const parsed = JSON.parse(manifestRaw.toString("utf8"));
|
|
1707
|
+
if (typeof parsed.head !== "string" || !parsed.head) return null;
|
|
1708
|
+
return {
|
|
1709
|
+
head: parsed.head,
|
|
1710
|
+
deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
|
|
1711
|
+
capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0
|
|
1712
|
+
};
|
|
1713
|
+
} catch {
|
|
1714
|
+
return null;
|
|
1718
1715
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
-
}
|
|
1716
|
+
}
|
|
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 };
|
|
1736
1723
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
}
|
|
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;
|
|
1758
1731
|
}
|
|
1732
|
+
filesExtracted++;
|
|
1733
|
+
return true;
|
|
1759
1734
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
if (isInitialDetection) {
|
|
1767
|
-
this.lockedPlanFile = target;
|
|
1768
|
-
}
|
|
1769
|
-
const fileName = target.split("/").pop() ?? "plan";
|
|
1770
|
-
let content;
|
|
1771
|
-
try {
|
|
1772
|
-
content = readFileSync(target, "utf-8").trim();
|
|
1773
|
-
} catch (err) {
|
|
1774
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1775
|
-
this.connection.postChatMessage(
|
|
1776
|
-
`Plan sync warning: could not read local plan file (${fileName}): ${reason}. The task plan was not updated.`
|
|
1777
|
-
);
|
|
1778
|
-
return;
|
|
1779
|
-
}
|
|
1780
|
-
if (!content) return;
|
|
1781
|
-
const result = await this.connection.updateTaskFields({ plan: content });
|
|
1782
|
-
if (!result.ok) {
|
|
1783
|
-
this.connection.postChatMessage(
|
|
1784
|
-
`Plan sync failed: the local plan file (${fileName}) was not persisted to the task (${result.error ?? "unknown error"}). Retry update_task_plan or re-run ExitPlanMode.`
|
|
1785
|
-
);
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
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}`);
|
|
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++;
|
|
1791
1741
|
}
|
|
1792
|
-
};
|
|
1742
|
+
return { status: "extracted", filesExtracted, deletionsApplied };
|
|
1743
|
+
}
|
|
1793
1744
|
|
|
1794
|
-
// src/
|
|
1795
|
-
import {
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1745
|
+
// src/runner/work-preservation/snapshot-transfer.ts
|
|
1746
|
+
import { randomUUID } from "crypto";
|
|
1747
|
+
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
|
|
1748
|
+
import { rm as rm2 } from "fs/promises";
|
|
1749
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1750
|
+
import path2 from "path";
|
|
1751
|
+
import { Readable } from "stream";
|
|
1752
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
1753
|
+
var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
|
|
1754
|
+
var SNAPSHOT_CAPTURED_AT_HEADER = "x-snapshot-captured-at";
|
|
1755
|
+
async function putSnapshotToGcs(url, tarResult) {
|
|
1800
1756
|
try {
|
|
1801
|
-
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1815
|
-
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
1816
|
-
opts.onLog?.(`waiting for workspace git (marker=${markerPath})`);
|
|
1817
|
-
return pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, opts.onLog);
|
|
1818
|
-
}
|
|
1819
|
-
async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog) {
|
|
1820
|
-
const deadline = Date.now() + timeoutMs;
|
|
1821
|
-
for (; ; ) {
|
|
1822
|
-
if (await fileExists(markerPath)) {
|
|
1823
|
-
onLog?.("workspace git ready");
|
|
1824
|
-
return "ready";
|
|
1825
|
-
}
|
|
1826
|
-
if (await fileExists(failedPath)) {
|
|
1827
|
-
onLog?.("workspace git preparation failed (marker present)");
|
|
1828
|
-
return "failed";
|
|
1829
|
-
}
|
|
1830
|
-
if (Date.now() >= deadline) {
|
|
1831
|
-
onLog?.(`workspace git not ready after ${timeoutMs}ms \u2014 giving up`);
|
|
1832
|
-
return "timeout";
|
|
1833
|
-
}
|
|
1834
|
-
await new Promise((resolve) => {
|
|
1835
|
-
setTimeout(resolve, pollMs);
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
// src/runner/work-preservation/index.ts
|
|
1841
|
-
import { rm as rm3 } from "fs/promises";
|
|
1842
|
-
|
|
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
|
-
// src/runner/work-preservation/restore-precedence.ts
|
|
1907
|
-
function selectRestoreSource(probe) {
|
|
1908
|
-
if (probe.gcsAvailable) return "gcs";
|
|
1909
|
-
if (probe.wipRestoreResult === "applied") return "wip";
|
|
1910
|
-
return "clean";
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
// src/runner/work-preservation/snapshot-tar.ts
|
|
1914
|
-
import { execSync as execSync3 } from "child_process";
|
|
1915
|
-
import { createReadStream, createWriteStream, mkdtempSync } from "fs";
|
|
1916
|
-
import { copyFile, mkdtemp, rm, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1917
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
1918
|
-
import path2 from "path";
|
|
1919
|
-
import { pipeline } from "stream/promises";
|
|
1920
|
-
import { createGzip } from "zlib";
|
|
1921
|
-
import * as tar from "tar";
|
|
1922
|
-
|
|
1923
|
-
// src/runner/work-preservation/snapshot-artifact.ts
|
|
1924
|
-
function planSnapshotFiles(git) {
|
|
1925
|
-
const include = git.trackedAndUntracked();
|
|
1926
|
-
const includeSet = new Set(include);
|
|
1927
|
-
const deletions = [...new Set(git.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
|
|
1928
|
-
return { include, deletions };
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
// src/runner/work-preservation/snapshot-tar.ts
|
|
1932
|
-
var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
|
|
1933
|
-
var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1934
|
-
function parseStatusPorcelainZ(raw) {
|
|
1935
|
-
const includes = [];
|
|
1936
|
-
const deletions = [];
|
|
1937
|
-
const tokens = raw.split("\0");
|
|
1938
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
1939
|
-
const token = tokens[i];
|
|
1940
|
-
if (!token || token.length < 4) continue;
|
|
1941
|
-
const x = token[0];
|
|
1942
|
-
const y = token[1];
|
|
1943
|
-
const filePath = token.slice(3);
|
|
1944
|
-
if (x === "R" || x === "C") {
|
|
1945
|
-
const origPath = tokens[++i];
|
|
1946
|
-
if (x === "R" && origPath) deletions.push(origPath);
|
|
1947
|
-
}
|
|
1948
|
-
if (x === "!") continue;
|
|
1949
|
-
const missingFromWorktree = y === "D" || x === "D" && y === " ";
|
|
1950
|
-
if (missingFromWorktree) deletions.push(filePath);
|
|
1951
|
-
else includes.push(filePath);
|
|
1952
|
-
}
|
|
1953
|
-
return { includes, deletions };
|
|
1954
|
-
}
|
|
1955
|
-
function realGitSurface(cwd) {
|
|
1956
|
-
let cached = null;
|
|
1957
|
-
const load = () => {
|
|
1958
|
-
if (!cached) {
|
|
1959
|
-
const out = execSync3("git status --porcelain=v1 -z -uall", {
|
|
1960
|
-
cwd,
|
|
1961
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1962
|
-
maxBuffer: STATUS_MAX_BUFFER
|
|
1963
|
-
}).toString("utf8");
|
|
1964
|
-
cached = parseStatusPorcelainZ(out);
|
|
1965
|
-
}
|
|
1966
|
-
return cached;
|
|
1967
|
-
};
|
|
1968
|
-
return {
|
|
1969
|
-
trackedAndUntracked: () => load().includes,
|
|
1970
|
-
deletedSinceHead: () => load().deletions
|
|
1971
|
-
};
|
|
1972
|
-
}
|
|
1973
|
-
function gitHead(cwd) {
|
|
1974
|
-
try {
|
|
1975
|
-
return execSync3("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
|
|
1976
|
-
} catch {
|
|
1977
|
-
return null;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
async function buildSnapshotTar(cwd, opts) {
|
|
1981
|
-
const staging = await mkdtemp(path2.join(tmpdir2(), "conveyor-snapshot-"));
|
|
1982
|
-
const cleanup = async () => {
|
|
1983
|
-
await rm(staging, { recursive: true, force: true }).catch(() => {
|
|
1984
|
-
});
|
|
1985
|
-
};
|
|
1986
|
-
try {
|
|
1987
|
-
const plan = planSnapshotFiles(realGitSurface(cwd));
|
|
1988
|
-
const head = gitHead(cwd);
|
|
1989
|
-
if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
|
|
1990
|
-
const capturedAt = Date.now();
|
|
1991
|
-
const manifest = {
|
|
1992
|
-
head,
|
|
1993
|
-
deletions: plan.deletions,
|
|
1994
|
-
capturedAt,
|
|
1995
|
-
pgDump: Boolean(opts?.pgDumpPath)
|
|
1996
|
-
};
|
|
1997
|
-
await writeFile2(path2.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
|
|
1998
|
-
const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
|
|
1999
|
-
if (opts?.pgDumpPath) {
|
|
2000
|
-
await copyFile(opts.pgDumpPath, path2.join(staging, PG_DUMP_FILENAME));
|
|
2001
|
-
stagedEntries.push(PG_DUMP_FILENAME);
|
|
2002
|
-
}
|
|
2003
|
-
const rawTarPath = path2.join(staging, "snapshot.tar");
|
|
2004
|
-
await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
|
|
2005
|
-
if (plan.include.length > 0) {
|
|
2006
|
-
await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
|
|
2007
|
-
}
|
|
2008
|
-
const tarPath = path2.join(staging, "snapshot.tar.gz");
|
|
2009
|
-
await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
|
|
2010
|
-
await rm(rawTarPath, { force: true });
|
|
2011
|
-
const { size } = await stat2(tarPath);
|
|
2012
|
-
return {
|
|
2013
|
-
tarPath,
|
|
2014
|
-
sizeBytes: size,
|
|
2015
|
-
fileCount: plan.include.length,
|
|
2016
|
-
deletionCount: plan.deletions.length,
|
|
2017
|
-
capturedAt,
|
|
2018
|
-
cleanup
|
|
2019
|
-
};
|
|
2020
|
-
} catch (err) {
|
|
2021
|
-
await cleanup();
|
|
2022
|
-
throw err;
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
function isWithinWorkspace(cwd, rel) {
|
|
2026
|
-
if (!rel || path2.isAbsolute(rel)) return false;
|
|
2027
|
-
const root = path2.resolve(cwd);
|
|
2028
|
-
const abs = path2.resolve(root, rel);
|
|
2029
|
-
return abs !== root && abs.startsWith(root + path2.sep);
|
|
2030
|
-
}
|
|
2031
|
-
async function readSnapshotArchive(tarPath) {
|
|
2032
|
-
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
|
-
try {
|
|
2041
|
-
await tar.list({
|
|
2042
|
-
file: tarPath,
|
|
2043
|
-
onReadEntry: (entry) => {
|
|
2044
|
-
if (entry.path === SNAPSHOT_MANIFEST_NAME) {
|
|
2045
|
-
const chunks = [];
|
|
2046
|
-
entry.on("data", (chunk) => chunks.push(chunk));
|
|
2047
|
-
entry.on("end", () => {
|
|
2048
|
-
manifestRaw = Buffer.concat(chunks);
|
|
2049
|
-
});
|
|
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
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
});
|
|
2062
|
-
await pgDumpWritten;
|
|
2063
|
-
} catch {
|
|
2064
|
-
await cleanup();
|
|
2065
|
-
return null;
|
|
2066
|
-
}
|
|
2067
|
-
if (!manifestRaw) {
|
|
2068
|
-
await cleanup();
|
|
2069
|
-
return null;
|
|
2070
|
-
}
|
|
2071
|
-
try {
|
|
2072
|
-
const parsed = JSON.parse(manifestRaw.toString("utf8"));
|
|
2073
|
-
if (typeof parsed.head !== "string" || !parsed.head) {
|
|
2074
|
-
await cleanup();
|
|
2075
|
-
return null;
|
|
2076
|
-
}
|
|
2077
|
-
return {
|
|
2078
|
-
manifest: {
|
|
2079
|
-
head: parsed.head,
|
|
2080
|
-
deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
|
|
2081
|
-
capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0,
|
|
2082
|
-
pgDump: parsed.pgDump === true
|
|
2083
|
-
},
|
|
2084
|
-
...pgDumpPath ? { pgDumpPath } : {},
|
|
2085
|
-
cleanup
|
|
2086
|
-
};
|
|
2087
|
-
} catch {
|
|
2088
|
-
await cleanup();
|
|
2089
|
-
return null;
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
async function extractSnapshotTar(cwd, tarPath, opts) {
|
|
2093
|
-
const read = await readSnapshotArchive(tarPath);
|
|
2094
|
-
if (!read) return { status: "invalid" };
|
|
2095
|
-
try {
|
|
2096
|
-
const { manifest, pgDumpPath } = read;
|
|
2097
|
-
const currentHead = gitHead(cwd);
|
|
2098
|
-
if (manifest.head !== currentHead) {
|
|
2099
|
-
return { status: "stale-head", snapshotHead: manifest.head, currentHead };
|
|
2100
|
-
}
|
|
2101
|
-
let filesExtracted = 0;
|
|
2102
|
-
await tar.extract({
|
|
2103
|
-
file: tarPath,
|
|
2104
|
-
cwd,
|
|
2105
|
-
filter: (entryPath) => {
|
|
2106
|
-
if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === PG_DUMP_FILENAME) return false;
|
|
2107
|
-
filesExtracted++;
|
|
2108
|
-
return true;
|
|
2109
|
-
}
|
|
2110
|
-
});
|
|
2111
|
-
let deletionsApplied = 0;
|
|
2112
|
-
for (const rel of manifest.deletions) {
|
|
2113
|
-
if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
|
|
2114
|
-
await rm(path2.resolve(cwd, rel), { recursive: true, force: true });
|
|
2115
|
-
deletionsApplied++;
|
|
2116
|
-
}
|
|
2117
|
-
const pgDumpApplied = manifest.pgDump && pgDumpPath ? applyPgDumpIfConfigured(pgDumpPath, opts?.pgExec) : false;
|
|
2118
|
-
return { status: "extracted", filesExtracted, deletionsApplied, pgDumpApplied };
|
|
2119
|
-
} finally {
|
|
2120
|
-
await read.cleanup();
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
// src/runner/work-preservation/snapshot-transfer.ts
|
|
2125
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2126
|
-
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
|
|
2127
|
-
import { rm as rm2 } from "fs/promises";
|
|
2128
|
-
import { tmpdir as tmpdir3 } from "os";
|
|
2129
|
-
import path3 from "path";
|
|
2130
|
-
import { Readable } from "stream";
|
|
2131
|
-
import { pipeline as pipeline2 } from "stream/promises";
|
|
2132
|
-
var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
|
|
2133
|
-
var SNAPSHOT_CAPTURED_AT_HEADER = "x-snapshot-captured-at";
|
|
2134
|
-
async function putSnapshotToGcs(url, tarResult) {
|
|
2135
|
-
try {
|
|
2136
|
-
const body = Readable.toWeb(createReadStream2(tarResult.tarPath));
|
|
2137
|
-
const res = await fetch(url, {
|
|
2138
|
-
method: "PUT",
|
|
2139
|
-
headers: {
|
|
2140
|
-
"Content-Type": "application/gzip",
|
|
2141
|
-
"Content-Length": String(tarResult.sizeBytes),
|
|
2142
|
-
[SNAPSHOT_CAPTURED_AT_HEADER]: String(tarResult.capturedAt)
|
|
2143
|
-
},
|
|
2144
|
-
body,
|
|
2145
|
-
duplex: "half",
|
|
2146
|
-
signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
|
|
2147
|
-
});
|
|
2148
|
-
return res.ok;
|
|
1757
|
+
const body = Readable.toWeb(createReadStream2(tarResult.tarPath));
|
|
1758
|
+
const res = await fetch(url, {
|
|
1759
|
+
method: "PUT",
|
|
1760
|
+
headers: {
|
|
1761
|
+
"Content-Type": "application/gzip",
|
|
1762
|
+
"Content-Length": String(tarResult.sizeBytes),
|
|
1763
|
+
[SNAPSHOT_CAPTURED_AT_HEADER]: String(tarResult.capturedAt)
|
|
1764
|
+
},
|
|
1765
|
+
body,
|
|
1766
|
+
duplex: "half",
|
|
1767
|
+
signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
|
|
1768
|
+
});
|
|
1769
|
+
return res.ok;
|
|
2149
1770
|
} catch {
|
|
2150
1771
|
return false;
|
|
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
|
}
|
|
@@ -2300,6 +1909,7 @@ async function restoreOnBoot(bundle, cwd) {
|
|
|
2300
1909
|
import { z } from "zod";
|
|
2301
1910
|
import { z as z2 } from "zod";
|
|
2302
1911
|
var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
|
|
1912
|
+
var TUI_KINDS = ["claude-code", "opencode"];
|
|
2303
1913
|
var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
|
|
2304
1914
|
var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
|
|
2305
1915
|
var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
|
|
@@ -2308,7 +1918,9 @@ var AgentHeartbeatSchema = z.object({
|
|
|
2308
1918
|
sessionId: z.string().optional(),
|
|
2309
1919
|
timestamp: z.string(),
|
|
2310
1920
|
status: z.enum(["active", "idle", "building"]),
|
|
2311
|
-
currentAction: z.string().optional()
|
|
1921
|
+
currentAction: z.string().optional(),
|
|
1922
|
+
/** Sender-observed main event-loop lag (ms) — see AgentHeartbeat.loopLagMs. */
|
|
1923
|
+
loopLagMs: z.number().nonnegative().optional()
|
|
2312
1924
|
});
|
|
2313
1925
|
var CreatePRInputSchema = z.object({
|
|
2314
1926
|
title: z.string().min(1),
|
|
@@ -2590,6 +2202,10 @@ var ListActivePtySessionsRequestSchema = z.object({
|
|
|
2590
2202
|
var SendSoftStopRequestSchema = z.object({
|
|
2591
2203
|
taskId: z.string()
|
|
2592
2204
|
});
|
|
2205
|
+
var StopTaskSessionRequestSchema = z.object({
|
|
2206
|
+
taskId: z.string(),
|
|
2207
|
+
sessionId: z.string()
|
|
2208
|
+
});
|
|
2593
2209
|
var FlushTaskQueueRequestSchema = z.object({
|
|
2594
2210
|
taskId: z.string(),
|
|
2595
2211
|
softStop: z.boolean().optional()
|
|
@@ -2669,6 +2285,29 @@ var PtyResizeRequestSchema = z.object({
|
|
|
2669
2285
|
var PtyAttachRequestSchema = z.object({
|
|
2670
2286
|
sessionId: z.string()
|
|
2671
2287
|
});
|
|
2288
|
+
var PtyChatEventPayloadSchema = z.discriminatedUnion("kind", [
|
|
2289
|
+
z.object({
|
|
2290
|
+
kind: z.literal("init"),
|
|
2291
|
+
model: z.string().max(200),
|
|
2292
|
+
claudeSessionId: z.string().max(100).optional()
|
|
2293
|
+
}),
|
|
2294
|
+
z.object({ kind: z.literal("user_text"), text: z.string().max(16384) }),
|
|
2295
|
+
z.object({ kind: z.literal("assistant_text"), text: z.string().max(16384) }),
|
|
2296
|
+
z.object({
|
|
2297
|
+
kind: z.literal("tool_use"),
|
|
2298
|
+
name: z.string().max(200),
|
|
2299
|
+
// Compact preview: JSON.stringify(input) truncated agent-side.
|
|
2300
|
+
input: z.string().max(2e3)
|
|
2301
|
+
}),
|
|
2302
|
+
z.object({ kind: z.literal("turn_end") })
|
|
2303
|
+
]);
|
|
2304
|
+
var PtyChatEventRequestSchema = z.object({
|
|
2305
|
+
sessionId: z.string(),
|
|
2306
|
+
event: PtyChatEventPayloadSchema
|
|
2307
|
+
});
|
|
2308
|
+
var PtyChatAttachRequestSchema = z.object({
|
|
2309
|
+
sessionId: z.string()
|
|
2310
|
+
});
|
|
2672
2311
|
var CreatePRResponseSchema = z.object({
|
|
2673
2312
|
prNumber: z.number().int().positive(),
|
|
2674
2313
|
prUrl: z.string().url()
|
|
@@ -2806,9 +2445,21 @@ var ListMyLiveSessionsRequestSchema = z2.object({
|
|
|
2806
2445
|
/** Admin-only: list another member's sessions instead of the caller's. */
|
|
2807
2446
|
targetUserId: z2.string().optional()
|
|
2808
2447
|
});
|
|
2448
|
+
var GetProjectAvailableTuisRequestSchema = z2.object({
|
|
2449
|
+
projectId: z2.string()
|
|
2450
|
+
});
|
|
2809
2451
|
var StartAdhocSessionRequestSchema = z2.object({
|
|
2810
2452
|
projectId: z2.string(),
|
|
2811
2453
|
label: z2.string().max(200).optional(),
|
|
2454
|
+
/** Coding-agent key to launch under — validated pick-time (ownership + TUI availability) in the handler. */
|
|
2455
|
+
codingAgentKeyId: z2.string().optional(),
|
|
2456
|
+
/**
|
|
2457
|
+
* Session role. Constrained: other task-less modes fall through to the pm
|
|
2458
|
+
* runner in the pod entrypoint, and "review" would crash without a task.
|
|
2459
|
+
*/
|
|
2460
|
+
mode: z2.enum(["adhoc", "pm"]).optional(),
|
|
2461
|
+
/** Base branch to check out (defaults to the project's dev branch). */
|
|
2462
|
+
branch: z2.string().max(300).optional(),
|
|
2812
2463
|
requestingUserId: z2.string().optional()
|
|
2813
2464
|
});
|
|
2814
2465
|
var StopAdhocSessionRequestSchema = z2.object({
|
|
@@ -3332,8 +2983,8 @@ var ClaudeCodeHarness = class {
|
|
|
3332
2983
|
};
|
|
3333
2984
|
|
|
3334
2985
|
// src/harness/pty/session.ts
|
|
3335
|
-
import { mkdtemp as mkdtemp2, mkdir as
|
|
3336
|
-
import { tmpdir as
|
|
2986
|
+
import { mkdtemp as mkdtemp2, mkdir as mkdir3, rm as rm5 } from "fs/promises";
|
|
2987
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
3337
2988
|
import { join as join4, dirname } from "path";
|
|
3338
2989
|
|
|
3339
2990
|
// src/harness/pty/event-queue.ts
|
|
@@ -3615,12 +3266,14 @@ function mapTranscriptRecord(raw) {
|
|
|
3615
3266
|
// src/harness/pty/jsonl-tailer.ts
|
|
3616
3267
|
var POLL_INTERVAL_MS = 25;
|
|
3617
3268
|
var JsonlTailer = class {
|
|
3618
|
-
constructor(path4, onEvent) {
|
|
3269
|
+
constructor(path4, onEvent, onRawRecord) {
|
|
3619
3270
|
this.path = path4;
|
|
3620
3271
|
this.onEvent = onEvent;
|
|
3272
|
+
this.onRawRecord = onRawRecord;
|
|
3621
3273
|
}
|
|
3622
3274
|
path;
|
|
3623
3275
|
onEvent;
|
|
3276
|
+
onRawRecord;
|
|
3624
3277
|
offset = 0;
|
|
3625
3278
|
buffer = "";
|
|
3626
3279
|
timer = null;
|
|
@@ -3692,79 +3345,349 @@ var JsonlTailer = class {
|
|
|
3692
3345
|
} catch {
|
|
3693
3346
|
return;
|
|
3694
3347
|
}
|
|
3348
|
+
this.onRawRecord?.(parsed);
|
|
3695
3349
|
const event = mapTranscriptRecord(parsed);
|
|
3696
3350
|
if (event) this.onEvent(event);
|
|
3697
3351
|
}
|
|
3698
3352
|
};
|
|
3699
3353
|
|
|
3700
|
-
// src/harness/pty/
|
|
3701
|
-
|
|
3702
|
-
|
|
3354
|
+
// src/harness/pty/chat-record-mapper.ts
|
|
3355
|
+
var TEXT_MAX = 16e3;
|
|
3356
|
+
var TOOL_INPUT_MAX = 1900;
|
|
3357
|
+
function isRecord2(value) {
|
|
3358
|
+
return typeof value === "object" && value !== null;
|
|
3703
3359
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
args.push("--model", input.model);
|
|
3712
|
-
if (input.permissionMode === "bypassPermissions") {
|
|
3713
|
-
args.push("--dangerously-skip-permissions");
|
|
3714
|
-
} else {
|
|
3715
|
-
args.push("--permission-mode", "plan");
|
|
3360
|
+
function isUnknownArray2(value) {
|
|
3361
|
+
return Array.isArray(value);
|
|
3362
|
+
}
|
|
3363
|
+
function stringField2(record, ...keys) {
|
|
3364
|
+
for (const key of keys) {
|
|
3365
|
+
const value = record[key];
|
|
3366
|
+
if (typeof value === "string") return value;
|
|
3716
3367
|
}
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3368
|
+
return void 0;
|
|
3369
|
+
}
|
|
3370
|
+
function truncate(text, max) {
|
|
3371
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
3372
|
+
}
|
|
3373
|
+
function isCommandWrapperText(text) {
|
|
3374
|
+
const trimmed = text.trimStart();
|
|
3375
|
+
return trimmed.startsWith("<command-name>") || trimmed.startsWith("<local-command-");
|
|
3376
|
+
}
|
|
3377
|
+
function mapSystem2(record) {
|
|
3378
|
+
if (record.subtype === "init") {
|
|
3379
|
+
const event = {
|
|
3380
|
+
kind: "init",
|
|
3381
|
+
model: stringField2(record, "model") ?? ""
|
|
3382
|
+
};
|
|
3383
|
+
const sessionId = stringField2(record, "session_id", "sessionId");
|
|
3384
|
+
if (sessionId !== void 0) event.claudeSessionId = sessionId;
|
|
3385
|
+
return [event];
|
|
3720
3386
|
}
|
|
3721
|
-
if (
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3387
|
+
if (record.subtype === "turn_duration") return [{ kind: "turn_end" }];
|
|
3388
|
+
return [];
|
|
3389
|
+
}
|
|
3390
|
+
function mapAssistant2(record) {
|
|
3391
|
+
const message = record.message;
|
|
3392
|
+
if (!isRecord2(message)) return [];
|
|
3393
|
+
const content = isUnknownArray2(message.content) ? message.content : [];
|
|
3394
|
+
const events = [];
|
|
3395
|
+
for (const raw of content) {
|
|
3396
|
+
if (!isRecord2(raw)) continue;
|
|
3397
|
+
if (raw.type === "text") {
|
|
3398
|
+
const text = stringField2(raw, "text");
|
|
3399
|
+
if (text && text.length > 0) {
|
|
3400
|
+
events.push({ kind: "assistant_text", text: truncate(text, TEXT_MAX) });
|
|
3401
|
+
}
|
|
3402
|
+
} else if (raw.type === "tool_use") {
|
|
3403
|
+
const name = stringField2(raw, "name");
|
|
3404
|
+
if (name) {
|
|
3405
|
+
const input = "input" in raw ? raw.input : void 0;
|
|
3406
|
+
events.push({
|
|
3407
|
+
kind: "tool_use",
|
|
3408
|
+
name: truncate(name, 200),
|
|
3409
|
+
input: JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX)
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3725
3412
|
}
|
|
3726
3413
|
}
|
|
3727
|
-
return
|
|
3414
|
+
return events;
|
|
3728
3415
|
}
|
|
3729
|
-
function
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3416
|
+
function mapUser(record) {
|
|
3417
|
+
const message = record.message;
|
|
3418
|
+
if (!isRecord2(message)) return [];
|
|
3419
|
+
const content = message.content;
|
|
3420
|
+
let text;
|
|
3421
|
+
if (typeof content === "string") {
|
|
3422
|
+
text = content;
|
|
3423
|
+
} else if (isUnknownArray2(content)) {
|
|
3424
|
+
const hasToolResult = content.some((b) => isRecord2(b) && b.type === "tool_result");
|
|
3425
|
+
if (hasToolResult) return [];
|
|
3426
|
+
text = content.filter((b) => isRecord2(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
|
|
3427
|
+
} else {
|
|
3428
|
+
return [];
|
|
3429
|
+
}
|
|
3430
|
+
const trimmed = text.trim();
|
|
3431
|
+
if (trimmed.length === 0 || isCommandWrapperText(trimmed)) return [];
|
|
3432
|
+
return [{ kind: "user_text", text: truncate(trimmed, TEXT_MAX) }];
|
|
3736
3433
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3434
|
+
function mapChatRecords(raw) {
|
|
3435
|
+
if (!isRecord2(raw)) return [];
|
|
3436
|
+
if (raw.isSidechain === true || raw.isMeta === true) return [];
|
|
3437
|
+
switch (raw.type) {
|
|
3438
|
+
case "system":
|
|
3439
|
+
return mapSystem2(raw);
|
|
3440
|
+
case "assistant":
|
|
3441
|
+
return mapAssistant2(raw);
|
|
3442
|
+
case "user":
|
|
3443
|
+
return mapUser(raw);
|
|
3444
|
+
case "result":
|
|
3445
|
+
return [{ kind: "turn_end" }];
|
|
3446
|
+
default:
|
|
3447
|
+
return [];
|
|
3747
3448
|
}
|
|
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
3449
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3450
|
+
|
|
3451
|
+
// src/harness/pty/settings.ts
|
|
3452
|
+
import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
|
|
3453
|
+
import { homedir } from "os";
|
|
3454
|
+
import { join } from "path";
|
|
3455
|
+
function claudeConfigHome() {
|
|
3456
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
3754
3457
|
}
|
|
3755
|
-
function
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3458
|
+
function projectSlug(cwd) {
|
|
3459
|
+
return cwd.replace(/\//g, "-");
|
|
3460
|
+
}
|
|
3461
|
+
function sessionTranscriptPath(cwd, sessionId) {
|
|
3462
|
+
return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
|
|
3463
|
+
}
|
|
3464
|
+
var ALLOW_RULES = [
|
|
3465
|
+
"Bash",
|
|
3466
|
+
"BashOutput",
|
|
3467
|
+
"Edit",
|
|
3468
|
+
"Write",
|
|
3469
|
+
"Read",
|
|
3470
|
+
"Glob",
|
|
3471
|
+
"Grep",
|
|
3472
|
+
"WebFetch",
|
|
3473
|
+
"WebSearch",
|
|
3474
|
+
"NotebookEdit",
|
|
3475
|
+
"Task",
|
|
3476
|
+
"TodoWrite",
|
|
3477
|
+
"ToolSearch",
|
|
3478
|
+
"KillShell",
|
|
3479
|
+
"SlashCommand",
|
|
3480
|
+
"Skill",
|
|
3481
|
+
"mcp__conveyor__*"
|
|
3482
|
+
];
|
|
3483
|
+
var HOOK_HELPER_SOURCE = `"use strict";
|
|
3484
|
+
const net = require("node:net");
|
|
3485
|
+
const crypto = require("node:crypto");
|
|
3486
|
+
|
|
3487
|
+
const PRE_TOOL_USE_TIMEOUT_MS = 110000;
|
|
3488
|
+
const ASK_USER_QUESTION_TIMEOUT_MS = 5000;
|
|
3489
|
+
|
|
3490
|
+
let raw = "";
|
|
3491
|
+
process.stdin.setEncoding("utf8");
|
|
3492
|
+
process.stdin.on("data", (chunk) => {
|
|
3493
|
+
raw += chunk;
|
|
3494
|
+
});
|
|
3495
|
+
process.stdin.on("end", () => {
|
|
3496
|
+
let payload = {};
|
|
3497
|
+
try {
|
|
3498
|
+
const parsed = JSON.parse(raw);
|
|
3499
|
+
if (parsed && typeof parsed === "object") payload = parsed;
|
|
3500
|
+
} catch {
|
|
3501
|
+
payload = {};
|
|
3502
|
+
}
|
|
3503
|
+
if (payload.hook_event_name === "PreToolUse") {
|
|
3504
|
+
preToolUse(payload);
|
|
3505
|
+
} else {
|
|
3506
|
+
relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
|
|
3507
|
+
}
|
|
3508
|
+
});
|
|
3509
|
+
|
|
3510
|
+
function relayProgress(toolName) {
|
|
3511
|
+
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
3512
|
+
let done = false;
|
|
3513
|
+
const finish = () => {
|
|
3514
|
+
if (done) return;
|
|
3515
|
+
done = true;
|
|
3516
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
3517
|
+
process.exit(0);
|
|
3518
|
+
};
|
|
3519
|
+
const fallback = setTimeout(finish, 250);
|
|
3520
|
+
if (!socketPath) {
|
|
3521
|
+
clearTimeout(fallback);
|
|
3522
|
+
finish();
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
const client = net.connect(socketPath, () => {
|
|
3526
|
+
// PostToolUse does not supply tool duration, so elapsed_time_seconds is
|
|
3527
|
+
// omitted rather than reported as a misleading 0 (the field is optional).
|
|
3528
|
+
const line = JSON.stringify({ tool_name: toolName }) + "\\n";
|
|
3529
|
+
client.write(line, () => {
|
|
3530
|
+
client.end();
|
|
3531
|
+
});
|
|
3532
|
+
});
|
|
3533
|
+
client.on("close", () => {
|
|
3534
|
+
clearTimeout(fallback);
|
|
3535
|
+
finish();
|
|
3536
|
+
});
|
|
3537
|
+
client.on("error", () => {
|
|
3538
|
+
clearTimeout(fallback);
|
|
3539
|
+
finish();
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
function preToolUse(payload) {
|
|
3544
|
+
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
3545
|
+
let done = false;
|
|
3546
|
+
const respond = (decision, reason) => {
|
|
3547
|
+
if (done) return;
|
|
3548
|
+
done = true;
|
|
3549
|
+
process.stdout.write(
|
|
3550
|
+
JSON.stringify({
|
|
3551
|
+
hookSpecificOutput: {
|
|
3552
|
+
hookEventName: "PreToolUse",
|
|
3553
|
+
permissionDecision: decision,
|
|
3554
|
+
...(reason ? { permissionDecisionReason: reason } : {}),
|
|
3555
|
+
},
|
|
3556
|
+
}),
|
|
3761
3557
|
);
|
|
3558
|
+
process.exit(0);
|
|
3559
|
+
};
|
|
3560
|
+
// MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
|
|
3561
|
+
// Plan mode prompts for them despite permissions.allow (the CLI can't
|
|
3562
|
+
// classify MCP tools as read-only), and the pod is the sandbox.
|
|
3563
|
+
if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
|
|
3564
|
+
respond("allow");
|
|
3565
|
+
return;
|
|
3762
3566
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3567
|
+
// AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
|
|
3568
|
+
// missing/slow socket never denies the questionnaire. Everything else
|
|
3569
|
+
// (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
|
|
3570
|
+
const failOpen = payload.tool_name === "AskUserQuestion";
|
|
3571
|
+
const respondUnavailable = () =>
|
|
3572
|
+
failOpen
|
|
3573
|
+
? respond("allow")
|
|
3574
|
+
: respond(
|
|
3575
|
+
"deny",
|
|
3576
|
+
"Conveyor validation is unavailable right now. Wait a moment and call the tool again.",
|
|
3577
|
+
);
|
|
3578
|
+
if (!socketPath) {
|
|
3579
|
+
respondUnavailable();
|
|
3580
|
+
return;
|
|
3766
3581
|
}
|
|
3767
|
-
|
|
3582
|
+
const id = crypto.randomBytes(8).toString("hex");
|
|
3583
|
+
const timer = setTimeout(
|
|
3584
|
+
respondUnavailable,
|
|
3585
|
+
failOpen ? ASK_USER_QUESTION_TIMEOUT_MS : PRE_TOOL_USE_TIMEOUT_MS,
|
|
3586
|
+
);
|
|
3587
|
+
const client = net.connect(socketPath, () => {
|
|
3588
|
+
client.write(
|
|
3589
|
+
JSON.stringify({
|
|
3590
|
+
kind: "pre_tool_use",
|
|
3591
|
+
id,
|
|
3592
|
+
tool_name: typeof payload.tool_name === "string" ? payload.tool_name : "",
|
|
3593
|
+
tool_input:
|
|
3594
|
+
payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {},
|
|
3595
|
+
}) + "\\n",
|
|
3596
|
+
);
|
|
3597
|
+
});
|
|
3598
|
+
let buffer = "";
|
|
3599
|
+
client.on("data", (chunk) => {
|
|
3600
|
+
buffer += chunk.toString("utf8");
|
|
3601
|
+
let index = buffer.indexOf("\\n");
|
|
3602
|
+
while (index >= 0) {
|
|
3603
|
+
const line = buffer.slice(0, index);
|
|
3604
|
+
buffer = buffer.slice(index + 1);
|
|
3605
|
+
try {
|
|
3606
|
+
const verdict = JSON.parse(line);
|
|
3607
|
+
if (verdict && verdict.id === id) {
|
|
3608
|
+
clearTimeout(timer);
|
|
3609
|
+
client.end();
|
|
3610
|
+
respond(verdict.decision === "allow" ? "allow" : "deny", verdict.reason);
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
} catch {
|
|
3614
|
+
/* keep scanning */
|
|
3615
|
+
}
|
|
3616
|
+
index = buffer.indexOf("\\n");
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
client.on("error", () => {
|
|
3620
|
+
clearTimeout(timer);
|
|
3621
|
+
respondUnavailable();
|
|
3622
|
+
});
|
|
3623
|
+
client.on("close", () => {
|
|
3624
|
+
clearTimeout(timer);
|
|
3625
|
+
respondUnavailable();
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
`;
|
|
3629
|
+
async function writeHookSettings(dir) {
|
|
3630
|
+
const helperPath = join(dir, "hook-helper.cjs");
|
|
3631
|
+
const settingsPath = join(dir, "settings.json");
|
|
3632
|
+
await mkdir(dir, { recursive: true });
|
|
3633
|
+
await writeFile2(helperPath, HOOK_HELPER_SOURCE, "utf8");
|
|
3634
|
+
await chmod(helperPath, 493);
|
|
3635
|
+
const settings = {
|
|
3636
|
+
// Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
|
|
3637
|
+
// spawns pass `--dangerously-skip-permissions`; on a real PTY the CLI otherwise
|
|
3638
|
+
// parks on the interactive "Yes, I accept / No, exit" dialog whose default focus
|
|
3639
|
+
// is "No, exit" — so it cannot be auto-dismissed by an Enter nudge and the run
|
|
3640
|
+
// stalls until the auto-nudge watchdog shuts it down. The CLI reads this key
|
|
3641
|
+
// from the `--settings` (flagSettings) layer via its bypass-mode gate, so
|
|
3642
|
+
// setting it here suppresses the dialog deterministically on every spawn —
|
|
3643
|
+
// independent of the `bypassPermissionsModeAccepted` seed in credentials.ts,
|
|
3644
|
+
// which the CLI's one-time migration consumes and which is subject to
|
|
3645
|
+
// persistence races on the codespace user-home.
|
|
3646
|
+
skipDangerousModePermissionPrompt: true,
|
|
3647
|
+
// Auto-approve tool calls so the interactive (PTY) agent never stops to
|
|
3648
|
+
// prompt the viewer. This only suppresses per-tool approval prompts — it
|
|
3649
|
+
// does NOT relax the planning gate: in discovery/plan mode the spawn passes
|
|
3650
|
+
// `--permission-mode plan`, which keeps the agent read-only (no edits/commits
|
|
3651
|
+
// until it exits plan mode) regardless of this allow-list. In build mode the
|
|
3652
|
+
// spawn already bypasses prompts via `--dangerously-skip-permissions`.
|
|
3653
|
+
// NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
|
|
3654
|
+
// a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
|
|
3655
|
+
permissions: {
|
|
3656
|
+
allow: ALLOW_RULES
|
|
3657
|
+
},
|
|
3658
|
+
hooks: {
|
|
3659
|
+
PreToolUse: [
|
|
3660
|
+
{
|
|
3661
|
+
matcher: "ExitPlanMode",
|
|
3662
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 120 }]
|
|
3663
|
+
},
|
|
3664
|
+
{
|
|
3665
|
+
// Observe-only: lets the session report waiting_for_input the moment
|
|
3666
|
+
// the planning questionnaire renders. The helper fails open for this
|
|
3667
|
+
// tool, so the questionnaire is never blocked by Conveyor.
|
|
3668
|
+
matcher: "AskUserQuestion",
|
|
3669
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
3670
|
+
},
|
|
3671
|
+
{
|
|
3672
|
+
// Plan-permission spawns prompt for MCP tools even when they're in
|
|
3673
|
+
// permissions.allow — the CLI can't classify them as read-only. A
|
|
3674
|
+
// hook allow bypasses the permission engine in every mode. Loose by
|
|
3675
|
+
// design: the pod is the sandbox, and planning agents must call
|
|
3676
|
+
// update_task etc. The helper answers locally (no socket).
|
|
3677
|
+
matcher: "mcp__.*",
|
|
3678
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
3679
|
+
}
|
|
3680
|
+
],
|
|
3681
|
+
PostToolUse: [
|
|
3682
|
+
{
|
|
3683
|
+
matcher: "*",
|
|
3684
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
|
|
3685
|
+
}
|
|
3686
|
+
]
|
|
3687
|
+
}
|
|
3688
|
+
};
|
|
3689
|
+
await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
3690
|
+
return { settingsPath, helperPath };
|
|
3768
3691
|
}
|
|
3769
3692
|
|
|
3770
3693
|
// src/harness/pty/output-coalescer.ts
|
|
@@ -3822,7 +3745,7 @@ var PtyOutputCoalescer = class {
|
|
|
3822
3745
|
// src/harness/pty/tool-server.ts
|
|
3823
3746
|
import { createServer as createServer2 } from "http";
|
|
3824
3747
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
3825
|
-
import { join as
|
|
3748
|
+
import { join as join2 } from "path";
|
|
3826
3749
|
import { randomBytes } from "crypto";
|
|
3827
3750
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3828
3751
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -3989,10 +3912,242 @@ async function startToolServers(mcpServers, tempDir) {
|
|
|
3989
3912
|
servers.push(server);
|
|
3990
3913
|
config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
|
|
3991
3914
|
}
|
|
3992
|
-
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
3993
|
-
const mcpConfigPath =
|
|
3994
|
-
await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
3995
|
-
return { servers, mcpConfigPath };
|
|
3915
|
+
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
3916
|
+
const mcpConfigPath = join2(tempDir, "mcp-config.json");
|
|
3917
|
+
await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
3918
|
+
return { servers, mcpConfigPath };
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
// src/harness/pty/spawn-args.ts
|
|
3922
|
+
function buildSpawnArgs(input) {
|
|
3923
|
+
const args = [];
|
|
3924
|
+
if (input.resume) {
|
|
3925
|
+
args.push("--resume", input.resume);
|
|
3926
|
+
} else if (input.sessionId) {
|
|
3927
|
+
args.push("--session-id", input.sessionId);
|
|
3928
|
+
}
|
|
3929
|
+
args.push("--model", input.model);
|
|
3930
|
+
if (input.permissionMode === "bypassPermissions") {
|
|
3931
|
+
args.push("--dangerously-skip-permissions");
|
|
3932
|
+
} else {
|
|
3933
|
+
args.push("--permission-mode", "plan");
|
|
3934
|
+
}
|
|
3935
|
+
args.push("--settings", input.settingsPath);
|
|
3936
|
+
if (input.appendSystemPrompt) {
|
|
3937
|
+
args.push("--append-system-prompt", input.appendSystemPrompt);
|
|
3938
|
+
}
|
|
3939
|
+
if (input.mcpConfigPath) {
|
|
3940
|
+
args.push("--mcp-config", input.mcpConfigPath);
|
|
3941
|
+
if (input.strictMcpConfig) {
|
|
3942
|
+
args.push("--strict-mcp-config");
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return args;
|
|
3946
|
+
}
|
|
3947
|
+
function spawnOptionsFingerprint(input) {
|
|
3948
|
+
return JSON.stringify([
|
|
3949
|
+
input.model,
|
|
3950
|
+
input.permissionMode,
|
|
3951
|
+
input.appendSystemPrompt ?? "",
|
|
3952
|
+
input.cwd
|
|
3953
|
+
]);
|
|
3954
|
+
}
|
|
3955
|
+
var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
3956
|
+
function cleanTerminalOutput(raw, maxChars = 1200) {
|
|
3957
|
+
const noAnsi = raw.replace(ANSI_CSI, "");
|
|
3958
|
+
let out = "";
|
|
3959
|
+
for (const ch of noAnsi) {
|
|
3960
|
+
const code = ch.charCodeAt(0);
|
|
3961
|
+
if (ch === "\r" || ch === "\n") out += "\n";
|
|
3962
|
+
else if (ch === " ") out += ch;
|
|
3963
|
+
else if (code < 32 || code === 127) continue;
|
|
3964
|
+
else out += ch;
|
|
3965
|
+
}
|
|
3966
|
+
const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
3967
|
+
const text = lines.join("\n").trim();
|
|
3968
|
+
return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
|
|
3969
|
+
}
|
|
3970
|
+
function isMissingBinaryFailure(tail) {
|
|
3971
|
+
return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
|
|
3972
|
+
}
|
|
3973
|
+
function buildExitErrors(exitCode, rawOutput, binary) {
|
|
3974
|
+
const errors = [`claude exited (code ${exitCode}) without a result`];
|
|
3975
|
+
const tail = cleanTerminalOutput(rawOutput);
|
|
3976
|
+
if (isMissingBinaryFailure(tail)) {
|
|
3977
|
+
errors.push(
|
|
3978
|
+
`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.`
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3981
|
+
if (tail) {
|
|
3982
|
+
errors.push(`Last terminal output before exit:
|
|
3983
|
+
${tail}`);
|
|
3984
|
+
}
|
|
3985
|
+
return errors;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
// src/harness/pty/credentials.ts
|
|
3989
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
3990
|
+
import { homedir as homedir2 } from "os";
|
|
3991
|
+
import { join as join3 } from "path";
|
|
3992
|
+
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
3993
|
+
var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3994
|
+
function claudeCredentialsPath() {
|
|
3995
|
+
return join3(claudeConfigHome(), ".credentials.json");
|
|
3996
|
+
}
|
|
3997
|
+
function isConveyorCloudEnv(env = process.env) {
|
|
3998
|
+
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
3999
|
+
}
|
|
4000
|
+
function parseClaudeAiOauth(raw) {
|
|
4001
|
+
if (!raw || raw.trim() === "") return null;
|
|
4002
|
+
try {
|
|
4003
|
+
const parsed = JSON.parse(raw);
|
|
4004
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4005
|
+
const oauth = parsed.claudeAiOauth;
|
|
4006
|
+
if (typeof oauth !== "object" || oauth === null) return null;
|
|
4007
|
+
const record = oauth;
|
|
4008
|
+
return {
|
|
4009
|
+
accessToken: record.accessToken,
|
|
4010
|
+
refreshToken: record.refreshToken,
|
|
4011
|
+
expiresAt: record.expiresAt
|
|
4012
|
+
};
|
|
4013
|
+
} catch {
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
function buildSynthesizedCredentials(token, now) {
|
|
4018
|
+
return JSON.stringify({
|
|
4019
|
+
claudeAiOauth: {
|
|
4020
|
+
accessToken: token,
|
|
4021
|
+
expiresAt: now + SYNTH_TOKEN_TTL_MS,
|
|
4022
|
+
scopes: ["user:inference", "user:profile"],
|
|
4023
|
+
subscriptionType: "max"
|
|
4024
|
+
}
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
function planCredentialsWrite(input) {
|
|
4028
|
+
if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
|
|
4029
|
+
if (!input.token) return { action: "skip", reason: "no-token" };
|
|
4030
|
+
const contents = buildSynthesizedCredentials(input.token, input.now);
|
|
4031
|
+
const existing = parseClaudeAiOauth(input.existingRaw);
|
|
4032
|
+
if (!existing) return { action: "write", contents };
|
|
4033
|
+
if (typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
4034
|
+
return { action: "skip", reason: "foreign-credentials" };
|
|
4035
|
+
}
|
|
4036
|
+
const fresh = existing.accessToken === input.token && typeof existing.expiresAt === "number" && existing.expiresAt > input.now + REFRESH_SKEW_MS;
|
|
4037
|
+
if (fresh) return { action: "skip", reason: "current" };
|
|
4038
|
+
return { action: "write", contents };
|
|
4039
|
+
}
|
|
4040
|
+
async function readRaw(path4) {
|
|
4041
|
+
try {
|
|
4042
|
+
return await readFile(path4, "utf8");
|
|
4043
|
+
} catch {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
async function ensureClaudeCredentials(env = process.env) {
|
|
4048
|
+
try {
|
|
4049
|
+
const path4 = claudeCredentialsPath();
|
|
4050
|
+
const plan = planCredentialsWrite({
|
|
4051
|
+
isCloud: isConveyorCloudEnv(env),
|
|
4052
|
+
token: env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
4053
|
+
existingRaw: await readRaw(path4),
|
|
4054
|
+
now: Date.now()
|
|
4055
|
+
});
|
|
4056
|
+
if (plan.action === "skip") return;
|
|
4057
|
+
await mkdir2(claudeConfigHome(), { recursive: true });
|
|
4058
|
+
await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
|
|
4059
|
+
await chmod2(path4, 384).catch(() => {
|
|
4060
|
+
});
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4063
|
+
process.stderr.write(`[conveyor-agent] claude credentials sync failed: ${message}
|
|
4064
|
+
`);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
function claudeJsonPath() {
|
|
4068
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
4069
|
+
return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
|
|
4070
|
+
}
|
|
4071
|
+
function asRecord(value) {
|
|
4072
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
4073
|
+
}
|
|
4074
|
+
function parseClaudeJson(existingRaw) {
|
|
4075
|
+
if (!existingRaw || existingRaw.trim() === "") return {};
|
|
4076
|
+
try {
|
|
4077
|
+
return asRecord(JSON.parse(existingRaw));
|
|
4078
|
+
} catch {
|
|
4079
|
+
return {};
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
function seedWorkspaceTrust(config, trustCwd) {
|
|
4083
|
+
const projects = asRecord(config.projects);
|
|
4084
|
+
const entry = asRecord(projects[trustCwd]);
|
|
4085
|
+
if (entry.hasTrustDialogAccepted === true) return false;
|
|
4086
|
+
entry.hasTrustDialogAccepted = true;
|
|
4087
|
+
projects[trustCwd] = entry;
|
|
4088
|
+
config.projects = projects;
|
|
4089
|
+
return true;
|
|
4090
|
+
}
|
|
4091
|
+
function planClaudeJsonSeed(existingRaw, trustCwd) {
|
|
4092
|
+
const config = parseClaudeJson(existingRaw);
|
|
4093
|
+
const isFresh = Object.keys(config).length === 0;
|
|
4094
|
+
let changed = false;
|
|
4095
|
+
if (config.hasCompletedOnboarding !== true) {
|
|
4096
|
+
config.hasCompletedOnboarding = true;
|
|
4097
|
+
changed = true;
|
|
4098
|
+
}
|
|
4099
|
+
if (config.bypassPermissionsModeAccepted !== true) {
|
|
4100
|
+
config.bypassPermissionsModeAccepted = true;
|
|
4101
|
+
changed = true;
|
|
4102
|
+
}
|
|
4103
|
+
if (isFresh && typeof config.theme !== "string") {
|
|
4104
|
+
config.theme = "dark";
|
|
4105
|
+
changed = true;
|
|
4106
|
+
}
|
|
4107
|
+
if (trustCwd && seedWorkspaceTrust(config, trustCwd)) {
|
|
4108
|
+
changed = true;
|
|
4109
|
+
}
|
|
4110
|
+
return changed ? JSON.stringify(config) : null;
|
|
4111
|
+
}
|
|
4112
|
+
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
4113
|
+
try {
|
|
4114
|
+
if (!isConveyorCloudEnv(env)) return;
|
|
4115
|
+
const path4 = claudeJsonPath();
|
|
4116
|
+
const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
|
|
4117
|
+
if (contents === null) return;
|
|
4118
|
+
await writeFile4(path4, contents, "utf8");
|
|
4119
|
+
const verify = await readRaw(path4);
|
|
4120
|
+
if (verify === contents) {
|
|
4121
|
+
process.stderr.write(
|
|
4122
|
+
`[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
|
|
4123
|
+
`
|
|
4124
|
+
);
|
|
4125
|
+
} else {
|
|
4126
|
+
process.stderr.write(
|
|
4127
|
+
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
|
|
4128
|
+
`
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
} catch (err) {
|
|
4132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4133
|
+
process.stderr.write(`[conveyor-agent] claude onboarding seed failed: ${message}
|
|
4134
|
+
`);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
async function removeConveyorCredentials(env = process.env) {
|
|
4138
|
+
try {
|
|
4139
|
+
if (!isConveyorCloudEnv(env)) return;
|
|
4140
|
+
const path4 = claudeCredentialsPath();
|
|
4141
|
+
const existing = parseClaudeAiOauth(await readRaw(path4));
|
|
4142
|
+
if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
await rm4(path4, { force: true });
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4148
|
+
process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
|
|
4149
|
+
`);
|
|
4150
|
+
}
|
|
3996
4151
|
}
|
|
3997
4152
|
|
|
3998
4153
|
// src/harness/pty/pty-support.ts
|
|
@@ -4029,14 +4184,14 @@ function turnOptionsFrom(options) {
|
|
|
4029
4184
|
abortController: options.abortController
|
|
4030
4185
|
};
|
|
4031
4186
|
}
|
|
4032
|
-
function
|
|
4187
|
+
function isRecord3(value) {
|
|
4033
4188
|
return typeof value === "object" && value !== null;
|
|
4034
4189
|
}
|
|
4035
4190
|
function extractSpawn(mod) {
|
|
4036
|
-
if (!
|
|
4191
|
+
if (!isRecord3(mod)) return null;
|
|
4037
4192
|
if (typeof mod.spawn === "function") return mod.spawn;
|
|
4038
4193
|
const def = mod.default;
|
|
4039
|
-
if (
|
|
4194
|
+
if (isRecord3(def) && typeof def.spawn === "function") return def.spawn;
|
|
4040
4195
|
return null;
|
|
4041
4196
|
}
|
|
4042
4197
|
async function loadPtySpawn() {
|
|
@@ -4050,7 +4205,9 @@ function inheritedEnv(socketPath) {
|
|
|
4050
4205
|
for (const [key, value] of Object.entries(process.env)) {
|
|
4051
4206
|
if (typeof value === "string") env[key] = value;
|
|
4052
4207
|
}
|
|
4053
|
-
|
|
4208
|
+
if (socketPath) {
|
|
4209
|
+
env.CONVEYOR_HOOK_SOCKET = socketPath;
|
|
4210
|
+
}
|
|
4054
4211
|
env.MCP_TIMEOUT ??= "60000";
|
|
4055
4212
|
env.MCP_TOOL_TIMEOUT ??= "180000";
|
|
4056
4213
|
return env;
|
|
@@ -4058,6 +4215,16 @@ function inheritedEnv(socketPath) {
|
|
|
4058
4215
|
function buildPromptBytes(text) {
|
|
4059
4216
|
return `\x1B[200~${text}\x1B[201~`;
|
|
4060
4217
|
}
|
|
4218
|
+
function renderPromptContentText(content) {
|
|
4219
|
+
return content.map((block) => {
|
|
4220
|
+
const b = block;
|
|
4221
|
+
if (b?.type === "text" && typeof b.text === "string") return b.text;
|
|
4222
|
+
if (b?.type === "image") {
|
|
4223
|
+
return `[Image attachment \u2014 use list_task_files / get_attachment to view]`;
|
|
4224
|
+
}
|
|
4225
|
+
return JSON.stringify(block);
|
|
4226
|
+
}).join("\n\n");
|
|
4227
|
+
}
|
|
4061
4228
|
function sleep3(ms) {
|
|
4062
4229
|
return new Promise((resolve) => {
|
|
4063
4230
|
setTimeout(resolve, ms);
|
|
@@ -4074,9 +4241,9 @@ function parseUserQuestions(input) {
|
|
|
4074
4241
|
if (!Array.isArray(input.questions)) return [];
|
|
4075
4242
|
const questions = [];
|
|
4076
4243
|
for (const entry of input.questions) {
|
|
4077
|
-
if (!
|
|
4244
|
+
if (!isRecord3(entry)) continue;
|
|
4078
4245
|
if (typeof entry.question !== "string") continue;
|
|
4079
|
-
const options = Array.isArray(entry.options) ? entry.options.filter(
|
|
4246
|
+
const options = Array.isArray(entry.options) ? entry.options.filter(isRecord3).filter((o) => typeof o.label === "string").map((o) => ({
|
|
4080
4247
|
label: o.label,
|
|
4081
4248
|
description: typeof o.description === "string" ? o.description : ""
|
|
4082
4249
|
})) : [];
|
|
@@ -4090,18 +4257,65 @@ function parseUserQuestions(input) {
|
|
|
4090
4257
|
return questions;
|
|
4091
4258
|
}
|
|
4092
4259
|
|
|
4260
|
+
// src/harness/pty/adapters/claude.ts
|
|
4261
|
+
var ClaudeTuiAdapter = class {
|
|
4262
|
+
id = "claude-code";
|
|
4263
|
+
capabilities = {
|
|
4264
|
+
resume: true,
|
|
4265
|
+
structuredEvents: true,
|
|
4266
|
+
prefill: true,
|
|
4267
|
+
passiveTurns: true
|
|
4268
|
+
};
|
|
4269
|
+
resolveBinary(env = process.env) {
|
|
4270
|
+
return env.CONVEYOR_CLAUDE_BIN ?? "claude";
|
|
4271
|
+
}
|
|
4272
|
+
buildSpawn(input) {
|
|
4273
|
+
const { options, resume } = input;
|
|
4274
|
+
const args = buildSpawnArgs({
|
|
4275
|
+
...resume ? { resume } : {},
|
|
4276
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
4277
|
+
model: options.model,
|
|
4278
|
+
permissionMode: options.permissionMode,
|
|
4279
|
+
settingsPath: input.settingsPath ?? "",
|
|
4280
|
+
...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
|
|
4281
|
+
...input.mcpConfigPath ? { mcpConfigPath: input.mcpConfigPath, strictMcpConfig: true } : {}
|
|
4282
|
+
});
|
|
4283
|
+
return {
|
|
4284
|
+
file: this.resolveBinary(),
|
|
4285
|
+
args,
|
|
4286
|
+
env: inheritedEnv(input.hookSocketPath)
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
async prepareEnvironment(opts) {
|
|
4290
|
+
const env = opts.env ?? process.env;
|
|
4291
|
+
await ensureClaudeCredentials(env);
|
|
4292
|
+
await ensureClaudeOnboarding(env, opts.cwd);
|
|
4293
|
+
}
|
|
4294
|
+
spawnFingerprint(input) {
|
|
4295
|
+
return spawnOptionsFingerprint(input);
|
|
4296
|
+
}
|
|
4297
|
+
encodePromptBytes(text) {
|
|
4298
|
+
return buildPromptBytes(text);
|
|
4299
|
+
}
|
|
4300
|
+
buildExitErrors(exitCode, rawOutput) {
|
|
4301
|
+
return buildExitErrors(exitCode, rawOutput, this.resolveBinary());
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
|
|
4093
4305
|
// src/harness/pty/session.ts
|
|
4094
4306
|
var PtySession = class {
|
|
4095
|
-
constructor(prompt, options, resume, bridge) {
|
|
4307
|
+
constructor(prompt, options, resume, bridge, adapter = new ClaudeTuiAdapter()) {
|
|
4096
4308
|
this.options = options;
|
|
4097
4309
|
this.resume = resume;
|
|
4098
4310
|
this.bridge = bridge;
|
|
4311
|
+
this.adapter = adapter;
|
|
4099
4312
|
this.turnPrompt = prompt;
|
|
4100
4313
|
this.turn = turnOptionsFrom(options);
|
|
4101
4314
|
}
|
|
4102
4315
|
options;
|
|
4103
4316
|
resume;
|
|
4104
4317
|
bridge;
|
|
4318
|
+
adapter;
|
|
4105
4319
|
// The current turn's event stream. Null between turns (process parked) — the
|
|
4106
4320
|
// process outlives any single turn, so the queue is per-turn, not per-process.
|
|
4107
4321
|
activeQueue = null;
|
|
@@ -4187,7 +4401,7 @@ var PtySession = class {
|
|
|
4187
4401
|
}
|
|
4188
4402
|
/** Fingerprint of the spawn-time options a reused process cannot change. */
|
|
4189
4403
|
get spawnFingerprint() {
|
|
4190
|
-
return
|
|
4404
|
+
return this.adapter.spawnFingerprint({
|
|
4191
4405
|
model: this.options.model,
|
|
4192
4406
|
permissionMode: this.options.permissionMode,
|
|
4193
4407
|
...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
|
|
@@ -4324,22 +4538,19 @@ var PtySession = class {
|
|
|
4324
4538
|
return;
|
|
4325
4539
|
}
|
|
4326
4540
|
try {
|
|
4327
|
-
this.
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
this.tailer = new JsonlTailer(transcriptPath, (event) => this.handleTranscriptEvent(event));
|
|
4341
|
-
this.tailer.start(startOffset);
|
|
4342
|
-
await this.spawn(settingsPath, socketPath);
|
|
4541
|
+
if (this.adapter.capabilities.structuredEvents) {
|
|
4542
|
+
const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
|
|
4543
|
+
await this.spawn(settingsPath, socketPath);
|
|
4544
|
+
} else {
|
|
4545
|
+
this.tempDir = await mkdtemp2(join4(tmpdir3(), "conveyor-pty-"));
|
|
4546
|
+
await this.spawn();
|
|
4547
|
+
this.pushEvent({
|
|
4548
|
+
type: "system",
|
|
4549
|
+
subtype: "init",
|
|
4550
|
+
session_id: sessionId,
|
|
4551
|
+
model: this.options.model
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
4343
4554
|
if (signal) {
|
|
4344
4555
|
this.abortHandler = () => {
|
|
4345
4556
|
void this.teardown();
|
|
@@ -4360,6 +4571,38 @@ var PtySession = class {
|
|
|
4360
4571
|
throw err;
|
|
4361
4572
|
}
|
|
4362
4573
|
}
|
|
4574
|
+
/**
|
|
4575
|
+
* Allocate the Claude-style structured-event sources: the PostToolUse hook
|
|
4576
|
+
* socket, the per-run settings file, the in-process tool servers, and the
|
|
4577
|
+
* transcript tailer. Only structured-events adapters call this — raw-terminal
|
|
4578
|
+
* adapters have no trusted event source and skip it entirely. Returns the
|
|
4579
|
+
* paths spawn() must wire into the child's argv/env.
|
|
4580
|
+
*/
|
|
4581
|
+
async startStructuredEventSources(sessionId) {
|
|
4582
|
+
this.tempDir = await mkdtemp2(join4(tmpdir3(), "conveyor-pty-"));
|
|
4583
|
+
const socketPath = join4(this.tempDir, "hook.sock");
|
|
4584
|
+
this.socket = new HookSocketServer(
|
|
4585
|
+
socketPath,
|
|
4586
|
+
(progress) => this.handleProgress(progress),
|
|
4587
|
+
(request) => this.handlePreToolUse(request)
|
|
4588
|
+
);
|
|
4589
|
+
await this.socket.listen();
|
|
4590
|
+
const { settingsPath } = await writeHookSettings(this.tempDir);
|
|
4591
|
+
await this.setupToolServers();
|
|
4592
|
+
const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
|
|
4593
|
+
await mkdir3(dirname(transcriptPath), { recursive: true });
|
|
4594
|
+
const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
|
|
4595
|
+
const sendChat = this.bridge?.sendChatEvent?.bind(this.bridge);
|
|
4596
|
+
this.tailer = new JsonlTailer(
|
|
4597
|
+
transcriptPath,
|
|
4598
|
+
(event) => this.handleTranscriptEvent(event),
|
|
4599
|
+
sendChat ? (raw) => {
|
|
4600
|
+
for (const chatEvent of mapChatRecords(raw)) sendChat(chatEvent);
|
|
4601
|
+
} : void 0
|
|
4602
|
+
);
|
|
4603
|
+
this.tailer.start(startOffset);
|
|
4604
|
+
return { settingsPath, socketPath };
|
|
4605
|
+
}
|
|
4363
4606
|
writeStdin(text) {
|
|
4364
4607
|
this.pty?.write(text);
|
|
4365
4608
|
}
|
|
@@ -4425,7 +4668,7 @@ var PtySession = class {
|
|
|
4425
4668
|
this.activeQueue?.close();
|
|
4426
4669
|
this.activeQueue = null;
|
|
4427
4670
|
if (this.tempDir) {
|
|
4428
|
-
await
|
|
4671
|
+
await rm5(this.tempDir, { recursive: true, force: true });
|
|
4429
4672
|
this.tempDir = "";
|
|
4430
4673
|
}
|
|
4431
4674
|
}
|
|
@@ -4444,25 +4687,23 @@ var PtySession = class {
|
|
|
4444
4687
|
this.mcpConfigPath = mcpConfigPath;
|
|
4445
4688
|
}
|
|
4446
4689
|
async spawn(settingsPath, socketPath) {
|
|
4447
|
-
const
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
settingsPath,
|
|
4453
|
-
...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
|
|
4690
|
+
const spec = this.adapter.buildSpawn({
|
|
4691
|
+
options: this.options,
|
|
4692
|
+
...this.resume ? { resume: this.resume } : {},
|
|
4693
|
+
...settingsPath ? { settingsPath } : {},
|
|
4694
|
+
...socketPath ? { hookSocketPath: socketPath } : {},
|
|
4454
4695
|
// Only this config: ignore the user's ~/.claude.json / project .mcp.json
|
|
4455
4696
|
// so the agent's tool set is deterministic (and a stale personal conveyor
|
|
4456
4697
|
// server doesn't load).
|
|
4457
|
-
...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath
|
|
4698
|
+
...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
|
|
4458
4699
|
});
|
|
4459
4700
|
const spawn2 = await loadPtySpawn();
|
|
4460
|
-
const pty = spawn2(
|
|
4701
|
+
const pty = spawn2(spec.file, spec.args, {
|
|
4461
4702
|
name: "xterm-color",
|
|
4462
4703
|
cols: this.cols,
|
|
4463
4704
|
rows: this.rows,
|
|
4464
4705
|
cwd: this.options.cwd,
|
|
4465
|
-
env:
|
|
4706
|
+
env: spec.env
|
|
4466
4707
|
});
|
|
4467
4708
|
const bridge = this.bridge;
|
|
4468
4709
|
if (bridge) {
|
|
@@ -4483,12 +4724,13 @@ var PtySession = class {
|
|
|
4483
4724
|
this.pty = pty;
|
|
4484
4725
|
}
|
|
4485
4726
|
async deliverPrompt(text) {
|
|
4486
|
-
this.
|
|
4727
|
+
if (text === "" && !this.adapter.capabilities.prefill) return;
|
|
4728
|
+
this.writeStdin(this.adapter.encodePromptBytes(text));
|
|
4487
4729
|
if (this.turn.promptDelivery === "prefill") return;
|
|
4488
4730
|
await sleep3(SUBMIT_SETTLE_MS);
|
|
4489
4731
|
if (this._toreDown) return;
|
|
4490
4732
|
this.writeStdin("\r");
|
|
4491
|
-
this.armSubmitNudge();
|
|
4733
|
+
if (this.adapter.capabilities.structuredEvents) this.armSubmitNudge();
|
|
4492
4734
|
}
|
|
4493
4735
|
async feedPrompt() {
|
|
4494
4736
|
if (typeof this.turnPrompt === "string") {
|
|
@@ -4497,7 +4739,7 @@ var PtySession = class {
|
|
|
4497
4739
|
}
|
|
4498
4740
|
for await (const message of this.turnPrompt) {
|
|
4499
4741
|
const content = message.message.content;
|
|
4500
|
-
const text = typeof content === "string" ? content :
|
|
4742
|
+
const text = typeof content === "string" ? content : renderPromptContentText(content);
|
|
4501
4743
|
await this.deliverPrompt(text);
|
|
4502
4744
|
}
|
|
4503
4745
|
}
|
|
@@ -4645,11 +4887,22 @@ var PtySession = class {
|
|
|
4645
4887
|
this.tailer.close();
|
|
4646
4888
|
await this.tailer.flush();
|
|
4647
4889
|
}
|
|
4648
|
-
if (!this.
|
|
4890
|
+
if (!this.adapter.capabilities.structuredEvents) {
|
|
4891
|
+
if (exitCode === 0) {
|
|
4892
|
+
this.pushEvent({ type: "result", subtype: "success", result: "", total_cost_usd: 0 });
|
|
4893
|
+
} else {
|
|
4894
|
+
this.pushEvent({
|
|
4895
|
+
type: "result",
|
|
4896
|
+
subtype: "error",
|
|
4897
|
+
errors: this.adapter.buildExitErrors(exitCode, this.recentOutput)
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
this.endTurn(exitCode === 0);
|
|
4901
|
+
} else if (!this.sawResult && this.activeQueue) {
|
|
4649
4902
|
this.activeQueue.push({
|
|
4650
4903
|
type: "result",
|
|
4651
4904
|
subtype: "error",
|
|
4652
|
-
errors: buildExitErrors(exitCode, this.recentOutput
|
|
4905
|
+
errors: this.adapter.buildExitErrors(exitCode, this.recentOutput)
|
|
4653
4906
|
});
|
|
4654
4907
|
}
|
|
4655
4908
|
for (const listener of this.exitListeners) listener(exitCode);
|
|
@@ -4658,191 +4911,29 @@ var PtySession = class {
|
|
|
4658
4911
|
}
|
|
4659
4912
|
};
|
|
4660
4913
|
|
|
4661
|
-
// src/harness/pty/credentials.ts
|
|
4662
|
-
import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
4663
|
-
import { homedir as homedir2 } from "os";
|
|
4664
|
-
import { join as join5 } from "path";
|
|
4665
|
-
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
4666
|
-
var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4667
|
-
function claudeCredentialsPath() {
|
|
4668
|
-
return join5(claudeConfigHome(), ".credentials.json");
|
|
4669
|
-
}
|
|
4670
|
-
function isConveyorCloudEnv(env = process.env) {
|
|
4671
|
-
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
4672
|
-
}
|
|
4673
|
-
function parseClaudeAiOauth(raw) {
|
|
4674
|
-
if (!raw || raw.trim() === "") return null;
|
|
4675
|
-
try {
|
|
4676
|
-
const parsed = JSON.parse(raw);
|
|
4677
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4678
|
-
const oauth = parsed.claudeAiOauth;
|
|
4679
|
-
if (typeof oauth !== "object" || oauth === null) return null;
|
|
4680
|
-
const record = oauth;
|
|
4681
|
-
return {
|
|
4682
|
-
accessToken: record.accessToken,
|
|
4683
|
-
refreshToken: record.refreshToken,
|
|
4684
|
-
expiresAt: record.expiresAt
|
|
4685
|
-
};
|
|
4686
|
-
} catch {
|
|
4687
|
-
return null;
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4690
|
-
function buildSynthesizedCredentials(token, now) {
|
|
4691
|
-
return JSON.stringify({
|
|
4692
|
-
claudeAiOauth: {
|
|
4693
|
-
accessToken: token,
|
|
4694
|
-
expiresAt: now + SYNTH_TOKEN_TTL_MS,
|
|
4695
|
-
scopes: ["user:inference", "user:profile"],
|
|
4696
|
-
subscriptionType: "max"
|
|
4697
|
-
}
|
|
4698
|
-
});
|
|
4699
|
-
}
|
|
4700
|
-
function planCredentialsWrite(input) {
|
|
4701
|
-
if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
|
|
4702
|
-
if (!input.token) return { action: "skip", reason: "no-token" };
|
|
4703
|
-
const contents = buildSynthesizedCredentials(input.token, input.now);
|
|
4704
|
-
const existing = parseClaudeAiOauth(input.existingRaw);
|
|
4705
|
-
if (!existing) return { action: "write", contents };
|
|
4706
|
-
if (typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
4707
|
-
return { action: "skip", reason: "foreign-credentials" };
|
|
4708
|
-
}
|
|
4709
|
-
const fresh = existing.accessToken === input.token && typeof existing.expiresAt === "number" && existing.expiresAt > input.now + REFRESH_SKEW_MS;
|
|
4710
|
-
if (fresh) return { action: "skip", reason: "current" };
|
|
4711
|
-
return { action: "write", contents };
|
|
4712
|
-
}
|
|
4713
|
-
async function readRaw(path4) {
|
|
4714
|
-
try {
|
|
4715
|
-
return await readFile(path4, "utf8");
|
|
4716
|
-
} catch {
|
|
4717
|
-
return null;
|
|
4718
|
-
}
|
|
4719
|
-
}
|
|
4720
|
-
async function ensureClaudeCredentials(env = process.env) {
|
|
4721
|
-
try {
|
|
4722
|
-
const path4 = claudeCredentialsPath();
|
|
4723
|
-
const plan = planCredentialsWrite({
|
|
4724
|
-
isCloud: isConveyorCloudEnv(env),
|
|
4725
|
-
token: env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
4726
|
-
existingRaw: await readRaw(path4),
|
|
4727
|
-
now: Date.now()
|
|
4728
|
-
});
|
|
4729
|
-
if (plan.action === "skip") return;
|
|
4730
|
-
await mkdir3(claudeConfigHome(), { recursive: true });
|
|
4731
|
-
await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
|
|
4732
|
-
await chmod2(path4, 384).catch(() => {
|
|
4733
|
-
});
|
|
4734
|
-
} catch (err) {
|
|
4735
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4736
|
-
process.stderr.write(`[conveyor-agent] claude credentials sync failed: ${message}
|
|
4737
|
-
`);
|
|
4738
|
-
}
|
|
4739
|
-
}
|
|
4740
|
-
function claudeJsonPath() {
|
|
4741
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
4742
|
-
return configDir ? join5(configDir, ".claude.json") : join5(homedir2(), ".claude.json");
|
|
4743
|
-
}
|
|
4744
|
-
function asRecord(value) {
|
|
4745
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
4746
|
-
}
|
|
4747
|
-
function parseClaudeJson(existingRaw) {
|
|
4748
|
-
if (!existingRaw || existingRaw.trim() === "") return {};
|
|
4749
|
-
try {
|
|
4750
|
-
return asRecord(JSON.parse(existingRaw));
|
|
4751
|
-
} catch {
|
|
4752
|
-
return {};
|
|
4753
|
-
}
|
|
4754
|
-
}
|
|
4755
|
-
function seedWorkspaceTrust(config, trustCwd) {
|
|
4756
|
-
const projects = asRecord(config.projects);
|
|
4757
|
-
const entry = asRecord(projects[trustCwd]);
|
|
4758
|
-
if (entry.hasTrustDialogAccepted === true) return false;
|
|
4759
|
-
entry.hasTrustDialogAccepted = true;
|
|
4760
|
-
projects[trustCwd] = entry;
|
|
4761
|
-
config.projects = projects;
|
|
4762
|
-
return true;
|
|
4763
|
-
}
|
|
4764
|
-
function planClaudeJsonSeed(existingRaw, trustCwd) {
|
|
4765
|
-
const config = parseClaudeJson(existingRaw);
|
|
4766
|
-
const isFresh = Object.keys(config).length === 0;
|
|
4767
|
-
let changed = false;
|
|
4768
|
-
if (config.hasCompletedOnboarding !== true) {
|
|
4769
|
-
config.hasCompletedOnboarding = true;
|
|
4770
|
-
changed = true;
|
|
4771
|
-
}
|
|
4772
|
-
if (config.bypassPermissionsModeAccepted !== true) {
|
|
4773
|
-
config.bypassPermissionsModeAccepted = true;
|
|
4774
|
-
changed = true;
|
|
4775
|
-
}
|
|
4776
|
-
if (isFresh && typeof config.theme !== "string") {
|
|
4777
|
-
config.theme = "dark";
|
|
4778
|
-
changed = true;
|
|
4779
|
-
}
|
|
4780
|
-
if (trustCwd && seedWorkspaceTrust(config, trustCwd)) {
|
|
4781
|
-
changed = true;
|
|
4782
|
-
}
|
|
4783
|
-
return changed ? JSON.stringify(config) : null;
|
|
4784
|
-
}
|
|
4785
|
-
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
4786
|
-
try {
|
|
4787
|
-
if (!isConveyorCloudEnv(env)) return;
|
|
4788
|
-
const path4 = claudeJsonPath();
|
|
4789
|
-
const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
|
|
4790
|
-
if (contents === null) return;
|
|
4791
|
-
await writeFile4(path4, contents, "utf8");
|
|
4792
|
-
const verify = await readRaw(path4);
|
|
4793
|
-
if (verify === contents) {
|
|
4794
|
-
process.stderr.write(
|
|
4795
|
-
`[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
|
|
4796
|
-
`
|
|
4797
|
-
);
|
|
4798
|
-
} else {
|
|
4799
|
-
process.stderr.write(
|
|
4800
|
-
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
|
|
4801
|
-
`
|
|
4802
|
-
);
|
|
4803
|
-
}
|
|
4804
|
-
} catch (err) {
|
|
4805
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4806
|
-
process.stderr.write(`[conveyor-agent] claude onboarding seed failed: ${message}
|
|
4807
|
-
`);
|
|
4808
|
-
}
|
|
4809
|
-
}
|
|
4810
|
-
async function removeConveyorCredentials(env = process.env) {
|
|
4811
|
-
try {
|
|
4812
|
-
if (!isConveyorCloudEnv(env)) return;
|
|
4813
|
-
const path4 = claudeCredentialsPath();
|
|
4814
|
-
const existing = parseClaudeAiOauth(await readRaw(path4));
|
|
4815
|
-
if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
4816
|
-
return;
|
|
4817
|
-
}
|
|
4818
|
-
await rm5(path4, { force: true });
|
|
4819
|
-
} catch (err) {
|
|
4820
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4821
|
-
process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
|
|
4822
|
-
`);
|
|
4823
|
-
}
|
|
4824
|
-
}
|
|
4825
|
-
|
|
4826
4914
|
// src/harness/pty/index.ts
|
|
4827
4915
|
var ENDED_GRACE_MS = 4e3;
|
|
4828
|
-
function fingerprintOf(options) {
|
|
4829
|
-
return spawnOptionsFingerprint({
|
|
4830
|
-
model: options.model,
|
|
4831
|
-
permissionMode: options.permissionMode,
|
|
4832
|
-
...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
|
|
4833
|
-
cwd: options.cwd
|
|
4834
|
-
});
|
|
4835
|
-
}
|
|
4836
4916
|
var PtyHarness = class {
|
|
4837
4917
|
/**
|
|
4838
4918
|
* `bridge` relays raw terminal I/O to/from the S2 server (and on to the S5
|
|
4839
4919
|
* terminal). It is undefined for SDK-only callers and PTY runs that never
|
|
4840
4920
|
* attach a relay; the session simply discards stdout in that case.
|
|
4841
4921
|
*/
|
|
4842
|
-
constructor(bridge) {
|
|
4922
|
+
constructor(bridge, adapter = new ClaudeTuiAdapter()) {
|
|
4843
4923
|
this.bridge = bridge;
|
|
4924
|
+
this.adapter = adapter;
|
|
4844
4925
|
}
|
|
4845
4926
|
bridge;
|
|
4927
|
+
adapter;
|
|
4928
|
+
/** Fingerprint of the spawn-time options a reused process cannot change. */
|
|
4929
|
+
fingerprintOf(options) {
|
|
4930
|
+
return this.adapter.spawnFingerprint({
|
|
4931
|
+
model: options.model,
|
|
4932
|
+
permissionMode: options.permissionMode,
|
|
4933
|
+
...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
|
|
4934
|
+
cwd: options.cwd
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4846
4937
|
/** The session currently streaming events, if any — repaint target. */
|
|
4847
4938
|
activeSession = null;
|
|
4848
4939
|
/** A completed-but-alive session kept for the next turn (keep-alive). */
|
|
@@ -4861,7 +4952,7 @@ var PtyHarness = class {
|
|
|
4861
4952
|
}
|
|
4862
4953
|
async *executeQuery(opts) {
|
|
4863
4954
|
const want = opts.resume ?? opts.options.resume;
|
|
4864
|
-
const fingerprint = fingerprintOf(opts.options);
|
|
4955
|
+
const fingerprint = this.fingerprintOf(opts.options);
|
|
4865
4956
|
let session;
|
|
4866
4957
|
if (this.parked?.canReuse(want, fingerprint)) {
|
|
4867
4958
|
session = this.parked;
|
|
@@ -4875,9 +4966,8 @@ var PtyHarness = class {
|
|
|
4875
4966
|
this.cancelEndedTimer();
|
|
4876
4967
|
await stale.teardown();
|
|
4877
4968
|
}
|
|
4878
|
-
session = new PtySession(opts.prompt, opts.options, want, this.bridge);
|
|
4879
|
-
await
|
|
4880
|
-
await ensureClaudeOnboarding(process.env, opts.options.cwd);
|
|
4969
|
+
session = new PtySession(opts.prompt, opts.options, want, this.bridge, this.adapter);
|
|
4970
|
+
await this.adapter.prepareEnvironment({ cwd: opts.options.cwd });
|
|
4881
4971
|
session.onExit(() => this.handleSessionExit(session));
|
|
4882
4972
|
await session.start();
|
|
4883
4973
|
}
|
|
@@ -4995,7 +5085,7 @@ function createHarness(kind = "sdk", ptyBridge) {
|
|
|
4995
5085
|
|
|
4996
5086
|
// src/execution/query-executor.ts
|
|
4997
5087
|
import { createHash } from "crypto";
|
|
4998
|
-
import { existsSync
|
|
5088
|
+
import { existsSync as existsSync2, readFileSync, truncateSync } from "fs";
|
|
4999
5089
|
|
|
5000
5090
|
// src/execution/pack-runner-prompt.ts
|
|
5001
5091
|
function findLastAgentMessageIndex(history) {
|
|
@@ -5150,8 +5240,9 @@ function formatChatFile(file) {
|
|
|
5150
5240
|
return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]: ${file.downloadUrl}`];
|
|
5151
5241
|
}
|
|
5152
5242
|
if (file.content && file.contentEncoding === "base64") {
|
|
5243
|
+
const link = file.downloadUrl ? ` or download: ${file.downloadUrl}` : "";
|
|
5153
5244
|
return [
|
|
5154
|
-
`[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_attachment("${file.fileId}") to view]`
|
|
5245
|
+
`[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_attachment("${file.fileId}") to view${link}]`
|
|
5155
5246
|
];
|
|
5156
5247
|
}
|
|
5157
5248
|
return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`];
|
|
@@ -5163,8 +5254,9 @@ function formatTaskFile(file) {
|
|
|
5163
5254
|
}
|
|
5164
5255
|
if (file.content && file.contentEncoding === "base64") {
|
|
5165
5256
|
const size = formatFileSize(file.fileSize);
|
|
5257
|
+
const link = file.downloadUrl ? ` or download: ${file.downloadUrl}` : "";
|
|
5166
5258
|
return [
|
|
5167
|
-
`- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_attachment("${file.fileId}") to view]`
|
|
5259
|
+
`- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_attachment("${file.fileId}") to view${link}]`
|
|
5168
5260
|
];
|
|
5169
5261
|
}
|
|
5170
5262
|
if (!file.content) {
|
|
@@ -5647,7 +5739,7 @@ function buildDiscoveryPrompt(context, runnerMode) {
|
|
|
5647
5739
|
## Mode: Discovery`,
|
|
5648
5740
|
`You are in Discovery mode \u2014 helping plan and scope this task.`,
|
|
5649
5741
|
`- 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`,
|
|
5742
|
+
`- 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
5743
|
`- Do NOT attempt to edit, write, or modify source code files \u2014 these operations will be denied`,
|
|
5652
5744
|
`- If you identify code changes needed, describe them in the plan instead of implementing them`,
|
|
5653
5745
|
`- You can create and manage subtasks`,
|
|
@@ -5697,7 +5789,7 @@ function buildDiscoveryPrompt(context, runnerMode) {
|
|
|
5697
5789
|
``,
|
|
5698
5790
|
`### Plan Verification Requirements`,
|
|
5699
5791
|
`Every plan MUST include a **Testing / Verification** section so the build agent has a clear definition of "done". Enumerate:`,
|
|
5700
|
-
`- The
|
|
5792
|
+
`- 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
5793
|
`- Any task-specific end-to-end checks (manual UI walk-through, API smoke test, migration dry-run, etc.)`,
|
|
5702
5794
|
`You are NOT expected to run these gates yourself \u2014 discovery is read-only. Just describe them in the plan.`,
|
|
5703
5795
|
``,
|
|
@@ -5776,14 +5868,14 @@ function buildModePrompt(agentMode, context, runnerMode) {
|
|
|
5776
5868
|
`- Goal: coordinate child task execution and ensure all children complete successfully`
|
|
5777
5869
|
] : [
|
|
5778
5870
|
`- If this is a leaf task (no children): execute the plan directly`,
|
|
5779
|
-
`- Goal: implement the plan,
|
|
5871
|
+
`- Goal: implement the plan, run scoped verification, open a PR when done`,
|
|
5780
5872
|
``,
|
|
5781
5873
|
`### 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.`,
|
|
5874
|
+
`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:`,
|
|
5875
|
+
`1. \`bun run check\` \u2014 lint + typecheck (fast; run whenever you changed code)`,
|
|
5876
|
+
`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)`,
|
|
5877
|
+
`Docs/markdown/.claude-only changes need NO local gates \u2014 open the PR and let CI validate.`,
|
|
5878
|
+
`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
5879
|
`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
5880
|
]
|
|
5789
5881
|
];
|
|
@@ -5884,12 +5976,13 @@ function buildPmPreamble(context) {
|
|
|
5884
5976
|
`
|
|
5885
5977
|
Environment (ready, no setup required):`,
|
|
5886
5978
|
`- Repository is cloned at your current working directory.`,
|
|
5979
|
+
`- 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
5980
|
`- You can read files and run git commands to understand the codebase before writing task plans.`,
|
|
5888
5981
|
`- 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
5982
|
`
|
|
5890
5983
|
Workflow:`,
|
|
5891
|
-
`- You can draft and iterate on plans in .claude/plans/*.md
|
|
5892
|
-
`-
|
|
5984
|
+
`- You can draft and iterate on plans in .claude/plans/*.md, but files on disk are NOT synced to the task.`,
|
|
5985
|
+
`- Save the plan to the task with update_task_plan \u2014 this is the only way the plan is persisted.`,
|
|
5893
5986
|
`- 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
5987
|
`- A separate task agent will handle execution after the team reviews and approves your plan.`
|
|
5895
5988
|
];
|
|
@@ -5922,6 +6015,7 @@ Environment (ready, no setup required):`,
|
|
|
5922
6015
|
`- Repository is cloned at your current working directory.`,
|
|
5923
6016
|
`- You can read, write, and edit files directly.`,
|
|
5924
6017
|
`- You can run shell commands including git, build tools, and test runners.`,
|
|
6018
|
+
`- 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
6019
|
context.githubBranch ? `- You are working on branch: \`${context.githubBranch}\`` : "",
|
|
5926
6020
|
`
|
|
5927
6021
|
Safety rules:`,
|
|
@@ -5946,6 +6040,10 @@ Environment (fully ready \u2014 do NOT verify or set up):`,
|
|
|
5946
6040
|
`- Branch \`${context.githubBranch}\` is already checked out.`,
|
|
5947
6041
|
`- All dependencies are installed, database is migrated, and the dev server is running.`,
|
|
5948
6042
|
`- Git is configured. Commit and push directly to this branch.`,
|
|
6043
|
+
`- 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\`.`,
|
|
6044
|
+
`- 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.`,
|
|
6045
|
+
`- The \`gh\` CLI may not be installed \u2014 use the mcp__conveyor__* tools for PR and task state.`,
|
|
6046
|
+
`- 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
6047
|
`
|
|
5950
6048
|
IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the following:`,
|
|
5951
6049
|
`- bun/npm install, pip install, or any dependency installation`,
|
|
@@ -6208,7 +6306,7 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
|
|
|
6208
6306
|
`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
6307
|
`Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
|
|
6210
6308
|
`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,
|
|
6309
|
+
`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
6310
|
`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
6311
|
];
|
|
6214
6312
|
if (isAutoMode) {
|
|
@@ -6280,7 +6378,7 @@ New messages since your last run:`,
|
|
|
6280
6378
|
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
6281
6379
|
`
|
|
6282
6380
|
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-
|
|
6381
|
+
`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
6382
|
`Implement your updates and open a PR when finished.`
|
|
6285
6383
|
];
|
|
6286
6384
|
if (context.githubPRUrl) {
|
|
@@ -6723,11 +6821,11 @@ function buildCreatePullRequestTool(connection, config) {
|
|
|
6723
6821
|
async ({ title, body, branch, baseBranch, commitMessage, skipVerify }) => {
|
|
6724
6822
|
try {
|
|
6725
6823
|
const cwd = config.workspaceDir;
|
|
6726
|
-
if (hasUncommittedChanges(cwd)) {
|
|
6824
|
+
if (await hasUncommittedChanges(cwd)) {
|
|
6727
6825
|
const message = commitMessage || `${title}
|
|
6728
6826
|
|
|
6729
6827
|
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
|
|
6730
|
-
const commitHash = stageAndCommit(cwd, message);
|
|
6828
|
+
const commitHash = await stageAndCommit(cwd, message);
|
|
6731
6829
|
if (commitHash) {
|
|
6732
6830
|
connection.sendEvent({
|
|
6733
6831
|
type: "message",
|
|
@@ -6739,7 +6837,7 @@ Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
|
|
|
6739
6837
|
);
|
|
6740
6838
|
}
|
|
6741
6839
|
}
|
|
6742
|
-
if (hasUnpushedCommits(cwd)) {
|
|
6840
|
+
if (await hasUnpushedCommits(cwd)) {
|
|
6743
6841
|
const pushSuccess = await pushToOrigin(
|
|
6744
6842
|
cwd,
|
|
6745
6843
|
async () => {
|
|
@@ -6938,7 +7036,7 @@ function buildMutationTools(connection, config) {
|
|
|
6938
7036
|
|
|
6939
7037
|
// src/tools/attachment-tools.ts
|
|
6940
7038
|
import { readFile as readFile3, stat as stat5 } from "fs/promises";
|
|
6941
|
-
import { basename, extname, isAbsolute, join as
|
|
7039
|
+
import { basename, extname, isAbsolute, join as join5 } from "path";
|
|
6942
7040
|
import { z as z6 } from "zod";
|
|
6943
7041
|
var IMAGE_MIME_BY_EXT = {
|
|
6944
7042
|
".png": "image/png",
|
|
@@ -6957,7 +7055,7 @@ function buildUploadAttachmentTool(connection, config) {
|
|
|
6957
7055
|
},
|
|
6958
7056
|
async ({ path: path4, title }) => {
|
|
6959
7057
|
try {
|
|
6960
|
-
const filePath = isAbsolute(path4) ? path4 :
|
|
7058
|
+
const filePath = isAbsolute(path4) ? path4 : join5(config.workspaceDir, path4);
|
|
6961
7059
|
const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
6962
7060
|
if (!mimeType) {
|
|
6963
7061
|
return textResult(
|
|
@@ -8092,7 +8190,6 @@ async function handleExitPlanMode(host, input) {
|
|
|
8092
8190
|
return { behavior: "allow", updatedInput: input };
|
|
8093
8191
|
}
|
|
8094
8192
|
try {
|
|
8095
|
-
await host.syncPlanFile();
|
|
8096
8193
|
const taskProps = await host.connection.getTaskProperties();
|
|
8097
8194
|
const missingProps = collectMissingProps(taskProps);
|
|
8098
8195
|
if (host.isParentTask) {
|
|
@@ -8362,7 +8459,7 @@ function sessionLineageKey(taskId, agentMode, runnerMode) {
|
|
|
8362
8459
|
}
|
|
8363
8460
|
function sessionFileExists(sessionUuid, cwd) {
|
|
8364
8461
|
try {
|
|
8365
|
-
return
|
|
8462
|
+
return existsSync2(sessionTranscriptPath(cwd, sessionUuid));
|
|
8366
8463
|
} catch {
|
|
8367
8464
|
return false;
|
|
8368
8465
|
}
|
|
@@ -8381,8 +8478,8 @@ function resolveSessionStart(lineageKey, cwd) {
|
|
|
8381
8478
|
}
|
|
8382
8479
|
function repairTornSessionFile(path4) {
|
|
8383
8480
|
try {
|
|
8384
|
-
if (!
|
|
8385
|
-
const content =
|
|
8481
|
+
if (!existsSync2(path4)) return false;
|
|
8482
|
+
const content = readFileSync(path4, "utf8");
|
|
8386
8483
|
if (content.length === 0) return false;
|
|
8387
8484
|
let keepEnd = content.length;
|
|
8388
8485
|
if (!content.endsWith("\n")) {
|
|
@@ -8526,14 +8623,21 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
|
|
|
8526
8623
|
|
|
8527
8624
|
The team says:
|
|
8528
8625
|
${followUpText}` : followUpText;
|
|
8626
|
+
const isPty = host.harnessKind === "pty";
|
|
8529
8627
|
if (isPmMode) {
|
|
8530
|
-
const prompt = buildMultimodalPrompt(textPrompt, context);
|
|
8628
|
+
const prompt = buildMultimodalPrompt(textPrompt, context, isPty);
|
|
8531
8629
|
if (followUpImages.length > 0 && Array.isArray(prompt)) {
|
|
8532
8630
|
prompt.push(...followUpImages);
|
|
8533
8631
|
}
|
|
8534
8632
|
return prompt;
|
|
8535
8633
|
}
|
|
8536
8634
|
if (followUpImages.length > 0) {
|
|
8635
|
+
if (isPty) {
|
|
8636
|
+
const refs = followUpImages.map(
|
|
8637
|
+
() => `[Image attachment \u2014 use list_task_files / get_attachment to view]`
|
|
8638
|
+
);
|
|
8639
|
+
return [textPrompt, ...refs].join("\n");
|
|
8640
|
+
}
|
|
8537
8641
|
return [{ type: "text", text: textPrompt }, ...followUpImages];
|
|
8538
8642
|
}
|
|
8539
8643
|
return textPrompt;
|
|
@@ -8574,10 +8678,6 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
|
|
|
8574
8678
|
if (host.isStopped()) return;
|
|
8575
8679
|
const mode = host.agentMode;
|
|
8576
8680
|
const isDiscoveryLike = mode === "discovery" || mode === "help";
|
|
8577
|
-
const needsPlanSync = isDiscoveryLike || mode === "auto" && !host.hasExitedPlanMode;
|
|
8578
|
-
if (needsPlanSync) {
|
|
8579
|
-
host.snapshotPlanFiles();
|
|
8580
|
-
}
|
|
8581
8681
|
const sessionStart = resolveSessionStart(
|
|
8582
8682
|
sessionLineageKey(context.taskId, mode, host.config.mode),
|
|
8583
8683
|
host.config.workspaceDir
|
|
@@ -8599,14 +8699,12 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
|
|
|
8599
8699
|
const resume = sessionStart.resume;
|
|
8600
8700
|
if (followUpContent) {
|
|
8601
8701
|
await runFollowUpQuery(host, context, options, resume, followUpContent);
|
|
8602
|
-
} else if (isDiscoveryLike && promptDelivery !== "prefill") {
|
|
8603
8702
|
return;
|
|
8604
|
-
} else {
|
|
8605
|
-
await runInitialQuery(host, context, options, resume, promptDelivery);
|
|
8606
8703
|
}
|
|
8607
|
-
if (
|
|
8608
|
-
|
|
8704
|
+
if (isDiscoveryLike && promptDelivery !== "prefill") {
|
|
8705
|
+
return;
|
|
8609
8706
|
}
|
|
8707
|
+
await runInitialQuery(host, context, options, resume, promptDelivery);
|
|
8610
8708
|
}
|
|
8611
8709
|
async function runFollowUpQuery(host, context, options, resume, followUpContent) {
|
|
8612
8710
|
if (options.promptDelivery === "prefill") {
|
|
@@ -8675,7 +8773,7 @@ function latestUserMessageText(context) {
|
|
|
8675
8773
|
}
|
|
8676
8774
|
return null;
|
|
8677
8775
|
}
|
|
8678
|
-
function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAppendSystemPrompt) {
|
|
8776
|
+
function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAppendSystemPrompt, harnessKind) {
|
|
8679
8777
|
if (promptDelivery === "prefill") {
|
|
8680
8778
|
return {
|
|
8681
8779
|
prompt: latestUserMessageText(context) ?? "",
|
|
@@ -8683,7 +8781,9 @@ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAp
|
|
|
8683
8781
|
};
|
|
8684
8782
|
}
|
|
8685
8783
|
return {
|
|
8686
|
-
|
|
8784
|
+
// On the PTY harness image blocks would be pasted into the TUI as text —
|
|
8785
|
+
// the prompt body already links each image (get_attachment / downloadUrl).
|
|
8786
|
+
prompt: buildMultimodalPrompt(initialPrompt, context, harnessKind === "pty"),
|
|
8687
8787
|
appendSystemPrompt: baseAppendSystemPrompt
|
|
8688
8788
|
};
|
|
8689
8789
|
}
|
|
@@ -8698,7 +8798,8 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
|
|
|
8698
8798
|
promptDelivery,
|
|
8699
8799
|
initialPrompt,
|
|
8700
8800
|
context,
|
|
8701
|
-
options.appendSystemPrompt
|
|
8801
|
+
options.appendSystemPrompt,
|
|
8802
|
+
host.harnessKind
|
|
8702
8803
|
);
|
|
8703
8804
|
const queryOptions = { ...options, appendSystemPrompt };
|
|
8704
8805
|
let agentQuery = host.harness.executeQuery({
|
|
@@ -8723,7 +8824,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
8723
8824
|
const retryPrompt = buildMultimodalPrompt(
|
|
8724
8825
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
8725
8826
|
context,
|
|
8726
|
-
lastErrorWasImage
|
|
8827
|
+
lastErrorWasImage || host.harnessKind === "pty"
|
|
8727
8828
|
);
|
|
8728
8829
|
return host.harness.executeQuery({
|
|
8729
8830
|
prompt: host.createInputStream(retryPrompt),
|
|
@@ -8751,7 +8852,8 @@ async function handleAuthError(context, host, options) {
|
|
|
8751
8852
|
host.connection.storeSessionId("");
|
|
8752
8853
|
const freshPrompt = buildMultimodalPrompt(
|
|
8753
8854
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
8754
|
-
context
|
|
8855
|
+
context,
|
|
8856
|
+
host.harnessKind === "pty"
|
|
8755
8857
|
);
|
|
8756
8858
|
const freshQuery = host.harness.executeQuery({
|
|
8757
8859
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -8765,7 +8867,8 @@ async function handleStaleSession(context, host, options) {
|
|
|
8765
8867
|
host.connection.storeSessionId("");
|
|
8766
8868
|
const freshPrompt = buildMultimodalPrompt(
|
|
8767
8869
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
8768
|
-
context
|
|
8870
|
+
context,
|
|
8871
|
+
host.harnessKind === "pty"
|
|
8769
8872
|
);
|
|
8770
8873
|
const freshQuery = host.harness.executeQuery({
|
|
8771
8874
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -8991,13 +9094,14 @@ function resolveHarnessKind() {
|
|
|
8991
9094
|
function buildPtyBridge(connection) {
|
|
8992
9095
|
return {
|
|
8993
9096
|
sendOutput: (data, dims) => connection.sendPtyOutput(data, dims),
|
|
9097
|
+
sendChatEvent: (event) => connection.sendPtyChatEvent(event),
|
|
8994
9098
|
sendEnded: () => connection.sendPtyEnded(),
|
|
8995
9099
|
onInput: (handler) => connection.onPtyInput(handler),
|
|
8996
9100
|
onResize: (handler) => connection.onPtyResize(handler)
|
|
8997
9101
|
};
|
|
8998
9102
|
}
|
|
8999
9103
|
var QueryBridge = class {
|
|
9000
|
-
constructor(connection, mode, runnerConfig, callbacks
|
|
9104
|
+
constructor(connection, mode, runnerConfig, callbacks) {
|
|
9001
9105
|
this.connection = connection;
|
|
9002
9106
|
this.mode = mode;
|
|
9003
9107
|
this.runnerConfig = runnerConfig;
|
|
@@ -9008,7 +9112,6 @@ var QueryBridge = class {
|
|
|
9008
9112
|
harnessKind,
|
|
9009
9113
|
harnessKind === "pty" ? buildPtyBridge(connection) : void 0
|
|
9010
9114
|
);
|
|
9011
|
-
this.planSync = new PlanSync(workspaceDir, connection);
|
|
9012
9115
|
}
|
|
9013
9116
|
connection;
|
|
9014
9117
|
mode;
|
|
@@ -9018,7 +9121,6 @@ var QueryBridge = class {
|
|
|
9018
9121
|
/** Which harness drives this bridge ("pty" task chat; "sdk" only under the
|
|
9019
9122
|
* CONVEYOR_FORCE_SDK_CARDS maintainer override). */
|
|
9020
9123
|
harnessKind;
|
|
9021
|
-
planSync;
|
|
9022
9124
|
sessionIds = /* @__PURE__ */ new Map();
|
|
9023
9125
|
activeQuery = null;
|
|
9024
9126
|
pendingToolOutputs = [];
|
|
@@ -9219,8 +9321,6 @@ var QueryBridge = class {
|
|
|
9219
9321
|
if (bridge.onSoftStop) bridge.onSoftStop();
|
|
9220
9322
|
},
|
|
9221
9323
|
createInputStream: (prompt) => bridge.createInputStream(prompt),
|
|
9222
|
-
snapshotPlanFiles: () => bridge.planSync.snapshotPlanFiles(),
|
|
9223
|
-
syncPlanFile: () => bridge.planSync.syncPlanFile(),
|
|
9224
9324
|
onModeTransition: bridge.onModeTransition
|
|
9225
9325
|
};
|
|
9226
9326
|
}
|
|
@@ -9236,9 +9336,9 @@ var QueryBridge = class {
|
|
|
9236
9336
|
};
|
|
9237
9337
|
|
|
9238
9338
|
// src/runner/session-runner-helpers.ts
|
|
9239
|
-
import { readFileSync as
|
|
9240
|
-
import { dirname as dirname2, join as
|
|
9241
|
-
import { fileURLToPath } from "url";
|
|
9339
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
9340
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
9341
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9242
9342
|
function mapChatHistory(messages) {
|
|
9243
9343
|
if (!messages) return [];
|
|
9244
9344
|
return messages.map((m) => ({
|
|
@@ -9263,10 +9363,10 @@ function mapChatHistory(messages) {
|
|
|
9263
9363
|
}
|
|
9264
9364
|
function readAgentVersion() {
|
|
9265
9365
|
try {
|
|
9266
|
-
const here = dirname2(
|
|
9366
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
9267
9367
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
9268
9368
|
try {
|
|
9269
|
-
const pkg = JSON.parse(
|
|
9369
|
+
const pkg = JSON.parse(readFileSync2(join6(here, rel), "utf-8"));
|
|
9270
9370
|
if (pkg.version) return pkg.version;
|
|
9271
9371
|
} catch {
|
|
9272
9372
|
}
|
|
@@ -9352,7 +9452,7 @@ async function sampleKeyUsage(token) {
|
|
|
9352
9452
|
|
|
9353
9453
|
// src/runner/port-discovery.ts
|
|
9354
9454
|
import { readFile as readFile4 } from "fs/promises";
|
|
9355
|
-
import { execFile } from "child_process";
|
|
9455
|
+
import { execFile as execFile3 } from "child_process";
|
|
9356
9456
|
var PROC_TCP_LISTEN_STATE = "0A";
|
|
9357
9457
|
function parseProcNetTcpListeners(content) {
|
|
9358
9458
|
const ports = [];
|
|
@@ -9387,7 +9487,7 @@ async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
|
|
|
9387
9487
|
}
|
|
9388
9488
|
async function readNetstatListeningPorts() {
|
|
9389
9489
|
const output = await new Promise((resolve) => {
|
|
9390
|
-
|
|
9490
|
+
execFile3("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
|
|
9391
9491
|
resolve(err ? null : stdout);
|
|
9392
9492
|
});
|
|
9393
9493
|
});
|
|
@@ -9538,10 +9638,12 @@ var PortDiscovery = class {
|
|
|
9538
9638
|
};
|
|
9539
9639
|
|
|
9540
9640
|
// src/runner/parent-pull-handler.ts
|
|
9541
|
-
import {
|
|
9542
|
-
|
|
9641
|
+
import { execFile as execFile4 } from "child_process";
|
|
9642
|
+
import { promisify as promisify3 } from "util";
|
|
9643
|
+
var execFileAsync3 = promisify3(execFile4);
|
|
9644
|
+
async function handlePullBranch(workDir, branch) {
|
|
9543
9645
|
if (!branch) return;
|
|
9544
|
-
const current = getCurrentBranch(workDir);
|
|
9646
|
+
const current = await getCurrentBranch(workDir);
|
|
9545
9647
|
if (current !== branch) {
|
|
9546
9648
|
process.stderr.write(
|
|
9547
9649
|
`[conveyor-agent] pull_branch ignored \u2014 current branch ${current ?? "(detached)"} != ${branch}
|
|
@@ -9549,7 +9651,7 @@ function handlePullBranch(workDir, branch) {
|
|
|
9549
9651
|
);
|
|
9550
9652
|
return;
|
|
9551
9653
|
}
|
|
9552
|
-
if (hasUncommittedChanges(workDir)) {
|
|
9654
|
+
if (await hasUncommittedChanges(workDir)) {
|
|
9553
9655
|
process.stderr.write(
|
|
9554
9656
|
`[conveyor-agent] pull_branch ignored \u2014 uncommitted changes on ${branch}
|
|
9555
9657
|
`
|
|
@@ -9557,16 +9659,15 @@ function handlePullBranch(workDir, branch) {
|
|
|
9557
9659
|
return;
|
|
9558
9660
|
}
|
|
9559
9661
|
try {
|
|
9560
|
-
|
|
9662
|
+
await execFileAsync3("git", ["fetch", "origin", branch], { cwd: workDir, timeout: 6e4 });
|
|
9561
9663
|
} catch {
|
|
9562
9664
|
process.stderr.write(`[conveyor-agent] pull_branch: fetch failed for ${branch}
|
|
9563
9665
|
`);
|
|
9564
9666
|
return;
|
|
9565
9667
|
}
|
|
9566
9668
|
try {
|
|
9567
|
-
|
|
9669
|
+
await execFileAsync3("git", ["pull", "--ff-only", "origin", branch], {
|
|
9568
9670
|
cwd: workDir,
|
|
9569
|
-
stdio: "ignore",
|
|
9570
9671
|
timeout: 6e4
|
|
9571
9672
|
});
|
|
9572
9673
|
process.stderr.write(`[conveyor-agent] pull_branch: pulled origin/${branch}
|
|
@@ -9579,6 +9680,33 @@ function handlePullBranch(workDir, branch) {
|
|
|
9579
9680
|
}
|
|
9580
9681
|
}
|
|
9581
9682
|
|
|
9683
|
+
// src/runner/heavy-gate.ts
|
|
9684
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
9685
|
+
import path3 from "path";
|
|
9686
|
+
var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
|
|
9687
|
+
function runDir() {
|
|
9688
|
+
return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
|
|
9689
|
+
}
|
|
9690
|
+
function pidAlive(pid) {
|
|
9691
|
+
try {
|
|
9692
|
+
process.kill(pid, 0);
|
|
9693
|
+
return true;
|
|
9694
|
+
} catch {
|
|
9695
|
+
return false;
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9698
|
+
function isHeavyGateActive() {
|
|
9699
|
+
for (const key of GATE_KEYS) {
|
|
9700
|
+
try {
|
|
9701
|
+
const raw = readFileSync3(path3.join(runDir(), `${key}.pid`), "utf8").trim();
|
|
9702
|
+
const pid = Number.parseInt(raw, 10);
|
|
9703
|
+
if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
|
|
9704
|
+
} catch {
|
|
9705
|
+
}
|
|
9706
|
+
}
|
|
9707
|
+
return false;
|
|
9708
|
+
}
|
|
9709
|
+
|
|
9582
9710
|
// src/runner/session-runner.ts
|
|
9583
9711
|
function isPostToChatTool(name) {
|
|
9584
9712
|
return typeof name === "string" && (name === "post_to_chat" || name.endsWith("__post_to_chat"));
|
|
@@ -9616,8 +9744,14 @@ var SessionRunner = class _SessionRunner {
|
|
|
9616
9744
|
agentSpokeThisTurn = false;
|
|
9617
9745
|
/** Guards overlapping runs of the periodic git flush. */
|
|
9618
9746
|
periodicFlushInFlight = false;
|
|
9747
|
+
/** Consecutive flush ticks skipped because a heavy gate was running. */
|
|
9748
|
+
gateSkipCount = 0;
|
|
9749
|
+
/** Max consecutive gate-deferred ticks (~2 min each) before flushing anyway. */
|
|
9750
|
+
static MAX_GATE_SKIPS = 10;
|
|
9619
9751
|
/** Runtime preview-port poller (v3 dynamic port discovery). */
|
|
9620
9752
|
portDiscovery = null;
|
|
9753
|
+
/** Main event-loop lag measurement, shared with the heartbeat worker. */
|
|
9754
|
+
loopLag = new LoopLagMonitor();
|
|
9621
9755
|
constructor(config, callbacks) {
|
|
9622
9756
|
this.config = config;
|
|
9623
9757
|
this.callbacks = callbacks;
|
|
@@ -9626,7 +9760,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
9626
9760
|
this.mode = new ModeController(initialMode, config.runnerMode, config.isAuto);
|
|
9627
9761
|
const lifecycleConfig = { ...DEFAULT_LIFECYCLE_CONFIG, ...config.lifecycle };
|
|
9628
9762
|
this.lifecycle = new Lifecycle(lifecycleConfig, {
|
|
9629
|
-
onHeartbeat: () => this.connection.sendHeartbeat(),
|
|
9763
|
+
onHeartbeat: () => this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs()),
|
|
9630
9764
|
onIdleTimeout: () => {
|
|
9631
9765
|
process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
|
|
9632
9766
|
this.stopped = true;
|
|
@@ -9674,6 +9808,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
9674
9808
|
this.connection.sendEvent({ type: "connected", sessionId: this.sessionId });
|
|
9675
9809
|
this.wireConnectionCallbacks();
|
|
9676
9810
|
this.lifecycle.startHeartbeat();
|
|
9811
|
+
this.loopLag.start();
|
|
9812
|
+
this.connection.startHeartbeatWorker(this.loopLag.sharedBuffer);
|
|
9677
9813
|
this.lifecycle.startTokenRefresh();
|
|
9678
9814
|
this.lifecycle.startUsageSample();
|
|
9679
9815
|
const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
|
|
@@ -9736,10 +9872,10 @@ var SessionRunner = class _SessionRunner {
|
|
|
9736
9872
|
}
|
|
9737
9873
|
if (process.env.CONVEYOR_GIT_READY !== "1") {
|
|
9738
9874
|
if (this.fullContext?.githubBranch) {
|
|
9739
|
-
ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
|
|
9875
|
+
await ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
|
|
9740
9876
|
}
|
|
9741
9877
|
if (this.fullContext?.baseBranch) {
|
|
9742
|
-
syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
|
|
9878
|
+
await syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
|
|
9743
9879
|
}
|
|
9744
9880
|
}
|
|
9745
9881
|
if (!this.stopped) {
|
|
@@ -9759,7 +9895,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
9759
9895
|
);
|
|
9760
9896
|
}
|
|
9761
9897
|
} else {
|
|
9762
|
-
const restored = restoreWipSnapshot(
|
|
9898
|
+
const restored = await restoreWipSnapshot(
|
|
9763
9899
|
this.config.workspaceDir,
|
|
9764
9900
|
this.fullContext.githubBranch
|
|
9765
9901
|
);
|
|
@@ -10102,6 +10238,14 @@ var SessionRunner = class _SessionRunner {
|
|
|
10102
10238
|
* clean tree. Guarded so two ticks can't overlap. Never throws. */
|
|
10103
10239
|
async periodicGitFlush() {
|
|
10104
10240
|
if (this.periodicFlushInFlight || this.stopped) return;
|
|
10241
|
+
if (isHeavyGateActive() && this.gateSkipCount < _SessionRunner.MAX_GATE_SKIPS) {
|
|
10242
|
+
this.gateSkipCount++;
|
|
10243
|
+
if (this.gateSkipCount === 1) {
|
|
10244
|
+
process.stderr.write("[conveyor-agent] Periodic git flush deferred \u2014 heavy gate running\n");
|
|
10245
|
+
}
|
|
10246
|
+
return;
|
|
10247
|
+
}
|
|
10248
|
+
this.gateSkipCount = 0;
|
|
10105
10249
|
this.periodicFlushInFlight = true;
|
|
10106
10250
|
try {
|
|
10107
10251
|
const result = await flushAllPendingWork(this.config.workspaceDir, {
|
|
@@ -10200,6 +10344,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10200
10344
|
void this.queryBridge?.dispose().catch(() => {
|
|
10201
10345
|
});
|
|
10202
10346
|
this.portDiscovery?.stop();
|
|
10347
|
+
this.loopLag.stop();
|
|
10203
10348
|
this.lifecycle.destroy();
|
|
10204
10349
|
this.connection.disconnect();
|
|
10205
10350
|
if (this.inputResolver) {
|
|
@@ -10219,23 +10364,41 @@ var SessionRunner = class _SessionRunner {
|
|
|
10219
10364
|
}
|
|
10220
10365
|
// ── Auto-mode stuck detection & nudge ───────────────────────────────
|
|
10221
10366
|
static MAX_STUCK_NUDGES = 3;
|
|
10222
|
-
/** The
|
|
10223
|
-
*
|
|
10367
|
+
/** The build-phase nudge (In Progress, no PR). Offers BOTH escape routes so
|
|
10368
|
+
* the agent never just goes idle. */
|
|
10224
10369
|
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.";
|
|
10370
|
+
/** The planning-phase nudge (still Planning, identification/plan unfinished).
|
|
10371
|
+
* Same two-escape-route shape, aimed at finishing discovery instead of a PR. */
|
|
10372
|
+
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
10373
|
/**
|
|
10226
|
-
*
|
|
10227
|
-
* post-turn, so the TUI is no longer
|
|
10228
|
-
*
|
|
10229
|
-
*
|
|
10230
|
-
*
|
|
10374
|
+
* Classify how an auto task is "stuck" after its turn ended (call sites run
|
|
10375
|
+
* post-turn, so the TUI is no longer working), or null if it isn't. Shared
|
|
10376
|
+
* guards: only auto, not rate-limited, under the nudge cap, and the agent
|
|
10377
|
+
* stayed silent this turn — if it posted to chat (explained itself / asked for
|
|
10378
|
+
* help) it's waiting on a human, not silently stuck.
|
|
10379
|
+
*
|
|
10380
|
+
* - "pr": In Progress with no open PR (build finished without shipping).
|
|
10381
|
+
* - "planning": still Planning without (identified + saved plan). A fresh auto
|
|
10382
|
+
* agent that finishes its plan turn WITHOUT ExitPlanMode would otherwise idle
|
|
10383
|
+
* → clean-exit → its session Ends → the workspace is reaped "agent_gone"
|
|
10384
|
+
* before a plan ever landed. Nudging (and, on exhaustion, going dormant)
|
|
10385
|
+
* keeps the session Active so the pod is never prematurely slept.
|
|
10231
10386
|
*/
|
|
10387
|
+
autoStuckKind() {
|
|
10388
|
+
if (!this.mode.isAuto || this.stopped) return null;
|
|
10389
|
+
if (this.queryBridge?.wasRateLimited) return null;
|
|
10390
|
+
if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) return null;
|
|
10391
|
+
if (this.agentSpokeThisTurn) return null;
|
|
10392
|
+
if (!this.taskContext) return null;
|
|
10393
|
+
const { status, storyPointId, plan, githubPRUrl } = this.taskContext;
|
|
10394
|
+
if (status === "InProgress" && !githubPRUrl) return "pr";
|
|
10395
|
+
if (status === "Planning" && !(storyPointId !== null && !!plan?.trim())) {
|
|
10396
|
+
return "planning";
|
|
10397
|
+
}
|
|
10398
|
+
return null;
|
|
10399
|
+
}
|
|
10232
10400
|
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;
|
|
10401
|
+
return this.autoStuckKind() !== null;
|
|
10239
10402
|
}
|
|
10240
10403
|
/**
|
|
10241
10404
|
* Stop driving the agent and route the core loop into dormant idle —
|
|
@@ -10271,6 +10434,16 @@ var SessionRunner = class _SessionRunner {
|
|
|
10271
10434
|
} catch {
|
|
10272
10435
|
}
|
|
10273
10436
|
}
|
|
10437
|
+
/** Prompt + chat-message label for a stuck-nudge of the given kind. */
|
|
10438
|
+
stuckNudgeContent(kind) {
|
|
10439
|
+
if (kind === "planning") {
|
|
10440
|
+
return {
|
|
10441
|
+
prompt: _SessionRunner.STUCK_PROMPT_PLANNING,
|
|
10442
|
+
situation: "still Planning with no identified plan"
|
|
10443
|
+
};
|
|
10444
|
+
}
|
|
10445
|
+
return { prompt: _SessionRunner.STUCK_PROMPT, situation: "still no PR" };
|
|
10446
|
+
}
|
|
10274
10447
|
async maybeHandleStuckAuto() {
|
|
10275
10448
|
await this.refreshTaskContext();
|
|
10276
10449
|
if (!this.isAutoStuck()) return;
|
|
@@ -10278,17 +10451,18 @@ var SessionRunner = class _SessionRunner {
|
|
|
10278
10451
|
this.prNudgeCount++;
|
|
10279
10452
|
if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) {
|
|
10280
10453
|
this.enterDormantForHumanTakeover(
|
|
10281
|
-
`Auto-mode: I couldn't
|
|
10454
|
+
`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
10455
|
);
|
|
10283
10456
|
return;
|
|
10284
10457
|
}
|
|
10458
|
+
const { prompt, situation } = this.stuckNudgeContent(this.autoStuckKind());
|
|
10285
10459
|
const attempt = this.prNudgeCount;
|
|
10286
|
-
const chatMsg = attempt === 1 ?
|
|
10460
|
+
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
10461
|
this.connection.postChatMessage(chatMsg);
|
|
10288
10462
|
await this.setState("running");
|
|
10289
|
-
await this.callbacks.onEvent({ type: "pr_nudge", prompt
|
|
10463
|
+
await this.callbacks.onEvent({ type: "pr_nudge", prompt });
|
|
10290
10464
|
if (this.interrupted || this.stopped) return;
|
|
10291
|
-
await this.executeQuery(
|
|
10465
|
+
await this.executeQuery(prompt);
|
|
10292
10466
|
if (this.interrupted || this.stopped) return;
|
|
10293
10467
|
await this.refreshTaskContext();
|
|
10294
10468
|
if (this.agentSpokeThisTurn && !this.taskContext?.githubPRUrl) {
|
|
@@ -10346,32 +10520,26 @@ var SessionRunner = class _SessionRunner {
|
|
|
10346
10520
|
mode: this.config.runnerMode,
|
|
10347
10521
|
isAuto: this.config.isAuto
|
|
10348
10522
|
};
|
|
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);
|
|
10523
|
+
const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
|
|
10524
|
+
onStatusChange: (status) => {
|
|
10525
|
+
if (status === "running" && this._state === "waiting_for_input") {
|
|
10526
|
+
this._state = "running";
|
|
10527
|
+
this.lifecycle.cancelIdleTimer();
|
|
10371
10528
|
}
|
|
10529
|
+
return this.callbacks.onStatusChange(status);
|
|
10372
10530
|
},
|
|
10373
|
-
|
|
10374
|
-
|
|
10531
|
+
onEvent: (event) => {
|
|
10532
|
+
const evt = event;
|
|
10533
|
+
if (evt.type === "tool_use" && isPostToChatTool(evt.tool)) {
|
|
10534
|
+
this.agentSpokeThisTurn = true;
|
|
10535
|
+
}
|
|
10536
|
+
if (event.type === "completed") {
|
|
10537
|
+
this.completedThisTurn = true;
|
|
10538
|
+
void this.connection.sendHeartbeat();
|
|
10539
|
+
}
|
|
10540
|
+
return this.callbacks.onEvent(event);
|
|
10541
|
+
}
|
|
10542
|
+
});
|
|
10375
10543
|
bridge.isParentTask = this.fullContext?.isParentTask ?? false;
|
|
10376
10544
|
bridge.onSoftStop = () => {
|
|
10377
10545
|
process.stderr.write("[conveyor-agent] Soft stop requested (discovery ExitPlanMode)\n");
|
|
@@ -10424,15 +10592,16 @@ var SessionRunner = class _SessionRunner {
|
|
|
10424
10592
|
}
|
|
10425
10593
|
});
|
|
10426
10594
|
this.connection.onPullBranch(({ branch }) => {
|
|
10427
|
-
handlePullBranch(this.config.workspaceDir, branch);
|
|
10595
|
+
void handlePullBranch(this.config.workspaceDir, branch);
|
|
10428
10596
|
});
|
|
10429
10597
|
this.connection.onFinalizeSnapshot(() => void this.finalizeSnapshotNow());
|
|
10430
10598
|
}
|
|
10431
10599
|
/** Eager finalize snapshot triggered by the reconciler's sleep signal
|
|
10432
|
-
* (session:finalizeSnapshot). Runs
|
|
10600
|
+
* (session:finalizeSnapshot). Runs a full workspace capture and uploads it
|
|
10433
10601
|
* NOW so the sleep confirms on this snapshot instead of waiting for the
|
|
10434
|
-
* ~2min periodic flush
|
|
10435
|
-
*
|
|
10602
|
+
* ~2min periodic flush. Sidecar DB state is intentionally NOT captured — it
|
|
10603
|
+
* is no longer durable across sleep/wake (#2623); only the agent's
|
|
10604
|
+
* /workspace rides the snapshot. Best-effort: on failure the
|
|
10436
10605
|
* periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
|
|
10437
10606
|
async finalizeSnapshotNow() {
|
|
10438
10607
|
const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
|
|
@@ -10453,9 +10622,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10453
10622
|
}
|
|
10454
10623
|
}
|
|
10455
10624
|
});
|
|
10456
|
-
process.stderr.write(
|
|
10457
|
-
"[conveyor-agent] Finalize snapshot (pg_dump) uploaded on sleep signal\n"
|
|
10458
|
-
);
|
|
10625
|
+
process.stderr.write("[conveyor-agent] Finalize snapshot uploaded on sleep signal\n");
|
|
10459
10626
|
} catch {
|
|
10460
10627
|
}
|
|
10461
10628
|
}
|
|
@@ -10465,7 +10632,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10465
10632
|
const res = await this.connection.call("refreshGithubToken", {
|
|
10466
10633
|
sessionId: this.connection.sessionId
|
|
10467
10634
|
});
|
|
10468
|
-
updateRemoteToken(this.config.workspaceDir, res.token);
|
|
10635
|
+
await updateRemoteToken(this.config.workspaceDir, res.token);
|
|
10469
10636
|
process.env.GITHUB_TOKEN = res.token;
|
|
10470
10637
|
process.env.GH_TOKEN = res.token;
|
|
10471
10638
|
process.stderr.write("[conveyor-agent] Proactively refreshed GitHub token\n");
|
|
@@ -10484,7 +10651,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10484
10651
|
});
|
|
10485
10652
|
if (ctx?.githubBranch && this.fullContext) {
|
|
10486
10653
|
this.fullContext.githubBranch = ctx.githubBranch;
|
|
10487
|
-
ensureOnTaskBranch(this.config.workspaceDir, ctx.githubBranch);
|
|
10654
|
+
await ensureOnTaskBranch(this.config.workspaceDir, ctx.githubBranch);
|
|
10488
10655
|
}
|
|
10489
10656
|
} catch {
|
|
10490
10657
|
process.stderr.write(
|
|
@@ -10494,6 +10661,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
10494
10661
|
}
|
|
10495
10662
|
async setState(status) {
|
|
10496
10663
|
this._state = status;
|
|
10664
|
+
const loopStatus = status === "idle" ? "idle" : "active";
|
|
10665
|
+
this.loopLag.setStatus(loopStatus);
|
|
10497
10666
|
await this.connection.emitStatus(status);
|
|
10498
10667
|
await this.callbacks.onStatusChange(status);
|
|
10499
10668
|
}
|
|
@@ -10502,6 +10671,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10502
10671
|
`);
|
|
10503
10672
|
this.connection.sendEvent({ type: "shutdown", reason: finalState });
|
|
10504
10673
|
this.portDiscovery?.stop();
|
|
10674
|
+
this.loopLag.stop();
|
|
10505
10675
|
this.lifecycle.destroy();
|
|
10506
10676
|
try {
|
|
10507
10677
|
await this.queryBridge?.dispose();
|
|
@@ -10551,12 +10721,12 @@ var SessionRunner = class _SessionRunner {
|
|
|
10551
10721
|
|
|
10552
10722
|
// src/setup/config.ts
|
|
10553
10723
|
import { readFile as readFile5 } from "fs/promises";
|
|
10554
|
-
import { join as
|
|
10724
|
+
import { join as join7 } from "path";
|
|
10555
10725
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
10556
10726
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
10557
10727
|
async function loadForwardPorts(workspaceDir) {
|
|
10558
10728
|
try {
|
|
10559
|
-
const raw = await readFile5(
|
|
10729
|
+
const raw = await readFile5(join7(workspaceDir, DEVCONTAINER_PATH), "utf-8");
|
|
10560
10730
|
const parsed = JSON.parse(raw);
|
|
10561
10731
|
const ports = (parsed.forwardPorts ?? []).filter(
|
|
10562
10732
|
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
|
@@ -10588,19 +10758,17 @@ function buildSessionPreviewPorts(result) {
|
|
|
10588
10758
|
function loadConveyorConfig() {
|
|
10589
10759
|
const envSetup = process.env.CONVEYOR_SETUP_COMMAND;
|
|
10590
10760
|
const envStart = process.env.CONVEYOR_START_COMMAND;
|
|
10591
|
-
const envPort = process.env.CONVEYOR_PREVIEW_PORT;
|
|
10592
10761
|
if (envSetup || envStart) {
|
|
10593
10762
|
return {
|
|
10594
10763
|
setupCommand: envSetup,
|
|
10595
|
-
startCommand: envStart
|
|
10596
|
-
previewPort: envPort ? Number(envPort) : void 0
|
|
10764
|
+
startCommand: envStart
|
|
10597
10765
|
};
|
|
10598
10766
|
}
|
|
10599
10767
|
return null;
|
|
10600
10768
|
}
|
|
10601
10769
|
|
|
10602
10770
|
// src/setup/commands.ts
|
|
10603
|
-
import { spawn, execSync
|
|
10771
|
+
import { spawn, execSync } from "child_process";
|
|
10604
10772
|
function runSetupCommand(cmd, cwd, onOutput) {
|
|
10605
10773
|
return new Promise((resolve, reject) => {
|
|
10606
10774
|
const child = spawn("sh", ["-c", cmd], {
|
|
@@ -10629,7 +10797,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
|
|
|
10629
10797
|
var AUTH_TOKEN_TIMEOUT_MS = 3e4;
|
|
10630
10798
|
function runAuthTokenCommand(cmd, userEmail, cwd) {
|
|
10631
10799
|
try {
|
|
10632
|
-
const output =
|
|
10800
|
+
const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {
|
|
10633
10801
|
cwd,
|
|
10634
10802
|
timeout: AUTH_TOKEN_TIMEOUT_MS,
|
|
10635
10803
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -10659,10 +10827,10 @@ function runStartCommand(cmd, cwd, onOutput) {
|
|
|
10659
10827
|
}
|
|
10660
10828
|
|
|
10661
10829
|
// src/setup/codespace.ts
|
|
10662
|
-
import { execSync as
|
|
10830
|
+
import { execSync as execSync2 } from "child_process";
|
|
10663
10831
|
function unshallowRepo(workspaceDir) {
|
|
10664
10832
|
try {
|
|
10665
|
-
|
|
10833
|
+
execSync2("git fetch --unshallow", {
|
|
10666
10834
|
cwd: workspaceDir,
|
|
10667
10835
|
timeout: 6e4,
|
|
10668
10836
|
stdio: "ignore"
|
|
@@ -10672,12 +10840,17 @@ function unshallowRepo(workspaceDir) {
|
|
|
10672
10840
|
}
|
|
10673
10841
|
|
|
10674
10842
|
export {
|
|
10675
|
-
DEFAULT_SONNET_MODEL,
|
|
10676
10843
|
fetchBootstrap,
|
|
10677
10844
|
applyBootstrapToEnv,
|
|
10678
10845
|
AgentConnection,
|
|
10846
|
+
DEFAULT_SONNET_MODEL,
|
|
10847
|
+
TUI_KINDS,
|
|
10679
10848
|
DEFAULT_LIFECYCLE_CONFIG,
|
|
10680
10849
|
Lifecycle,
|
|
10850
|
+
cleanTerminalOutput,
|
|
10851
|
+
inheritedEnv,
|
|
10852
|
+
buildPromptBytes,
|
|
10853
|
+
ClaudeTuiAdapter,
|
|
10681
10854
|
PtyHarness,
|
|
10682
10855
|
createServiceLogger,
|
|
10683
10856
|
hasUncommittedChanges,
|
|
@@ -10688,7 +10861,6 @@ export {
|
|
|
10688
10861
|
flushPendingChanges,
|
|
10689
10862
|
pushToOrigin,
|
|
10690
10863
|
resolveSessionStart,
|
|
10691
|
-
PlanSync,
|
|
10692
10864
|
sampleKeyUsage,
|
|
10693
10865
|
awaitGitReady,
|
|
10694
10866
|
uploadSnapshotToGcs,
|
|
@@ -10706,4 +10878,4 @@ export {
|
|
|
10706
10878
|
runStartCommand,
|
|
10707
10879
|
unshallowRepo
|
|
10708
10880
|
};
|
|
10709
|
-
//# sourceMappingURL=chunk-
|
|
10881
|
+
//# sourceMappingURL=chunk-CFELRV35.js.map
|