@rallycry/conveyor-agent 10.2.1 → 10.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-7TQO4ZF4.js +60 -0
- package/dist/chunk-7TQO4ZF4.js.map +1 -0
- package/dist/{chunk-YGLTLUGA.js → chunk-DEMRCBJN.js} +994 -862
- package/dist/chunk-DEMRCBJN.js.map +1 -0
- package/dist/cli.js +319 -49
- 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 +74 -42
- package/dist/index.js +19 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-YGLTLUGA.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
|
|
@@ -188,6 +195,8 @@ var AgentConnection = class _AgentConnection {
|
|
|
188
195
|
finalizeSnapshotCallback = null;
|
|
189
196
|
runStartCommandCallback = null;
|
|
190
197
|
earlyPullBranches = [];
|
|
198
|
+
spawnReviewCallback = null;
|
|
199
|
+
earlySpawnReviews = [];
|
|
191
200
|
// PTY relay (S5 terminal). Single-slot callbacks, set per PtySession run.
|
|
192
201
|
ptyInputCallback = null;
|
|
193
202
|
ptyResizeCallback = null;
|
|
@@ -391,6 +400,10 @@ var AgentConnection = class _AgentConnection {
|
|
|
391
400
|
if (this.pullBranchCallback) this.pullBranchCallback(data);
|
|
392
401
|
else this.earlyPullBranches.push(data);
|
|
393
402
|
});
|
|
403
|
+
this.socket.on("session:spawnReview", (data) => {
|
|
404
|
+
if (this.spawnReviewCallback) this.spawnReviewCallback(data);
|
|
405
|
+
else this.earlySpawnReviews.push(data);
|
|
406
|
+
});
|
|
394
407
|
this.socket.on("session:finalizeSnapshot", () => {
|
|
395
408
|
this.finalizeSnapshotCallback?.();
|
|
396
409
|
});
|
|
@@ -445,8 +458,12 @@ var AgentConnection = class _AgentConnection {
|
|
|
445
458
|
void this.refreshTaskTokenFromBootstrap().catch(() => {
|
|
446
459
|
});
|
|
447
460
|
});
|
|
448
|
-
this.socket.io.on("reconnect", () => {
|
|
449
|
-
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();
|
|
450
467
|
void this.reconnectToSession();
|
|
451
468
|
});
|
|
452
469
|
this.socket.io.on("reconnect_attempt", () => {
|
|
@@ -456,6 +473,7 @@ var AgentConnection = class _AgentConnection {
|
|
|
456
473
|
disconnect() {
|
|
457
474
|
this.stopProactiveTokenRefresh();
|
|
458
475
|
this.clearSocketRecycle();
|
|
476
|
+
this.stopHeartbeatWorker();
|
|
459
477
|
void this.flushEvents();
|
|
460
478
|
if (this.socket) {
|
|
461
479
|
this.socket.io.reconnection(false);
|
|
@@ -621,6 +639,26 @@ var AgentConnection = class _AgentConnection {
|
|
|
621
639
|
for (const data of this.earlyPullBranches) callback(data);
|
|
622
640
|
this.earlyPullBranches = [];
|
|
623
641
|
}
|
|
642
|
+
onSpawnReview(callback) {
|
|
643
|
+
this.spawnReviewCallback = callback;
|
|
644
|
+
for (const data of this.earlySpawnReviews) callback(data);
|
|
645
|
+
this.earlySpawnReviews = [];
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Report that a same-pod review child failed to spawn (fire-and-forget).
|
|
649
|
+
* The server Ends the orphaned review session and falls back to a dedicated
|
|
650
|
+
* review pod. sessionId is OUR (builder) session — the SEC-9 guard runs on
|
|
651
|
+
* it; the review session is identified separately.
|
|
652
|
+
*/
|
|
653
|
+
reportReviewSpawnFailure(reviewSessionId, error) {
|
|
654
|
+
if (!this.socket) return;
|
|
655
|
+
void this.call("reportReviewSpawnFailure", {
|
|
656
|
+
sessionId: this.config.sessionId,
|
|
657
|
+
reviewSessionId,
|
|
658
|
+
...error ? { error: error.slice(0, 2e3) } : {}
|
|
659
|
+
}).catch(() => {
|
|
660
|
+
});
|
|
661
|
+
}
|
|
624
662
|
onFinalizeSnapshot(callback) {
|
|
625
663
|
this.finalizeSnapshotCallback = callback;
|
|
626
664
|
}
|
|
@@ -642,6 +680,19 @@ var AgentConnection = class _AgentConnection {
|
|
|
642
680
|
}).catch(() => {
|
|
643
681
|
});
|
|
644
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
|
+
}
|
|
645
696
|
/**
|
|
646
697
|
* Report that the interactive CLI process for this session has died and no
|
|
647
698
|
* respawn is imminent (fire-and-forget). The server clears the scrollback
|
|
@@ -751,7 +802,7 @@ var AgentConnection = class _AgentConnection {
|
|
|
751
802
|
if (this.recentMessages.length > 3) this.recentMessages.shift();
|
|
752
803
|
return { duplicate: false };
|
|
753
804
|
}
|
|
754
|
-
sendHeartbeat() {
|
|
805
|
+
sendHeartbeat(loopLagMs) {
|
|
755
806
|
if (!this.socket) return;
|
|
756
807
|
const statusMap = {
|
|
757
808
|
running: "active",
|
|
@@ -764,10 +815,67 @@ var AgentConnection = class _AgentConnection {
|
|
|
764
815
|
void this.call("heartbeat", {
|
|
765
816
|
sessionId: this.config.sessionId,
|
|
766
817
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
767
|
-
status: heartbeatStatus
|
|
818
|
+
status: heartbeatStatus,
|
|
819
|
+
...loopLagMs !== void 0 && loopLagMs > 0 ? { loopLagMs: Math.round(loopLagMs) } : {}
|
|
768
820
|
}).catch(() => {
|
|
769
821
|
});
|
|
770
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
|
+
}
|
|
771
879
|
emitModeChanged(agentMode) {
|
|
772
880
|
this.sendEvent({ type: "mode_changed", agentMode });
|
|
773
881
|
}
|
|
@@ -901,6 +1009,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
|
|
|
901
1009
|
auth.taskToken = result.config.taskToken;
|
|
902
1010
|
}
|
|
903
1011
|
}
|
|
1012
|
+
this.heartbeatWorker?.postMessage({ taskToken: result.config.taskToken });
|
|
904
1013
|
}
|
|
905
1014
|
const refreshedClaude = Boolean(result.config.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
|
|
906
1015
|
return { refreshedClaude, refreshedTaskToken };
|
|
@@ -933,6 +1042,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
|
|
|
933
1042
|
auth.taskToken = bundle.sessionJwt;
|
|
934
1043
|
}
|
|
935
1044
|
}
|
|
1045
|
+
this.heartbeatWorker?.postMessage({ taskToken: bundle.sessionJwt });
|
|
936
1046
|
}
|
|
937
1047
|
const refreshedClaude = Boolean(bundle.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
|
|
938
1048
|
return { refreshedClaude, refreshedTaskToken };
|
|
@@ -1014,14 +1124,25 @@ function createServiceLogger(service) {
|
|
|
1014
1124
|
}
|
|
1015
1125
|
|
|
1016
1126
|
// src/runner/git-utils.ts
|
|
1017
|
-
import {
|
|
1127
|
+
import { execFile } from "child_process";
|
|
1018
1128
|
import { realpathSync } from "fs";
|
|
1019
1129
|
import { promisify } from "util";
|
|
1020
|
-
var
|
|
1021
|
-
|
|
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) {
|
|
1022
1143
|
if (!baseBranch) return true;
|
|
1023
1144
|
try {
|
|
1024
|
-
|
|
1145
|
+
await git(cwd, ["fetch", "origin", baseBranch]);
|
|
1025
1146
|
} catch {
|
|
1026
1147
|
process.stderr.write(
|
|
1027
1148
|
`[conveyor-agent] Warning: git fetch origin ${baseBranch} failed, continuing with current base
|
|
@@ -1030,14 +1151,14 @@ function syncWithBaseBranch(cwd, baseBranch) {
|
|
|
1030
1151
|
return false;
|
|
1031
1152
|
}
|
|
1032
1153
|
try {
|
|
1033
|
-
|
|
1154
|
+
await git(cwd, ["merge", `origin/${baseBranch}`, "--no-edit"], 3e4);
|
|
1034
1155
|
} catch {
|
|
1035
1156
|
process.stderr.write(
|
|
1036
1157
|
`[conveyor-agent] Warning: merge origin/${baseBranch} failed, aborting merge and continuing
|
|
1037
1158
|
`
|
|
1038
1159
|
);
|
|
1039
1160
|
try {
|
|
1040
|
-
|
|
1161
|
+
await git(cwd, ["merge", "--abort"], 3e4);
|
|
1041
1162
|
} catch {
|
|
1042
1163
|
}
|
|
1043
1164
|
return false;
|
|
@@ -1046,23 +1167,19 @@ function syncWithBaseBranch(cwd, baseBranch) {
|
|
|
1046
1167
|
`);
|
|
1047
1168
|
return true;
|
|
1048
1169
|
}
|
|
1049
|
-
function ensureOnTaskBranch(cwd, taskBranch) {
|
|
1170
|
+
async function ensureOnTaskBranch(cwd, taskBranch) {
|
|
1050
1171
|
if (!taskBranch) return true;
|
|
1051
|
-
const current = getCurrentBranch(cwd);
|
|
1172
|
+
const current = await getCurrentBranch(cwd);
|
|
1052
1173
|
if (current === taskBranch) return true;
|
|
1053
1174
|
try {
|
|
1054
|
-
|
|
1175
|
+
await git(cwd, ["fetch", "origin", taskBranch]);
|
|
1055
1176
|
} catch {
|
|
1056
1177
|
process.stderr.write(`[conveyor-agent] Warning: git fetch origin ${taskBranch} failed
|
|
1057
1178
|
`);
|
|
1058
1179
|
return false;
|
|
1059
1180
|
}
|
|
1060
1181
|
try {
|
|
1061
|
-
|
|
1062
|
-
cwd,
|
|
1063
|
-
stdio: "ignore",
|
|
1064
|
-
timeout: 3e4
|
|
1065
|
-
});
|
|
1182
|
+
await git(cwd, ["checkout", "-B", taskBranch, `origin/${taskBranch}`], 3e4);
|
|
1066
1183
|
} catch {
|
|
1067
1184
|
process.stderr.write(`[conveyor-agent] Warning: git checkout ${taskBranch} failed
|
|
1068
1185
|
`);
|
|
@@ -1072,74 +1189,62 @@ function ensureOnTaskBranch(cwd, taskBranch) {
|
|
|
1072
1189
|
`);
|
|
1073
1190
|
return true;
|
|
1074
1191
|
}
|
|
1075
|
-
function hasUncommittedChanges(cwd) {
|
|
1076
|
-
const status =
|
|
1077
|
-
cwd,
|
|
1078
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1079
|
-
}).toString().trim();
|
|
1192
|
+
async function hasUncommittedChanges(cwd) {
|
|
1193
|
+
const status = await git(cwd, ["status", "--porcelain"], GIT_SLOW_TIMEOUT_MS);
|
|
1080
1194
|
return status.length > 0;
|
|
1081
1195
|
}
|
|
1082
|
-
function getCurrentBranch(cwd) {
|
|
1196
|
+
async function getCurrentBranch(cwd) {
|
|
1083
1197
|
try {
|
|
1084
|
-
const branch =
|
|
1085
|
-
cwd,
|
|
1086
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1087
|
-
}).toString().trim();
|
|
1198
|
+
const branch = await git(cwd, ["branch", "--show-current"]);
|
|
1088
1199
|
return branch || null;
|
|
1089
1200
|
} catch {
|
|
1090
1201
|
return null;
|
|
1091
1202
|
}
|
|
1092
1203
|
}
|
|
1093
|
-
function hasUnpushedCommits(cwd) {
|
|
1204
|
+
async function hasUnpushedCommits(cwd) {
|
|
1094
1205
|
try {
|
|
1095
|
-
const currentBranch = getCurrentBranch(cwd);
|
|
1206
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
1096
1207
|
if (!currentBranch) return false;
|
|
1097
1208
|
try {
|
|
1098
|
-
|
|
1099
|
-
cwd,
|
|
1100
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1101
|
-
});
|
|
1209
|
+
await git(cwd, ["rev-parse", `origin/${currentBranch}`]);
|
|
1102
1210
|
} catch {
|
|
1103
1211
|
try {
|
|
1104
|
-
|
|
1212
|
+
await git(cwd, ["rev-parse", "HEAD"]);
|
|
1105
1213
|
return true;
|
|
1106
1214
|
} catch {
|
|
1107
1215
|
return false;
|
|
1108
1216
|
}
|
|
1109
1217
|
}
|
|
1110
|
-
const ahead =
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1218
|
+
const ahead = await git(cwd, [
|
|
1219
|
+
"rev-list",
|
|
1220
|
+
"--count",
|
|
1221
|
+
"HEAD",
|
|
1222
|
+
"--not",
|
|
1223
|
+
`origin/${currentBranch}`
|
|
1224
|
+
]);
|
|
1114
1225
|
return parseInt(ahead, 10) > 0;
|
|
1115
1226
|
} catch {
|
|
1116
1227
|
return false;
|
|
1117
1228
|
}
|
|
1118
1229
|
}
|
|
1119
|
-
function stageAndCommit(cwd, message) {
|
|
1230
|
+
async function stageAndCommit(cwd, message) {
|
|
1120
1231
|
try {
|
|
1121
|
-
|
|
1122
|
-
if (!hasUncommittedChanges(cwd)) return null;
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1126
|
-
});
|
|
1127
|
-
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"]);
|
|
1128
1236
|
} catch {
|
|
1129
1237
|
return null;
|
|
1130
1238
|
}
|
|
1131
1239
|
}
|
|
1132
1240
|
async function tryPush(cwd, branch, skipVerify = false) {
|
|
1133
|
-
const noVerify = skipVerify ? "--no-verify
|
|
1241
|
+
const noVerify = skipVerify ? ["--no-verify"] : [];
|
|
1134
1242
|
try {
|
|
1135
|
-
await
|
|
1243
|
+
await git(cwd, ["push", ...noVerify, "origin", branch], 3e4);
|
|
1136
1244
|
return true;
|
|
1137
1245
|
} catch {
|
|
1138
1246
|
try {
|
|
1139
|
-
await
|
|
1140
|
-
cwd,
|
|
1141
|
-
timeout: 3e4
|
|
1142
|
-
});
|
|
1247
|
+
await git(cwd, ["push", ...noVerify, "--force-with-lease", "origin", branch], 3e4);
|
|
1143
1248
|
return true;
|
|
1144
1249
|
} catch {
|
|
1145
1250
|
return false;
|
|
@@ -1148,7 +1253,7 @@ async function tryPush(cwd, branch, skipVerify = false) {
|
|
|
1148
1253
|
}
|
|
1149
1254
|
async function isAuthError(cwd) {
|
|
1150
1255
|
try {
|
|
1151
|
-
await
|
|
1256
|
+
await git(cwd, ["push", "--dry-run"], 3e4);
|
|
1152
1257
|
return false;
|
|
1153
1258
|
} catch (err) {
|
|
1154
1259
|
if (err.killed) return true;
|
|
@@ -1158,16 +1263,18 @@ async function isAuthError(cwd) {
|
|
|
1158
1263
|
return /authentication|authorization|403|401|token/i.test(msg);
|
|
1159
1264
|
}
|
|
1160
1265
|
}
|
|
1161
|
-
function updateRemoteToken(cwd, token) {
|
|
1266
|
+
async function updateRemoteToken(cwd, token) {
|
|
1162
1267
|
try {
|
|
1163
|
-
const url =
|
|
1268
|
+
const url = await git(cwd, ["remote", "get-url", "origin"]);
|
|
1164
1269
|
const match = url.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
1165
1270
|
if (match) {
|
|
1166
1271
|
const repo = match[1].replace(/\.git$/, "");
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1272
|
+
await git(cwd, [
|
|
1273
|
+
"remote",
|
|
1274
|
+
"set-url",
|
|
1275
|
+
"origin",
|
|
1276
|
+
`https://x-access-token:${token}@github.com/${repo}.git`
|
|
1277
|
+
]);
|
|
1171
1278
|
}
|
|
1172
1279
|
} catch {
|
|
1173
1280
|
}
|
|
@@ -1176,30 +1283,24 @@ function wipRefForBranch(branch) {
|
|
|
1176
1283
|
return `conveyor-wip/${branch}`;
|
|
1177
1284
|
}
|
|
1178
1285
|
var wipRefPushed = /* @__PURE__ */ new Set();
|
|
1179
|
-
function createWipSnapshot(cwd, message) {
|
|
1286
|
+
async function createWipSnapshot(cwd, message) {
|
|
1180
1287
|
try {
|
|
1181
|
-
|
|
1182
|
-
const sha =
|
|
1183
|
-
cwd,
|
|
1184
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1185
|
-
}).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);
|
|
1186
1290
|
return sha || null;
|
|
1187
1291
|
} catch {
|
|
1188
1292
|
return null;
|
|
1189
1293
|
} finally {
|
|
1190
1294
|
try {
|
|
1191
|
-
|
|
1295
|
+
await git(cwd, ["reset", "-q"], GIT_SLOW_TIMEOUT_MS);
|
|
1192
1296
|
} catch {
|
|
1193
1297
|
}
|
|
1194
1298
|
}
|
|
1195
1299
|
}
|
|
1196
|
-
function tryPushRefspec(cwd, refspec, force = false) {
|
|
1300
|
+
async function tryPushRefspec(cwd, refspec, force = false) {
|
|
1197
1301
|
try {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1201
|
-
timeout: 3e4
|
|
1202
|
-
});
|
|
1302
|
+
const forceArgs = force ? ["--force"] : [];
|
|
1303
|
+
await git(cwd, ["push", "--no-verify", ...forceArgs, "origin", refspec], 3e4);
|
|
1203
1304
|
return true;
|
|
1204
1305
|
} catch {
|
|
1205
1306
|
return false;
|
|
@@ -1210,49 +1311,39 @@ async function refreshRemoteToken(cwd, refreshToken) {
|
|
|
1210
1311
|
try {
|
|
1211
1312
|
const token = await refreshToken();
|
|
1212
1313
|
if (token) {
|
|
1213
|
-
updateRemoteToken(cwd, token);
|
|
1314
|
+
await updateRemoteToken(cwd, token);
|
|
1214
1315
|
process.env.GITHUB_TOKEN = token;
|
|
1215
1316
|
process.env.GH_TOKEN = token;
|
|
1216
1317
|
}
|
|
1217
1318
|
} catch {
|
|
1218
1319
|
}
|
|
1219
1320
|
}
|
|
1220
|
-
function restoreWipSnapshot(cwd, branch) {
|
|
1321
|
+
async function restoreWipSnapshot(cwd, branch) {
|
|
1221
1322
|
if (!branch) return "none";
|
|
1222
1323
|
const ref = wipRefForBranch(branch);
|
|
1223
1324
|
try {
|
|
1224
|
-
|
|
1225
|
-
cwd,
|
|
1226
|
-
stdio: "ignore",
|
|
1227
|
-
timeout: 6e4
|
|
1228
|
-
});
|
|
1325
|
+
await git(cwd, ["fetch", "origin", `+refs/heads/${ref}:refs/remotes/origin/${ref}`]);
|
|
1229
1326
|
} catch {
|
|
1230
1327
|
return "none";
|
|
1231
1328
|
}
|
|
1232
1329
|
wipRefPushed.add(cwd);
|
|
1233
1330
|
try {
|
|
1234
|
-
const sha =
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
}).toString().trim();
|
|
1238
|
-
const parent = execSync(`git rev-parse "${sha}^"`, {
|
|
1239
|
-
cwd,
|
|
1240
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1241
|
-
}).toString().trim();
|
|
1242
|
-
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"]);
|
|
1243
1334
|
if (parent !== head) {
|
|
1244
1335
|
try {
|
|
1245
|
-
|
|
1336
|
+
await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
|
|
1246
1337
|
return "applied";
|
|
1247
1338
|
} catch {
|
|
1248
1339
|
try {
|
|
1249
|
-
|
|
1340
|
+
await git(cwd, ["reset", "--merge"], GIT_SLOW_TIMEOUT_MS);
|
|
1250
1341
|
} catch {
|
|
1251
1342
|
}
|
|
1252
1343
|
return "stale";
|
|
1253
1344
|
}
|
|
1254
1345
|
}
|
|
1255
|
-
|
|
1346
|
+
await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
|
|
1256
1347
|
return "applied";
|
|
1257
1348
|
} catch {
|
|
1258
1349
|
return "failed";
|
|
@@ -1263,10 +1354,10 @@ async function flushPendingChanges(cwd, opts) {
|
|
|
1263
1354
|
let pushed = false;
|
|
1264
1355
|
let hadWork = false;
|
|
1265
1356
|
try {
|
|
1266
|
-
const branch = getCurrentBranch(cwd);
|
|
1357
|
+
const branch = await getCurrentBranch(cwd);
|
|
1267
1358
|
if (!branch) return { committed, pushed, hadWork };
|
|
1268
|
-
const dirty = hasUncommittedChanges(cwd);
|
|
1269
|
-
const unpushed = hasUnpushedCommits(cwd);
|
|
1359
|
+
const dirty = await hasUncommittedChanges(cwd);
|
|
1360
|
+
const unpushed = await hasUnpushedCommits(cwd);
|
|
1270
1361
|
if (!dirty && !unpushed) {
|
|
1271
1362
|
await dropStaleWipRef(cwd, branch, opts?.refreshToken);
|
|
1272
1363
|
return { committed, pushed, hadWork };
|
|
@@ -1278,9 +1369,9 @@ async function flushPendingChanges(cwd, opts) {
|
|
|
1278
1369
|
}
|
|
1279
1370
|
if (dirty) {
|
|
1280
1371
|
const message = opts?.wipMessage ?? "WIP: conveyor-agent snapshot";
|
|
1281
|
-
const sha = createWipSnapshot(cwd, message);
|
|
1372
|
+
const sha = await createWipSnapshot(cwd, message);
|
|
1282
1373
|
if (sha) {
|
|
1283
|
-
committed = tryPushRefspec(cwd, `${sha}:refs/heads/${wipRefForBranch(branch)}`, true);
|
|
1374
|
+
committed = await tryPushRefspec(cwd, `${sha}:refs/heads/${wipRefForBranch(branch)}`, true);
|
|
1284
1375
|
if (committed) wipRefPushed.add(cwd);
|
|
1285
1376
|
}
|
|
1286
1377
|
}
|
|
@@ -1291,19 +1382,19 @@ async function flushPendingChanges(cwd, opts) {
|
|
|
1291
1382
|
async function dropStaleWipRef(cwd, branch, refreshToken) {
|
|
1292
1383
|
if (!wipRefPushed.has(cwd)) return;
|
|
1293
1384
|
await refreshRemoteToken(cwd, refreshToken);
|
|
1294
|
-
if (tryPushRefspec(cwd, `:refs/heads/${wipRefForBranch(branch)}`)) {
|
|
1385
|
+
if (await tryPushRefspec(cwd, `:refs/heads/${wipRefForBranch(branch)}`)) {
|
|
1295
1386
|
wipRefPushed.delete(cwd);
|
|
1296
1387
|
}
|
|
1297
1388
|
}
|
|
1298
1389
|
async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
|
|
1299
1390
|
try {
|
|
1300
|
-
const currentBranch = getCurrentBranch(cwd);
|
|
1391
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
1301
1392
|
if (!currentBranch) return false;
|
|
1302
1393
|
if (refreshToken) {
|
|
1303
1394
|
try {
|
|
1304
1395
|
const token = await refreshToken();
|
|
1305
1396
|
if (token) {
|
|
1306
|
-
updateRemoteToken(cwd, token);
|
|
1397
|
+
await updateRemoteToken(cwd, token);
|
|
1307
1398
|
process.env.GITHUB_TOKEN = token;
|
|
1308
1399
|
process.env.GH_TOKEN = token;
|
|
1309
1400
|
}
|
|
@@ -1314,7 +1405,7 @@ async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
|
|
|
1314
1405
|
if (refreshToken && await isAuthError(cwd)) {
|
|
1315
1406
|
const token = await refreshToken();
|
|
1316
1407
|
if (token) {
|
|
1317
|
-
updateRemoteToken(cwd, token);
|
|
1408
|
+
await updateRemoteToken(cwd, token);
|
|
1318
1409
|
process.env.GITHUB_TOKEN = token;
|
|
1319
1410
|
process.env.GH_TOKEN = token;
|
|
1320
1411
|
return await tryPush(cwd, currentBranch, skipVerify);
|
|
@@ -1328,12 +1419,9 @@ async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
|
|
|
1328
1419
|
function branchBackupRef(branch) {
|
|
1329
1420
|
return `conveyor-wip/branches/${branch}`;
|
|
1330
1421
|
}
|
|
1331
|
-
function listWorktrees(cwd) {
|
|
1422
|
+
async function listWorktrees(cwd) {
|
|
1332
1423
|
try {
|
|
1333
|
-
const out =
|
|
1334
|
-
cwd,
|
|
1335
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1336
|
-
}).toString();
|
|
1424
|
+
const out = await git(cwd, ["worktree", "list", "--porcelain"]);
|
|
1337
1425
|
const result = [];
|
|
1338
1426
|
for (const entry of out.split("\n\n")) {
|
|
1339
1427
|
const lines = entry.trim().split("\n");
|
|
@@ -1350,22 +1438,17 @@ function listWorktrees(cwd) {
|
|
|
1350
1438
|
return [];
|
|
1351
1439
|
}
|
|
1352
1440
|
}
|
|
1353
|
-
function listLocalBranches(cwd) {
|
|
1441
|
+
async function listLocalBranches(cwd) {
|
|
1354
1442
|
try {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1358
|
-
}).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);
|
|
1359
1445
|
} catch {
|
|
1360
1446
|
return [];
|
|
1361
1447
|
}
|
|
1362
1448
|
}
|
|
1363
|
-
function branchUnpushedCount(cwd, branch) {
|
|
1449
|
+
async function branchUnpushedCount(cwd, branch) {
|
|
1364
1450
|
try {
|
|
1365
|
-
const n =
|
|
1366
|
-
cwd,
|
|
1367
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1368
|
-
}).toString().trim();
|
|
1451
|
+
const n = await git(cwd, ["rev-list", "--count", branch, "--not", "--remotes=origin"]);
|
|
1369
1452
|
return Number.parseInt(n, 10) || 0;
|
|
1370
1453
|
} catch {
|
|
1371
1454
|
return 0;
|
|
@@ -1378,392 +1461,56 @@ function samePath(a, b) {
|
|
|
1378
1461
|
return a === b;
|
|
1379
1462
|
}
|
|
1380
1463
|
}
|
|
1381
|
-
async function flushAllPendingWork(cwd, opts) {
|
|
1382
|
-
try {
|
|
1383
|
-
const primary = await flushPendingChanges(cwd, opts);
|
|
1384
|
-
await refreshRemoteToken(cwd, opts?.refreshToken);
|
|
1385
|
-
const currentBranch = getCurrentBranch(cwd);
|
|
1386
|
-
const worktreesSnapshotted = snapshotOtherWorktrees(cwd, opts?.wipMessage);
|
|
1387
|
-
const branchesBackedUp = backupOtherBranches(cwd, currentBranch);
|
|
1388
|
-
return {
|
|
1389
|
-
hadWork: primary.hadWork || worktreesSnapshotted > 0 || branchesBackedUp > 0,
|
|
1390
|
-
branchesBackedUp,
|
|
1391
|
-
worktreesSnapshotted
|
|
1392
|
-
};
|
|
1393
|
-
} catch {
|
|
1394
|
-
return { hadWork: false, branchesBackedUp: 0, worktreesSnapshotted: 0 };
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
function snapshotOtherWorktrees(cwd, wipMessage) {
|
|
1398
|
-
let count = 0;
|
|
1399
|
-
for (const wt of listWorktrees(cwd)) {
|
|
1400
|
-
if (samePath(wt.path, cwd) || !wt.branch) continue;
|
|
1401
|
-
try {
|
|
1402
|
-
if (!hasUncommittedChanges(wt.path)) continue;
|
|
1403
|
-
const sha = createWipSnapshot(wt.path, wipMessage ?? "WIP: conveyor-agent snapshot");
|
|
1404
|
-
if (sha && tryPushRefspec(wt.path, `${sha}:refs/heads/${wipRefForBranch(wt.branch)}`, true)) {
|
|
1405
|
-
wipRefPushed.add(wt.path);
|
|
1406
|
-
count++;
|
|
1407
|
-
}
|
|
1408
|
-
} catch {
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
return count;
|
|
1412
|
-
}
|
|
1413
|
-
function backupOtherBranches(cwd, currentBranch) {
|
|
1414
|
-
let count = 0;
|
|
1415
|
-
for (const branch of listLocalBranches(cwd)) {
|
|
1416
|
-
if (branch === currentBranch || branch.startsWith("conveyor-wip/")) continue;
|
|
1417
|
-
try {
|
|
1418
|
-
if (branchUnpushedCount(cwd, branch) === 0) continue;
|
|
1419
|
-
if (tryPushRefspec(cwd, `refs/heads/${branch}:refs/heads/${branchBackupRef(branch)}`, true)) {
|
|
1420
|
-
count++;
|
|
1421
|
-
}
|
|
1422
|
-
} catch {
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
return count;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
// src/runner/plan-sync.ts
|
|
1429
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
1430
|
-
import { join as join2 } from "path";
|
|
1431
|
-
|
|
1432
|
-
// src/harness/pty/settings.ts
|
|
1433
|
-
import { mkdir, writeFile, chmod } from "fs/promises";
|
|
1434
|
-
import { homedir } from "os";
|
|
1435
|
-
import { join } from "path";
|
|
1436
|
-
function claudeConfigHome() {
|
|
1437
|
-
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
1438
|
-
}
|
|
1439
|
-
function projectSlug(cwd) {
|
|
1440
|
-
return cwd.replace(/\//g, "-");
|
|
1441
|
-
}
|
|
1442
|
-
function sessionTranscriptPath(cwd, sessionId) {
|
|
1443
|
-
return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
|
|
1444
|
-
}
|
|
1445
|
-
function configHomePlansDir() {
|
|
1446
|
-
return join(claudeConfigHome(), "plans");
|
|
1447
|
-
}
|
|
1448
|
-
var ALLOW_RULES = [
|
|
1449
|
-
"Bash",
|
|
1450
|
-
"BashOutput",
|
|
1451
|
-
"Edit",
|
|
1452
|
-
"Write",
|
|
1453
|
-
"Read",
|
|
1454
|
-
"Glob",
|
|
1455
|
-
"Grep",
|
|
1456
|
-
"WebFetch",
|
|
1457
|
-
"WebSearch",
|
|
1458
|
-
"NotebookEdit",
|
|
1459
|
-
"Task",
|
|
1460
|
-
"TodoWrite",
|
|
1461
|
-
"ToolSearch",
|
|
1462
|
-
"KillShell",
|
|
1463
|
-
"SlashCommand",
|
|
1464
|
-
"Skill",
|
|
1465
|
-
"mcp__conveyor__*"
|
|
1466
|
-
];
|
|
1467
|
-
var HOOK_HELPER_SOURCE = `"use strict";
|
|
1468
|
-
const net = require("node:net");
|
|
1469
|
-
const crypto = require("node:crypto");
|
|
1470
|
-
|
|
1471
|
-
const PRE_TOOL_USE_TIMEOUT_MS = 110000;
|
|
1472
|
-
const ASK_USER_QUESTION_TIMEOUT_MS = 5000;
|
|
1473
|
-
|
|
1474
|
-
let raw = "";
|
|
1475
|
-
process.stdin.setEncoding("utf8");
|
|
1476
|
-
process.stdin.on("data", (chunk) => {
|
|
1477
|
-
raw += chunk;
|
|
1478
|
-
});
|
|
1479
|
-
process.stdin.on("end", () => {
|
|
1480
|
-
let payload = {};
|
|
1481
|
-
try {
|
|
1482
|
-
const parsed = JSON.parse(raw);
|
|
1483
|
-
if (parsed && typeof parsed === "object") payload = parsed;
|
|
1484
|
-
} catch {
|
|
1485
|
-
payload = {};
|
|
1486
|
-
}
|
|
1487
|
-
if (payload.hook_event_name === "PreToolUse") {
|
|
1488
|
-
preToolUse(payload);
|
|
1489
|
-
} else {
|
|
1490
|
-
relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
|
|
1491
|
-
}
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
function relayProgress(toolName) {
|
|
1495
|
-
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
1496
|
-
let done = false;
|
|
1497
|
-
const finish = () => {
|
|
1498
|
-
if (done) return;
|
|
1499
|
-
done = true;
|
|
1500
|
-
process.stdout.write(JSON.stringify({ continue: true }));
|
|
1501
|
-
process.exit(0);
|
|
1502
|
-
};
|
|
1503
|
-
const fallback = setTimeout(finish, 250);
|
|
1504
|
-
if (!socketPath) {
|
|
1505
|
-
clearTimeout(fallback);
|
|
1506
|
-
finish();
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
const client = net.connect(socketPath, () => {
|
|
1510
|
-
// PostToolUse does not supply tool duration, so elapsed_time_seconds is
|
|
1511
|
-
// omitted rather than reported as a misleading 0 (the field is optional).
|
|
1512
|
-
const line = JSON.stringify({ tool_name: toolName }) + "\\n";
|
|
1513
|
-
client.write(line, () => {
|
|
1514
|
-
client.end();
|
|
1515
|
-
});
|
|
1516
|
-
});
|
|
1517
|
-
client.on("close", () => {
|
|
1518
|
-
clearTimeout(fallback);
|
|
1519
|
-
finish();
|
|
1520
|
-
});
|
|
1521
|
-
client.on("error", () => {
|
|
1522
|
-
clearTimeout(fallback);
|
|
1523
|
-
finish();
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function preToolUse(payload) {
|
|
1528
|
-
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
1529
|
-
let done = false;
|
|
1530
|
-
const respond = (decision, reason) => {
|
|
1531
|
-
if (done) return;
|
|
1532
|
-
done = true;
|
|
1533
|
-
process.stdout.write(
|
|
1534
|
-
JSON.stringify({
|
|
1535
|
-
hookSpecificOutput: {
|
|
1536
|
-
hookEventName: "PreToolUse",
|
|
1537
|
-
permissionDecision: decision,
|
|
1538
|
-
...(reason ? { permissionDecisionReason: reason } : {}),
|
|
1539
|
-
},
|
|
1540
|
-
}),
|
|
1541
|
-
);
|
|
1542
|
-
process.exit(0);
|
|
1543
|
-
};
|
|
1544
|
-
// MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
|
|
1545
|
-
// Plan mode prompts for them despite permissions.allow (the CLI can't
|
|
1546
|
-
// classify MCP tools as read-only), and the pod is the sandbox.
|
|
1547
|
-
if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
|
|
1548
|
-
respond("allow");
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
// AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
|
|
1552
|
-
// missing/slow socket never denies the questionnaire. Everything else
|
|
1553
|
-
// (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
|
|
1554
|
-
const failOpen = payload.tool_name === "AskUserQuestion";
|
|
1555
|
-
const respondUnavailable = () =>
|
|
1556
|
-
failOpen
|
|
1557
|
-
? respond("allow")
|
|
1558
|
-
: respond(
|
|
1559
|
-
"deny",
|
|
1560
|
-
"Conveyor validation is unavailable right now. Wait a moment and call the tool again.",
|
|
1561
|
-
);
|
|
1562
|
-
if (!socketPath) {
|
|
1563
|
-
respondUnavailable();
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
const id = crypto.randomBytes(8).toString("hex");
|
|
1567
|
-
const timer = setTimeout(
|
|
1568
|
-
respondUnavailable,
|
|
1569
|
-
failOpen ? ASK_USER_QUESTION_TIMEOUT_MS : PRE_TOOL_USE_TIMEOUT_MS,
|
|
1570
|
-
);
|
|
1571
|
-
const client = net.connect(socketPath, () => {
|
|
1572
|
-
client.write(
|
|
1573
|
-
JSON.stringify({
|
|
1574
|
-
kind: "pre_tool_use",
|
|
1575
|
-
id,
|
|
1576
|
-
tool_name: typeof payload.tool_name === "string" ? payload.tool_name : "",
|
|
1577
|
-
tool_input:
|
|
1578
|
-
payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {},
|
|
1579
|
-
}) + "\\n",
|
|
1580
|
-
);
|
|
1581
|
-
});
|
|
1582
|
-
let buffer = "";
|
|
1583
|
-
client.on("data", (chunk) => {
|
|
1584
|
-
buffer += chunk.toString("utf8");
|
|
1585
|
-
let index = buffer.indexOf("\\n");
|
|
1586
|
-
while (index >= 0) {
|
|
1587
|
-
const line = buffer.slice(0, index);
|
|
1588
|
-
buffer = buffer.slice(index + 1);
|
|
1589
|
-
try {
|
|
1590
|
-
const verdict = JSON.parse(line);
|
|
1591
|
-
if (verdict && verdict.id === id) {
|
|
1592
|
-
clearTimeout(timer);
|
|
1593
|
-
client.end();
|
|
1594
|
-
respond(verdict.decision === "allow" ? "allow" : "deny", verdict.reason);
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
|
-
} catch {
|
|
1598
|
-
/* keep scanning */
|
|
1599
|
-
}
|
|
1600
|
-
index = buffer.indexOf("\\n");
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
client.on("error", () => {
|
|
1604
|
-
clearTimeout(timer);
|
|
1605
|
-
respondUnavailable();
|
|
1606
|
-
});
|
|
1607
|
-
client.on("close", () => {
|
|
1608
|
-
clearTimeout(timer);
|
|
1609
|
-
respondUnavailable();
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
`;
|
|
1613
|
-
async function writeHookSettings(dir) {
|
|
1614
|
-
const helperPath = join(dir, "hook-helper.cjs");
|
|
1615
|
-
const settingsPath = join(dir, "settings.json");
|
|
1616
|
-
await mkdir(dir, { recursive: true });
|
|
1617
|
-
await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
|
|
1618
|
-
await chmod(helperPath, 493);
|
|
1619
|
-
const settings = {
|
|
1620
|
-
// Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
|
|
1621
|
-
// spawns pass `--dangerously-skip-permissions`; on a real PTY the CLI otherwise
|
|
1622
|
-
// parks on the interactive "Yes, I accept / No, exit" dialog whose default focus
|
|
1623
|
-
// is "No, exit" — so it cannot be auto-dismissed by an Enter nudge and the run
|
|
1624
|
-
// stalls until the auto-nudge watchdog shuts it down. The CLI reads this key
|
|
1625
|
-
// from the `--settings` (flagSettings) layer via its bypass-mode gate, so
|
|
1626
|
-
// setting it here suppresses the dialog deterministically on every spawn —
|
|
1627
|
-
// independent of the `bypassPermissionsModeAccepted` seed in credentials.ts,
|
|
1628
|
-
// which the CLI's one-time migration consumes and which is subject to
|
|
1629
|
-
// persistence races on the codespace user-home.
|
|
1630
|
-
skipDangerousModePermissionPrompt: true,
|
|
1631
|
-
// Auto-approve tool calls so the interactive (PTY) agent never stops to
|
|
1632
|
-
// prompt the viewer. This only suppresses per-tool approval prompts — it
|
|
1633
|
-
// does NOT relax the planning gate: in discovery/plan mode the spawn passes
|
|
1634
|
-
// `--permission-mode plan`, which keeps the agent read-only (no edits/commits
|
|
1635
|
-
// until it exits plan mode) regardless of this allow-list. In build mode the
|
|
1636
|
-
// spawn already bypasses prompts via `--dangerously-skip-permissions`.
|
|
1637
|
-
// NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
|
|
1638
|
-
// a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
|
|
1639
|
-
permissions: {
|
|
1640
|
-
allow: ALLOW_RULES
|
|
1641
|
-
},
|
|
1642
|
-
hooks: {
|
|
1643
|
-
PreToolUse: [
|
|
1644
|
-
{
|
|
1645
|
-
matcher: "ExitPlanMode",
|
|
1646
|
-
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 120 }]
|
|
1647
|
-
},
|
|
1648
|
-
{
|
|
1649
|
-
// Observe-only: lets the session report waiting_for_input the moment
|
|
1650
|
-
// the planning questionnaire renders. The helper fails open for this
|
|
1651
|
-
// tool, so the questionnaire is never blocked by Conveyor.
|
|
1652
|
-
matcher: "AskUserQuestion",
|
|
1653
|
-
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
1654
|
-
},
|
|
1655
|
-
{
|
|
1656
|
-
// Plan-permission spawns prompt for MCP tools even when they're in
|
|
1657
|
-
// permissions.allow — the CLI can't classify them as read-only. A
|
|
1658
|
-
// hook allow bypasses the permission engine in every mode. Loose by
|
|
1659
|
-
// design: the pod is the sandbox, and planning agents must call
|
|
1660
|
-
// update_task etc. The helper answers locally (no socket).
|
|
1661
|
-
matcher: "mcp__.*",
|
|
1662
|
-
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
1663
|
-
}
|
|
1664
|
-
],
|
|
1665
|
-
PostToolUse: [
|
|
1666
|
-
{
|
|
1667
|
-
matcher: "*",
|
|
1668
|
-
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
|
|
1669
|
-
}
|
|
1670
|
-
]
|
|
1671
|
-
}
|
|
1672
|
-
};
|
|
1673
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
1674
|
-
return { settingsPath, helperPath };
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// src/runner/plan-sync.ts
|
|
1678
|
-
var PlanSync = class {
|
|
1679
|
-
planFileSnapshot = /* @__PURE__ */ new Map();
|
|
1680
|
-
lockedPlanFile = null;
|
|
1681
|
-
workspaceDir;
|
|
1682
|
-
connection;
|
|
1683
|
-
constructor(workspaceDir, connection) {
|
|
1684
|
-
this.workspaceDir = workspaceDir;
|
|
1685
|
-
this.connection = connection;
|
|
1686
|
-
}
|
|
1687
|
-
updateWorkspaceDir(workspaceDir) {
|
|
1688
|
-
this.workspaceDir = workspaceDir;
|
|
1689
|
-
}
|
|
1690
|
-
getPlanDirs() {
|
|
1691
|
-
return [join2(this.workspaceDir, ".claude", "plans"), configHomePlansDir()];
|
|
1692
|
-
}
|
|
1693
|
-
snapshotPlanFiles() {
|
|
1694
|
-
this.planFileSnapshot.clear();
|
|
1695
|
-
this.lockedPlanFile = null;
|
|
1696
|
-
for (const plansDir of this.getPlanDirs()) {
|
|
1697
|
-
try {
|
|
1698
|
-
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1699
|
-
try {
|
|
1700
|
-
const fullPath = join2(plansDir, file);
|
|
1701
|
-
const stat6 = statSync(fullPath);
|
|
1702
|
-
this.planFileSnapshot.set(fullPath, stat6.mtimeMs);
|
|
1703
|
-
} catch {
|
|
1704
|
-
continue;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
} catch {
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1464
|
+
async function flushAllPendingWork(cwd, opts) {
|
|
1465
|
+
try {
|
|
1466
|
+
const primary = await flushPendingChanges(cwd, opts);
|
|
1467
|
+
await refreshRemoteToken(cwd, opts?.refreshToken);
|
|
1468
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
1469
|
+
const worktreesSnapshotted = await snapshotOtherWorktrees(cwd, opts?.wipMessage);
|
|
1470
|
+
const branchesBackedUp = await backupOtherBranches(cwd, currentBranch);
|
|
1471
|
+
return {
|
|
1472
|
+
hadWork: primary.hadWork || worktreesSnapshotted > 0 || branchesBackedUp > 0,
|
|
1473
|
+
branchesBackedUp,
|
|
1474
|
+
worktreesSnapshotted
|
|
1475
|
+
};
|
|
1476
|
+
} catch {
|
|
1477
|
+
return { hadWork: false, branchesBackedUp: 0, worktreesSnapshotted: 0 };
|
|
1710
1478
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
try {
|
|
1723
|
-
const stat6 = statSync(fullPath);
|
|
1724
|
-
const prevMtime = this.planFileSnapshot.get(fullPath);
|
|
1725
|
-
const isNew = prevMtime === void 0 || stat6.mtimeMs > prevMtime;
|
|
1726
|
-
if (isNew && (!newest || stat6.mtimeMs > newest.mtime)) {
|
|
1727
|
-
newest = { path: fullPath, mtime: stat6.mtimeMs };
|
|
1728
|
-
}
|
|
1729
|
-
} catch {
|
|
1730
|
-
continue;
|
|
1731
|
-
}
|
|
1479
|
+
}
|
|
1480
|
+
async function snapshotOtherWorktrees(cwd, wipMessage) {
|
|
1481
|
+
let count = 0;
|
|
1482
|
+
for (const wt of await listWorktrees(cwd)) {
|
|
1483
|
+
if (samePath(wt.path, cwd) || !wt.branch) continue;
|
|
1484
|
+
try {
|
|
1485
|
+
if (!await hasUncommittedChanges(wt.path)) continue;
|
|
1486
|
+
const sha = await createWipSnapshot(wt.path, wipMessage ?? "WIP: conveyor-agent snapshot");
|
|
1487
|
+
if (sha && await tryPushRefspec(wt.path, `${sha}:refs/heads/${wipRefForBranch(wt.branch)}`, true)) {
|
|
1488
|
+
wipRefPushed.add(wt.path);
|
|
1489
|
+
count++;
|
|
1732
1490
|
}
|
|
1491
|
+
} catch {
|
|
1733
1492
|
}
|
|
1734
|
-
return newest;
|
|
1735
1493
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
const fileName = target.split("/").pop() ?? "plan";
|
|
1744
|
-
let content;
|
|
1494
|
+
return count;
|
|
1495
|
+
}
|
|
1496
|
+
async function backupOtherBranches(cwd, currentBranch) {
|
|
1497
|
+
let count = 0;
|
|
1498
|
+
for (const branch of await listLocalBranches(cwd)) {
|
|
1499
|
+
if (branch === currentBranch || branch.startsWith("conveyor-wip/")) continue;
|
|
1745
1500
|
try {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
)
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
const result = await this.connection.updateTaskFields({ plan: content });
|
|
1756
|
-
if (!result.ok) {
|
|
1757
|
-
this.connection.postChatMessage(
|
|
1758
|
-
`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.`
|
|
1759
|
-
);
|
|
1760
|
-
return;
|
|
1501
|
+
if (await branchUnpushedCount(cwd, branch) === 0) continue;
|
|
1502
|
+
if (await tryPushRefspec(
|
|
1503
|
+
cwd,
|
|
1504
|
+
`refs/heads/${branch}:refs/heads/${branchBackupRef(branch)}`,
|
|
1505
|
+
true
|
|
1506
|
+
)) {
|
|
1507
|
+
count++;
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1761
1510
|
}
|
|
1762
|
-
const verb = isInitialDetection ? "Detected local plan file" : "Synced local plan file";
|
|
1763
|
-
const suffix = isInitialDetection ? " and synced it to the task plan." : " to the task plan.";
|
|
1764
|
-
this.connection.postChatMessage(`${verb} (${fileName})${suffix}`);
|
|
1765
1511
|
}
|
|
1766
|
-
|
|
1512
|
+
return count;
|
|
1513
|
+
}
|
|
1767
1514
|
|
|
1768
1515
|
// src/setup/git-ready.ts
|
|
1769
1516
|
import { access, stat } from "fs/promises";
|
|
@@ -1814,69 +1561,6 @@ async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog)
|
|
|
1814
1561
|
// src/runner/work-preservation/index.ts
|
|
1815
1562
|
import { rm as rm3 } from "fs/promises";
|
|
1816
1563
|
|
|
1817
|
-
// src/runner/work-preservation/pg-dump.ts
|
|
1818
|
-
import { execSync as execSync2 } from "child_process";
|
|
1819
|
-
import { randomUUID } from "crypto";
|
|
1820
|
-
import { tmpdir } from "os";
|
|
1821
|
-
import path from "path";
|
|
1822
|
-
var PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
|
|
1823
|
-
function connectionDefaults(opts) {
|
|
1824
|
-
return {
|
|
1825
|
-
host: opts.host ?? "localhost",
|
|
1826
|
-
port: opts.port ?? 5432,
|
|
1827
|
-
db: opts.db ?? "conveyor"
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
function buildPgDumpCommand(opts) {
|
|
1831
|
-
const { host, port, db } = connectionDefaults(opts);
|
|
1832
|
-
return `pg_dump -h ${host} -p ${port} -U postgres ${db} -f ${JSON.stringify(
|
|
1833
|
-
opts.outputPath
|
|
1834
|
-
)} --no-owner --no-privileges`;
|
|
1835
|
-
}
|
|
1836
|
-
function buildPsqlRestoreCommand(opts) {
|
|
1837
|
-
const { host, port, db } = connectionDefaults(opts);
|
|
1838
|
-
return `psql -h ${host} -p ${port} -U postgres -d ${db} -f ${JSON.stringify(opts.dumpPath)}`;
|
|
1839
|
-
}
|
|
1840
|
-
function hasPostgresDep() {
|
|
1841
|
-
return (process.env.CONVEYOR_DEPS ?? "").split(",").map((dep) => dep.trim()).includes("postgresql");
|
|
1842
|
-
}
|
|
1843
|
-
function execErrorDetail(err) {
|
|
1844
|
-
const withStreams = err;
|
|
1845
|
-
const stderr = withStreams.stderr instanceof Buffer ? withStreams.stderr.toString("utf8").trim() : typeof withStreams.stderr === "string" ? withStreams.stderr.trim() : "";
|
|
1846
|
-
const message = withStreams.message ?? String(err);
|
|
1847
|
-
return stderr ? `${message} \u2014 stderr: ${stderr}` : message;
|
|
1848
|
-
}
|
|
1849
|
-
function dumpPostgresIfPresent(exec2 = execSync2) {
|
|
1850
|
-
if (!hasPostgresDep()) return Promise.resolve({ dumped: false });
|
|
1851
|
-
const outputPath = path.join(tmpdir(), `conveyor-pgdump-${randomUUID()}.sql`);
|
|
1852
|
-
try {
|
|
1853
|
-
exec2(buildPgDumpCommand({ outputPath }), {
|
|
1854
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1855
|
-
timeout: 6e4
|
|
1856
|
-
});
|
|
1857
|
-
return Promise.resolve({ dumped: true, path: outputPath });
|
|
1858
|
-
} catch (err) {
|
|
1859
|
-
process.stderr.write(`[conveyor-agent] pg_dump failed: ${execErrorDetail(err)}
|
|
1860
|
-
`);
|
|
1861
|
-
return Promise.resolve({ dumped: false });
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
function applyPgDumpIfConfigured(dumpPath, exec2 = execSync2) {
|
|
1865
|
-
if (!hasPostgresDep()) return false;
|
|
1866
|
-
try {
|
|
1867
|
-
exec2(buildPsqlRestoreCommand({ dumpPath }), {
|
|
1868
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1869
|
-
timeout: 12e4
|
|
1870
|
-
});
|
|
1871
|
-
process.stderr.write("[conveyor-agent] Restored postgres state from snapshot pg dump\n");
|
|
1872
|
-
return true;
|
|
1873
|
-
} catch (err) {
|
|
1874
|
-
process.stderr.write(`[conveyor-agent] psql dump restore failed: ${execErrorDetail(err)}
|
|
1875
|
-
`);
|
|
1876
|
-
return false;
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
1564
|
// src/runner/work-preservation/restore-precedence.ts
|
|
1881
1565
|
function selectRestoreSource(probe) {
|
|
1882
1566
|
if (probe.gcsAvailable) return "gcs";
|
|
@@ -1885,25 +1569,27 @@ function selectRestoreSource(probe) {
|
|
|
1885
1569
|
}
|
|
1886
1570
|
|
|
1887
1571
|
// src/runner/work-preservation/snapshot-tar.ts
|
|
1888
|
-
import {
|
|
1889
|
-
import { createReadStream, createWriteStream
|
|
1890
|
-
import {
|
|
1891
|
-
import { tmpdir
|
|
1892
|
-
import
|
|
1572
|
+
import { execFile as execFile2 } from "child_process";
|
|
1573
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
1574
|
+
import { mkdtemp, rm, stat as stat2, writeFile } from "fs/promises";
|
|
1575
|
+
import { tmpdir } from "os";
|
|
1576
|
+
import path from "path";
|
|
1893
1577
|
import { pipeline } from "stream/promises";
|
|
1578
|
+
import { promisify as promisify2 } from "util";
|
|
1894
1579
|
import { createGzip } from "zlib";
|
|
1895
1580
|
import * as tar from "tar";
|
|
1896
1581
|
|
|
1897
1582
|
// src/runner/work-preservation/snapshot-artifact.ts
|
|
1898
|
-
function planSnapshotFiles(
|
|
1899
|
-
const include =
|
|
1583
|
+
function planSnapshotFiles(git2) {
|
|
1584
|
+
const include = git2.trackedAndUntracked();
|
|
1900
1585
|
const includeSet = new Set(include);
|
|
1901
|
-
const deletions = [...new Set(
|
|
1586
|
+
const deletions = [...new Set(git2.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
|
|
1902
1587
|
return { include, deletions };
|
|
1903
1588
|
}
|
|
1904
1589
|
|
|
1905
1590
|
// src/runner/work-preservation/snapshot-tar.ts
|
|
1906
1591
|
var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
|
|
1592
|
+
var LEGACY_PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
|
|
1907
1593
|
var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1908
1594
|
function parseStatusPorcelainZ(raw) {
|
|
1909
1595
|
const includes = [];
|
|
@@ -1926,60 +1612,55 @@ function parseStatusPorcelainZ(raw) {
|
|
|
1926
1612
|
}
|
|
1927
1613
|
return { includes, deletions };
|
|
1928
1614
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
cached = parseStatusPorcelainZ(out);
|
|
1939
|
-
}
|
|
1940
|
-
return cached;
|
|
1941
|
-
};
|
|
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());
|
|
1942
1624
|
return {
|
|
1943
|
-
trackedAndUntracked: () =>
|
|
1944
|
-
deletedSinceHead: () =>
|
|
1625
|
+
trackedAndUntracked: () => cached.includes,
|
|
1626
|
+
deletedSinceHead: () => cached.deletions
|
|
1945
1627
|
};
|
|
1946
1628
|
}
|
|
1947
|
-
function gitHead(cwd) {
|
|
1629
|
+
async function gitHead(cwd) {
|
|
1948
1630
|
try {
|
|
1949
|
-
|
|
1631
|
+
const { stdout } = await execFileAsync2("git", ["rev-parse", "HEAD"], {
|
|
1632
|
+
cwd,
|
|
1633
|
+
timeout: GIT_TIMEOUT_MS2
|
|
1634
|
+
});
|
|
1635
|
+
return stdout.toString().trim() || null;
|
|
1950
1636
|
} catch {
|
|
1951
1637
|
return null;
|
|
1952
1638
|
}
|
|
1953
1639
|
}
|
|
1954
|
-
async function buildSnapshotTar(cwd
|
|
1955
|
-
const staging = await mkdtemp(
|
|
1640
|
+
async function buildSnapshotTar(cwd) {
|
|
1641
|
+
const staging = await mkdtemp(path.join(tmpdir(), "conveyor-snapshot-"));
|
|
1956
1642
|
const cleanup = async () => {
|
|
1957
1643
|
await rm(staging, { recursive: true, force: true }).catch(() => {
|
|
1958
1644
|
});
|
|
1959
1645
|
};
|
|
1960
1646
|
try {
|
|
1961
|
-
const plan = planSnapshotFiles(realGitSurface(cwd));
|
|
1962
|
-
const head = gitHead(cwd);
|
|
1647
|
+
const plan = planSnapshotFiles(await realGitSurface(cwd));
|
|
1648
|
+
const head = await gitHead(cwd);
|
|
1963
1649
|
if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
|
|
1964
1650
|
const capturedAt = Date.now();
|
|
1965
1651
|
const manifest = {
|
|
1966
1652
|
head,
|
|
1967
1653
|
deletions: plan.deletions,
|
|
1968
|
-
capturedAt
|
|
1969
|
-
pgDump: Boolean(opts?.pgDumpPath)
|
|
1654
|
+
capturedAt
|
|
1970
1655
|
};
|
|
1971
|
-
await
|
|
1656
|
+
await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
|
|
1972
1657
|
const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
|
|
1973
|
-
|
|
1974
|
-
await copyFile(opts.pgDumpPath, path2.join(staging, PG_DUMP_FILENAME));
|
|
1975
|
-
stagedEntries.push(PG_DUMP_FILENAME);
|
|
1976
|
-
}
|
|
1977
|
-
const rawTarPath = path2.join(staging, "snapshot.tar");
|
|
1658
|
+
const rawTarPath = path.join(staging, "snapshot.tar");
|
|
1978
1659
|
await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
|
|
1979
1660
|
if (plan.include.length > 0) {
|
|
1980
1661
|
await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
|
|
1981
1662
|
}
|
|
1982
|
-
const tarPath =
|
|
1663
|
+
const tarPath = path.join(staging, "snapshot.tar.gz");
|
|
1983
1664
|
await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
|
|
1984
1665
|
await rm(rawTarPath, { force: true });
|
|
1985
1666
|
const { size } = await stat2(tarPath);
|
|
@@ -1997,20 +1678,13 @@ async function buildSnapshotTar(cwd, opts) {
|
|
|
1997
1678
|
}
|
|
1998
1679
|
}
|
|
1999
1680
|
function isWithinWorkspace(cwd, rel) {
|
|
2000
|
-
if (!rel ||
|
|
2001
|
-
const root =
|
|
2002
|
-
const abs =
|
|
2003
|
-
return abs !== root && abs.startsWith(root +
|
|
1681
|
+
if (!rel || path.isAbsolute(rel)) return false;
|
|
1682
|
+
const root = path.resolve(cwd);
|
|
1683
|
+
const abs = path.resolve(root, rel);
|
|
1684
|
+
return abs !== root && abs.startsWith(root + path.sep);
|
|
2004
1685
|
}
|
|
2005
1686
|
async function readSnapshotArchive(tarPath) {
|
|
2006
1687
|
let manifestRaw = null;
|
|
2007
|
-
let pgDumpDir = null;
|
|
2008
|
-
let pgDumpPath;
|
|
2009
|
-
let pgDumpWritten = Promise.resolve();
|
|
2010
|
-
const cleanup = async () => {
|
|
2011
|
-
if (pgDumpDir) await rm(pgDumpDir, { recursive: true, force: true }).catch(() => {
|
|
2012
|
-
});
|
|
2013
|
-
};
|
|
2014
1688
|
try {
|
|
2015
1689
|
await tar.list({
|
|
2016
1690
|
file: tarPath,
|
|
@@ -2021,86 +1695,59 @@ async function readSnapshotArchive(tarPath) {
|
|
|
2021
1695
|
entry.on("end", () => {
|
|
2022
1696
|
manifestRaw = Buffer.concat(chunks);
|
|
2023
1697
|
});
|
|
2024
|
-
} else if (entry.path === PG_DUMP_FILENAME) {
|
|
2025
|
-
pgDumpDir = mkdtempSync(path2.join(tmpdir2(), "conveyor-pgdump-restore-"));
|
|
2026
|
-
pgDumpPath = path2.join(pgDumpDir, "dump.sql");
|
|
2027
|
-
const dest = createWriteStream(pgDumpPath);
|
|
2028
|
-
pgDumpWritten = new Promise((resolve, reject) => {
|
|
2029
|
-
dest.on("finish", () => resolve());
|
|
2030
|
-
dest.on("error", reject);
|
|
2031
|
-
});
|
|
2032
|
-
entry.pipe(dest);
|
|
2033
1698
|
}
|
|
2034
1699
|
}
|
|
2035
1700
|
});
|
|
2036
|
-
await pgDumpWritten;
|
|
2037
1701
|
} catch {
|
|
2038
|
-
await cleanup();
|
|
2039
|
-
return null;
|
|
2040
|
-
}
|
|
2041
|
-
if (!manifestRaw) {
|
|
2042
|
-
await cleanup();
|
|
2043
1702
|
return null;
|
|
2044
1703
|
}
|
|
1704
|
+
if (!manifestRaw) return null;
|
|
2045
1705
|
try {
|
|
2046
1706
|
const parsed = JSON.parse(manifestRaw.toString("utf8"));
|
|
2047
|
-
if (typeof parsed.head !== "string" || !parsed.head)
|
|
2048
|
-
await cleanup();
|
|
2049
|
-
return null;
|
|
2050
|
-
}
|
|
1707
|
+
if (typeof parsed.head !== "string" || !parsed.head) return null;
|
|
2051
1708
|
return {
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0,
|
|
2056
|
-
pgDump: parsed.pgDump === true
|
|
2057
|
-
},
|
|
2058
|
-
...pgDumpPath ? { pgDumpPath } : {},
|
|
2059
|
-
cleanup
|
|
1709
|
+
head: parsed.head,
|
|
1710
|
+
deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
|
|
1711
|
+
capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0
|
|
2060
1712
|
};
|
|
2061
1713
|
} catch {
|
|
2062
|
-
await cleanup();
|
|
2063
1714
|
return null;
|
|
2064
1715
|
}
|
|
2065
1716
|
}
|
|
2066
|
-
async function extractSnapshotTar(cwd, tarPath
|
|
2067
|
-
const
|
|
2068
|
-
if (!
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === PG_DUMP_FILENAME) return false;
|
|
2081
|
-
filesExtracted++;
|
|
2082
|
-
return true;
|
|
1717
|
+
async function extractSnapshotTar(cwd, tarPath) {
|
|
1718
|
+
const manifest = await readSnapshotArchive(tarPath);
|
|
1719
|
+
if (!manifest) return { status: "invalid" };
|
|
1720
|
+
const currentHead = await gitHead(cwd);
|
|
1721
|
+
if (manifest.head !== currentHead) {
|
|
1722
|
+
return { status: "stale-head", snapshotHead: manifest.head, currentHead };
|
|
1723
|
+
}
|
|
1724
|
+
let filesExtracted = 0;
|
|
1725
|
+
await tar.extract({
|
|
1726
|
+
file: tarPath,
|
|
1727
|
+
cwd,
|
|
1728
|
+
filter: (entryPath) => {
|
|
1729
|
+
if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === LEGACY_PG_DUMP_FILENAME) {
|
|
1730
|
+
return false;
|
|
2083
1731
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
} finally {
|
|
2094
|
-
await read.cleanup();
|
|
1732
|
+
filesExtracted++;
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
let deletionsApplied = 0;
|
|
1737
|
+
for (const rel of manifest.deletions) {
|
|
1738
|
+
if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
|
|
1739
|
+
await rm(path.resolve(cwd, rel), { recursive: true, force: true });
|
|
1740
|
+
deletionsApplied++;
|
|
2095
1741
|
}
|
|
1742
|
+
return { status: "extracted", filesExtracted, deletionsApplied };
|
|
2096
1743
|
}
|
|
2097
1744
|
|
|
2098
1745
|
// src/runner/work-preservation/snapshot-transfer.ts
|
|
2099
|
-
import { randomUUID
|
|
1746
|
+
import { randomUUID } from "crypto";
|
|
2100
1747
|
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
|
|
2101
1748
|
import { rm as rm2 } from "fs/promises";
|
|
2102
|
-
import { tmpdir as
|
|
2103
|
-
import
|
|
1749
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1750
|
+
import path2 from "path";
|
|
2104
1751
|
import { Readable } from "stream";
|
|
2105
1752
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
2106
1753
|
var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
|
|
@@ -2125,7 +1772,7 @@ async function putSnapshotToGcs(url, tarResult) {
|
|
|
2125
1772
|
}
|
|
2126
1773
|
}
|
|
2127
1774
|
async function downloadSnapshotToTemp(url) {
|
|
2128
|
-
const tmpTar =
|
|
1775
|
+
const tmpTar = path2.join(tmpdir2(), `conveyor-snapshot-restore-${randomUUID()}.tar.gz`);
|
|
2129
1776
|
try {
|
|
2130
1777
|
const res = await fetch(url, {
|
|
2131
1778
|
method: "GET",
|
|
@@ -2152,13 +1799,12 @@ var EMPTY_GCS_RESULT = {
|
|
|
2152
1799
|
uploaded: false,
|
|
2153
1800
|
fileCount: 0,
|
|
2154
1801
|
deletionCount: 0,
|
|
2155
|
-
sizeBytes: 0
|
|
2156
|
-
pgDumped: false
|
|
1802
|
+
sizeBytes: 0
|
|
2157
1803
|
};
|
|
2158
1804
|
async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
2159
1805
|
let tarResult;
|
|
2160
1806
|
try {
|
|
2161
|
-
tarResult = await buildSnapshotTar(cwd
|
|
1807
|
+
tarResult = await buildSnapshotTar(cwd);
|
|
2162
1808
|
} catch {
|
|
2163
1809
|
return EMPTY_GCS_RESULT;
|
|
2164
1810
|
}
|
|
@@ -2168,8 +1814,7 @@ async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
|
2168
1814
|
uploaded,
|
|
2169
1815
|
fileCount: tarResult.fileCount,
|
|
2170
1816
|
deletionCount: tarResult.deletionCount,
|
|
2171
|
-
sizeBytes: tarResult.sizeBytes
|
|
2172
|
-
pgDumped: Boolean(opts?.pgDumpPath)
|
|
1817
|
+
sizeBytes: tarResult.sizeBytes
|
|
2173
1818
|
};
|
|
2174
1819
|
} finally {
|
|
2175
1820
|
await tarResult.cleanup();
|
|
@@ -2191,7 +1836,6 @@ async function uploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
|
2191
1836
|
}
|
|
2192
1837
|
async function captureSnapshot(ctx) {
|
|
2193
1838
|
const gcs = await uploadSnapshotToGcs(ctx.cwd, ctx.snapshotUploadUrl, {
|
|
2194
|
-
pgDumpPath: ctx.pgDumpPath,
|
|
2195
1839
|
forceUpload: ctx.forceUpload
|
|
2196
1840
|
});
|
|
2197
1841
|
const wipResult = await flushPendingChanges(ctx.cwd, {
|
|
@@ -2203,8 +1847,7 @@ async function captureSnapshot(ctx) {
|
|
|
2203
1847
|
deletionCount: gcs.deletionCount,
|
|
2204
1848
|
sizeBytes: gcs.sizeBytes,
|
|
2205
1849
|
gcsUploaded: gcs.uploaded,
|
|
2206
|
-
wipPushed: wipResult.committed || wipResult.pushed
|
|
2207
|
-
pgDumped: gcs.pgDumped
|
|
1850
|
+
wipPushed: wipResult.committed || wipResult.pushed
|
|
2208
1851
|
};
|
|
2209
1852
|
}
|
|
2210
1853
|
function startPeriodic(intervalMs, ctx) {
|
|
@@ -2229,21 +1872,13 @@ async function finalizeForSleep(ctx) {
|
|
|
2229
1872
|
stop();
|
|
2230
1873
|
if (inFlightCapture) await inFlightCapture.catch(() => {
|
|
2231
1874
|
});
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
// the finalize so teardown doesn't wait out the reconciler's cap.
|
|
2240
|
-
forceUpload: true
|
|
2241
|
-
});
|
|
2242
|
-
return { ...artifact, pgDumped: dump.dumped };
|
|
2243
|
-
} finally {
|
|
2244
|
-
if (dump.path) await rm3(dump.path, { force: true }).catch(() => {
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
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
|
+
});
|
|
2247
1882
|
}
|
|
2248
1883
|
async function restoreOnBoot(bundle, cwd) {
|
|
2249
1884
|
let gcsAvailable = false;
|
|
@@ -2265,7 +1900,7 @@ async function restoreOnBoot(bundle, cwd) {
|
|
|
2265
1900
|
}
|
|
2266
1901
|
}
|
|
2267
1902
|
}
|
|
2268
|
-
const wipRestoreResult = gcsAvailable ? "none" : restoreWipSnapshot(cwd, bundle.gitPlan.branch);
|
|
1903
|
+
const wipRestoreResult = gcsAvailable ? "none" : await restoreWipSnapshot(cwd, bundle.gitPlan.branch);
|
|
2269
1904
|
const source = selectRestoreSource({ gcsAvailable, wipRestoreResult });
|
|
2270
1905
|
return source === "gcs" ? { source, fileCount: gcsFileCount } : { source };
|
|
2271
1906
|
}
|
|
@@ -2282,7 +1917,9 @@ var AgentHeartbeatSchema = z.object({
|
|
|
2282
1917
|
sessionId: z.string().optional(),
|
|
2283
1918
|
timestamp: z.string(),
|
|
2284
1919
|
status: z.enum(["active", "idle", "building"]),
|
|
2285
|
-
currentAction: z.string().optional()
|
|
1920
|
+
currentAction: z.string().optional(),
|
|
1921
|
+
/** Sender-observed main event-loop lag (ms) — see AgentHeartbeat.loopLagMs. */
|
|
1922
|
+
loopLagMs: z.number().nonnegative().optional()
|
|
2286
1923
|
});
|
|
2287
1924
|
var CreatePRInputSchema = z.object({
|
|
2288
1925
|
title: z.string().min(1),
|
|
@@ -2564,6 +2201,10 @@ var ListActivePtySessionsRequestSchema = z.object({
|
|
|
2564
2201
|
var SendSoftStopRequestSchema = z.object({
|
|
2565
2202
|
taskId: z.string()
|
|
2566
2203
|
});
|
|
2204
|
+
var StopTaskSessionRequestSchema = z.object({
|
|
2205
|
+
taskId: z.string(),
|
|
2206
|
+
sessionId: z.string()
|
|
2207
|
+
});
|
|
2567
2208
|
var FlushTaskQueueRequestSchema = z.object({
|
|
2568
2209
|
taskId: z.string(),
|
|
2569
2210
|
softStop: z.boolean().optional()
|
|
@@ -2612,6 +2253,11 @@ var EmitAgentEventRequestSchema = z.object({
|
|
|
2612
2253
|
var RefreshGithubTokenRequestSchema = z.object({
|
|
2613
2254
|
sessionId: z.string()
|
|
2614
2255
|
});
|
|
2256
|
+
var ReportReviewSpawnFailureRequestSchema = z.object({
|
|
2257
|
+
sessionId: z.string(),
|
|
2258
|
+
reviewSessionId: z.string(),
|
|
2259
|
+
error: z.string().max(2e3).optional()
|
|
2260
|
+
});
|
|
2615
2261
|
var RefreshGithubTokenResponseSchema = z.object({
|
|
2616
2262
|
token: z.string()
|
|
2617
2263
|
});
|
|
@@ -2638,6 +2284,29 @@ var PtyResizeRequestSchema = z.object({
|
|
|
2638
2284
|
var PtyAttachRequestSchema = z.object({
|
|
2639
2285
|
sessionId: z.string()
|
|
2640
2286
|
});
|
|
2287
|
+
var PtyChatEventPayloadSchema = z.discriminatedUnion("kind", [
|
|
2288
|
+
z.object({
|
|
2289
|
+
kind: z.literal("init"),
|
|
2290
|
+
model: z.string().max(200),
|
|
2291
|
+
claudeSessionId: z.string().max(100).optional()
|
|
2292
|
+
}),
|
|
2293
|
+
z.object({ kind: z.literal("user_text"), text: z.string().max(16384) }),
|
|
2294
|
+
z.object({ kind: z.literal("assistant_text"), text: z.string().max(16384) }),
|
|
2295
|
+
z.object({
|
|
2296
|
+
kind: z.literal("tool_use"),
|
|
2297
|
+
name: z.string().max(200),
|
|
2298
|
+
// Compact preview: JSON.stringify(input) truncated agent-side.
|
|
2299
|
+
input: z.string().max(2e3)
|
|
2300
|
+
}),
|
|
2301
|
+
z.object({ kind: z.literal("turn_end") })
|
|
2302
|
+
]);
|
|
2303
|
+
var PtyChatEventRequestSchema = z.object({
|
|
2304
|
+
sessionId: z.string(),
|
|
2305
|
+
event: PtyChatEventPayloadSchema
|
|
2306
|
+
});
|
|
2307
|
+
var PtyChatAttachRequestSchema = z.object({
|
|
2308
|
+
sessionId: z.string()
|
|
2309
|
+
});
|
|
2641
2310
|
var CreatePRResponseSchema = z.object({
|
|
2642
2311
|
prNumber: z.number().int().positive(),
|
|
2643
2312
|
prUrl: z.string().url()
|
|
@@ -2778,6 +2447,13 @@ var ListMyLiveSessionsRequestSchema = z2.object({
|
|
|
2778
2447
|
var StartAdhocSessionRequestSchema = z2.object({
|
|
2779
2448
|
projectId: z2.string(),
|
|
2780
2449
|
label: z2.string().max(200).optional(),
|
|
2450
|
+
/**
|
|
2451
|
+
* Session role. Constrained: other task-less modes fall through to the pm
|
|
2452
|
+
* runner in the pod entrypoint, and "review" would crash without a task.
|
|
2453
|
+
*/
|
|
2454
|
+
mode: z2.enum(["adhoc", "pm"]).optional(),
|
|
2455
|
+
/** Base branch to check out (defaults to the project's dev branch). */
|
|
2456
|
+
branch: z2.string().max(300).optional(),
|
|
2781
2457
|
requestingUserId: z2.string().optional()
|
|
2782
2458
|
});
|
|
2783
2459
|
var StopAdhocSessionRequestSchema = z2.object({
|
|
@@ -3302,8 +2978,8 @@ var ClaudeCodeHarness = class {
|
|
|
3302
2978
|
|
|
3303
2979
|
// src/harness/pty/session.ts
|
|
3304
2980
|
import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4 } from "fs/promises";
|
|
3305
|
-
import { tmpdir as
|
|
3306
|
-
import { join as
|
|
2981
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2982
|
+
import { join as join3, dirname } from "path";
|
|
3307
2983
|
|
|
3308
2984
|
// src/harness/pty/event-queue.ts
|
|
3309
2985
|
var AsyncEventQueue = class {
|
|
@@ -3584,12 +3260,14 @@ function mapTranscriptRecord(raw) {
|
|
|
3584
3260
|
// src/harness/pty/jsonl-tailer.ts
|
|
3585
3261
|
var POLL_INTERVAL_MS = 25;
|
|
3586
3262
|
var JsonlTailer = class {
|
|
3587
|
-
constructor(path4, onEvent) {
|
|
3263
|
+
constructor(path4, onEvent, onRawRecord) {
|
|
3588
3264
|
this.path = path4;
|
|
3589
3265
|
this.onEvent = onEvent;
|
|
3266
|
+
this.onRawRecord = onRawRecord;
|
|
3590
3267
|
}
|
|
3591
3268
|
path;
|
|
3592
3269
|
onEvent;
|
|
3270
|
+
onRawRecord;
|
|
3593
3271
|
offset = 0;
|
|
3594
3272
|
buffer = "";
|
|
3595
3273
|
timer = null;
|
|
@@ -3661,11 +3339,109 @@ var JsonlTailer = class {
|
|
|
3661
3339
|
} catch {
|
|
3662
3340
|
return;
|
|
3663
3341
|
}
|
|
3342
|
+
this.onRawRecord?.(parsed);
|
|
3664
3343
|
const event = mapTranscriptRecord(parsed);
|
|
3665
3344
|
if (event) this.onEvent(event);
|
|
3666
3345
|
}
|
|
3667
3346
|
};
|
|
3668
3347
|
|
|
3348
|
+
// src/harness/pty/chat-record-mapper.ts
|
|
3349
|
+
var TEXT_MAX = 16e3;
|
|
3350
|
+
var TOOL_INPUT_MAX = 1900;
|
|
3351
|
+
function isRecord2(value) {
|
|
3352
|
+
return typeof value === "object" && value !== null;
|
|
3353
|
+
}
|
|
3354
|
+
function isUnknownArray2(value) {
|
|
3355
|
+
return Array.isArray(value);
|
|
3356
|
+
}
|
|
3357
|
+
function stringField2(record, ...keys) {
|
|
3358
|
+
for (const key of keys) {
|
|
3359
|
+
const value = record[key];
|
|
3360
|
+
if (typeof value === "string") return value;
|
|
3361
|
+
}
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
function truncate(text, max) {
|
|
3365
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
3366
|
+
}
|
|
3367
|
+
function isCommandWrapperText(text) {
|
|
3368
|
+
const trimmed = text.trimStart();
|
|
3369
|
+
return trimmed.startsWith("<command-name>") || trimmed.startsWith("<local-command-");
|
|
3370
|
+
}
|
|
3371
|
+
function mapSystem2(record) {
|
|
3372
|
+
if (record.subtype === "init") {
|
|
3373
|
+
const event = {
|
|
3374
|
+
kind: "init",
|
|
3375
|
+
model: stringField2(record, "model") ?? ""
|
|
3376
|
+
};
|
|
3377
|
+
const sessionId = stringField2(record, "session_id", "sessionId");
|
|
3378
|
+
if (sessionId !== void 0) event.claudeSessionId = sessionId;
|
|
3379
|
+
return [event];
|
|
3380
|
+
}
|
|
3381
|
+
if (record.subtype === "turn_duration") return [{ kind: "turn_end" }];
|
|
3382
|
+
return [];
|
|
3383
|
+
}
|
|
3384
|
+
function mapAssistant2(record) {
|
|
3385
|
+
const message = record.message;
|
|
3386
|
+
if (!isRecord2(message)) return [];
|
|
3387
|
+
const content = isUnknownArray2(message.content) ? message.content : [];
|
|
3388
|
+
const events = [];
|
|
3389
|
+
for (const raw of content) {
|
|
3390
|
+
if (!isRecord2(raw)) continue;
|
|
3391
|
+
if (raw.type === "text") {
|
|
3392
|
+
const text = stringField2(raw, "text");
|
|
3393
|
+
if (text && text.length > 0) {
|
|
3394
|
+
events.push({ kind: "assistant_text", text: truncate(text, TEXT_MAX) });
|
|
3395
|
+
}
|
|
3396
|
+
} else if (raw.type === "tool_use") {
|
|
3397
|
+
const name = stringField2(raw, "name");
|
|
3398
|
+
if (name) {
|
|
3399
|
+
const input = "input" in raw ? raw.input : void 0;
|
|
3400
|
+
events.push({
|
|
3401
|
+
kind: "tool_use",
|
|
3402
|
+
name: truncate(name, 200),
|
|
3403
|
+
input: JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX)
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return events;
|
|
3409
|
+
}
|
|
3410
|
+
function mapUser(record) {
|
|
3411
|
+
const message = record.message;
|
|
3412
|
+
if (!isRecord2(message)) return [];
|
|
3413
|
+
const content = message.content;
|
|
3414
|
+
let text;
|
|
3415
|
+
if (typeof content === "string") {
|
|
3416
|
+
text = content;
|
|
3417
|
+
} else if (isUnknownArray2(content)) {
|
|
3418
|
+
const hasToolResult = content.some((b) => isRecord2(b) && b.type === "tool_result");
|
|
3419
|
+
if (hasToolResult) return [];
|
|
3420
|
+
text = content.filter((b) => isRecord2(b) && b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").filter((t) => t.length > 0).join("\n");
|
|
3421
|
+
} else {
|
|
3422
|
+
return [];
|
|
3423
|
+
}
|
|
3424
|
+
const trimmed = text.trim();
|
|
3425
|
+
if (trimmed.length === 0 || isCommandWrapperText(trimmed)) return [];
|
|
3426
|
+
return [{ kind: "user_text", text: truncate(trimmed, TEXT_MAX) }];
|
|
3427
|
+
}
|
|
3428
|
+
function mapChatRecords(raw) {
|
|
3429
|
+
if (!isRecord2(raw)) return [];
|
|
3430
|
+
if (raw.isSidechain === true || raw.isMeta === true) return [];
|
|
3431
|
+
switch (raw.type) {
|
|
3432
|
+
case "system":
|
|
3433
|
+
return mapSystem2(raw);
|
|
3434
|
+
case "assistant":
|
|
3435
|
+
return mapAssistant2(raw);
|
|
3436
|
+
case "user":
|
|
3437
|
+
return mapUser(raw);
|
|
3438
|
+
case "result":
|
|
3439
|
+
return [{ kind: "turn_end" }];
|
|
3440
|
+
default:
|
|
3441
|
+
return [];
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3669
3445
|
// src/harness/pty/spawn-args.ts
|
|
3670
3446
|
function resolveClaudeBinary() {
|
|
3671
3447
|
return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
|
|
@@ -3714,26 +3490,268 @@ function cleanTerminalOutput(raw, maxChars = 1200) {
|
|
|
3714
3490
|
else if (code < 32 || code === 127) continue;
|
|
3715
3491
|
else out += ch;
|
|
3716
3492
|
}
|
|
3717
|
-
const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
3718
|
-
const text = lines.join("\n").trim();
|
|
3719
|
-
return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
|
|
3720
|
-
}
|
|
3721
|
-
function isMissingBinaryFailure(tail) {
|
|
3722
|
-
return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
|
|
3493
|
+
const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
3494
|
+
const text = lines.join("\n").trim();
|
|
3495
|
+
return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
|
|
3496
|
+
}
|
|
3497
|
+
function isMissingBinaryFailure(tail) {
|
|
3498
|
+
return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
|
|
3499
|
+
}
|
|
3500
|
+
function buildExitErrors(exitCode, rawOutput, binary) {
|
|
3501
|
+
const errors = [`claude exited (code ${exitCode}) without a result`];
|
|
3502
|
+
const tail = cleanTerminalOutput(rawOutput);
|
|
3503
|
+
if (isMissingBinaryFailure(tail)) {
|
|
3504
|
+
errors.push(
|
|
3505
|
+
`The \`${binary}\` CLI could not be started \u2014 it is not installed or not on PATH. Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) or set CONVEYOR_CLAUDE_BIN to its absolute path.`
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
if (tail) {
|
|
3509
|
+
errors.push(`Last terminal output before exit:
|
|
3510
|
+
${tail}`);
|
|
3511
|
+
}
|
|
3512
|
+
return errors;
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
// src/harness/pty/settings.ts
|
|
3516
|
+
import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
|
|
3517
|
+
import { homedir } from "os";
|
|
3518
|
+
import { join } from "path";
|
|
3519
|
+
function claudeConfigHome() {
|
|
3520
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
3521
|
+
}
|
|
3522
|
+
function projectSlug(cwd) {
|
|
3523
|
+
return cwd.replace(/\//g, "-");
|
|
3524
|
+
}
|
|
3525
|
+
function sessionTranscriptPath(cwd, sessionId) {
|
|
3526
|
+
return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
|
|
3527
|
+
}
|
|
3528
|
+
var ALLOW_RULES = [
|
|
3529
|
+
"Bash",
|
|
3530
|
+
"BashOutput",
|
|
3531
|
+
"Edit",
|
|
3532
|
+
"Write",
|
|
3533
|
+
"Read",
|
|
3534
|
+
"Glob",
|
|
3535
|
+
"Grep",
|
|
3536
|
+
"WebFetch",
|
|
3537
|
+
"WebSearch",
|
|
3538
|
+
"NotebookEdit",
|
|
3539
|
+
"Task",
|
|
3540
|
+
"TodoWrite",
|
|
3541
|
+
"ToolSearch",
|
|
3542
|
+
"KillShell",
|
|
3543
|
+
"SlashCommand",
|
|
3544
|
+
"Skill",
|
|
3545
|
+
"mcp__conveyor__*"
|
|
3546
|
+
];
|
|
3547
|
+
var HOOK_HELPER_SOURCE = `"use strict";
|
|
3548
|
+
const net = require("node:net");
|
|
3549
|
+
const crypto = require("node:crypto");
|
|
3550
|
+
|
|
3551
|
+
const PRE_TOOL_USE_TIMEOUT_MS = 110000;
|
|
3552
|
+
const ASK_USER_QUESTION_TIMEOUT_MS = 5000;
|
|
3553
|
+
|
|
3554
|
+
let raw = "";
|
|
3555
|
+
process.stdin.setEncoding("utf8");
|
|
3556
|
+
process.stdin.on("data", (chunk) => {
|
|
3557
|
+
raw += chunk;
|
|
3558
|
+
});
|
|
3559
|
+
process.stdin.on("end", () => {
|
|
3560
|
+
let payload = {};
|
|
3561
|
+
try {
|
|
3562
|
+
const parsed = JSON.parse(raw);
|
|
3563
|
+
if (parsed && typeof parsed === "object") payload = parsed;
|
|
3564
|
+
} catch {
|
|
3565
|
+
payload = {};
|
|
3566
|
+
}
|
|
3567
|
+
if (payload.hook_event_name === "PreToolUse") {
|
|
3568
|
+
preToolUse(payload);
|
|
3569
|
+
} else {
|
|
3570
|
+
relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
|
|
3571
|
+
}
|
|
3572
|
+
});
|
|
3573
|
+
|
|
3574
|
+
function relayProgress(toolName) {
|
|
3575
|
+
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
3576
|
+
let done = false;
|
|
3577
|
+
const finish = () => {
|
|
3578
|
+
if (done) return;
|
|
3579
|
+
done = true;
|
|
3580
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
3581
|
+
process.exit(0);
|
|
3582
|
+
};
|
|
3583
|
+
const fallback = setTimeout(finish, 250);
|
|
3584
|
+
if (!socketPath) {
|
|
3585
|
+
clearTimeout(fallback);
|
|
3586
|
+
finish();
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
const client = net.connect(socketPath, () => {
|
|
3590
|
+
// PostToolUse does not supply tool duration, so elapsed_time_seconds is
|
|
3591
|
+
// omitted rather than reported as a misleading 0 (the field is optional).
|
|
3592
|
+
const line = JSON.stringify({ tool_name: toolName }) + "\\n";
|
|
3593
|
+
client.write(line, () => {
|
|
3594
|
+
client.end();
|
|
3595
|
+
});
|
|
3596
|
+
});
|
|
3597
|
+
client.on("close", () => {
|
|
3598
|
+
clearTimeout(fallback);
|
|
3599
|
+
finish();
|
|
3600
|
+
});
|
|
3601
|
+
client.on("error", () => {
|
|
3602
|
+
clearTimeout(fallback);
|
|
3603
|
+
finish();
|
|
3604
|
+
});
|
|
3723
3605
|
}
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
const
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3606
|
+
|
|
3607
|
+
function preToolUse(payload) {
|
|
3608
|
+
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
3609
|
+
let done = false;
|
|
3610
|
+
const respond = (decision, reason) => {
|
|
3611
|
+
if (done) return;
|
|
3612
|
+
done = true;
|
|
3613
|
+
process.stdout.write(
|
|
3614
|
+
JSON.stringify({
|
|
3615
|
+
hookSpecificOutput: {
|
|
3616
|
+
hookEventName: "PreToolUse",
|
|
3617
|
+
permissionDecision: decision,
|
|
3618
|
+
...(reason ? { permissionDecisionReason: reason } : {}),
|
|
3619
|
+
},
|
|
3620
|
+
}),
|
|
3730
3621
|
);
|
|
3622
|
+
process.exit(0);
|
|
3623
|
+
};
|
|
3624
|
+
// MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
|
|
3625
|
+
// Plan mode prompts for them despite permissions.allow (the CLI can't
|
|
3626
|
+
// classify MCP tools as read-only), and the pod is the sandbox.
|
|
3627
|
+
if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
|
|
3628
|
+
respond("allow");
|
|
3629
|
+
return;
|
|
3731
3630
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3631
|
+
// AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
|
|
3632
|
+
// missing/slow socket never denies the questionnaire. Everything else
|
|
3633
|
+
// (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
|
|
3634
|
+
const failOpen = payload.tool_name === "AskUserQuestion";
|
|
3635
|
+
const respondUnavailable = () =>
|
|
3636
|
+
failOpen
|
|
3637
|
+
? respond("allow")
|
|
3638
|
+
: respond(
|
|
3639
|
+
"deny",
|
|
3640
|
+
"Conveyor validation is unavailable right now. Wait a moment and call the tool again.",
|
|
3641
|
+
);
|
|
3642
|
+
if (!socketPath) {
|
|
3643
|
+
respondUnavailable();
|
|
3644
|
+
return;
|
|
3735
3645
|
}
|
|
3736
|
-
|
|
3646
|
+
const id = crypto.randomBytes(8).toString("hex");
|
|
3647
|
+
const timer = setTimeout(
|
|
3648
|
+
respondUnavailable,
|
|
3649
|
+
failOpen ? ASK_USER_QUESTION_TIMEOUT_MS : PRE_TOOL_USE_TIMEOUT_MS,
|
|
3650
|
+
);
|
|
3651
|
+
const client = net.connect(socketPath, () => {
|
|
3652
|
+
client.write(
|
|
3653
|
+
JSON.stringify({
|
|
3654
|
+
kind: "pre_tool_use",
|
|
3655
|
+
id,
|
|
3656
|
+
tool_name: typeof payload.tool_name === "string" ? payload.tool_name : "",
|
|
3657
|
+
tool_input:
|
|
3658
|
+
payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {},
|
|
3659
|
+
}) + "\\n",
|
|
3660
|
+
);
|
|
3661
|
+
});
|
|
3662
|
+
let buffer = "";
|
|
3663
|
+
client.on("data", (chunk) => {
|
|
3664
|
+
buffer += chunk.toString("utf8");
|
|
3665
|
+
let index = buffer.indexOf("\\n");
|
|
3666
|
+
while (index >= 0) {
|
|
3667
|
+
const line = buffer.slice(0, index);
|
|
3668
|
+
buffer = buffer.slice(index + 1);
|
|
3669
|
+
try {
|
|
3670
|
+
const verdict = JSON.parse(line);
|
|
3671
|
+
if (verdict && verdict.id === id) {
|
|
3672
|
+
clearTimeout(timer);
|
|
3673
|
+
client.end();
|
|
3674
|
+
respond(verdict.decision === "allow" ? "allow" : "deny", verdict.reason);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
} catch {
|
|
3678
|
+
/* keep scanning */
|
|
3679
|
+
}
|
|
3680
|
+
index = buffer.indexOf("\\n");
|
|
3681
|
+
}
|
|
3682
|
+
});
|
|
3683
|
+
client.on("error", () => {
|
|
3684
|
+
clearTimeout(timer);
|
|
3685
|
+
respondUnavailable();
|
|
3686
|
+
});
|
|
3687
|
+
client.on("close", () => {
|
|
3688
|
+
clearTimeout(timer);
|
|
3689
|
+
respondUnavailable();
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
`;
|
|
3693
|
+
async function writeHookSettings(dir) {
|
|
3694
|
+
const helperPath = join(dir, "hook-helper.cjs");
|
|
3695
|
+
const settingsPath = join(dir, "settings.json");
|
|
3696
|
+
await mkdir(dir, { recursive: true });
|
|
3697
|
+
await writeFile2(helperPath, HOOK_HELPER_SOURCE, "utf8");
|
|
3698
|
+
await chmod(helperPath, 493);
|
|
3699
|
+
const settings = {
|
|
3700
|
+
// Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
|
|
3701
|
+
// spawns pass `--dangerously-skip-permissions`; on a real PTY the CLI otherwise
|
|
3702
|
+
// parks on the interactive "Yes, I accept / No, exit" dialog whose default focus
|
|
3703
|
+
// is "No, exit" — so it cannot be auto-dismissed by an Enter nudge and the run
|
|
3704
|
+
// stalls until the auto-nudge watchdog shuts it down. The CLI reads this key
|
|
3705
|
+
// from the `--settings` (flagSettings) layer via its bypass-mode gate, so
|
|
3706
|
+
// setting it here suppresses the dialog deterministically on every spawn —
|
|
3707
|
+
// independent of the `bypassPermissionsModeAccepted` seed in credentials.ts,
|
|
3708
|
+
// which the CLI's one-time migration consumes and which is subject to
|
|
3709
|
+
// persistence races on the codespace user-home.
|
|
3710
|
+
skipDangerousModePermissionPrompt: true,
|
|
3711
|
+
// Auto-approve tool calls so the interactive (PTY) agent never stops to
|
|
3712
|
+
// prompt the viewer. This only suppresses per-tool approval prompts — it
|
|
3713
|
+
// does NOT relax the planning gate: in discovery/plan mode the spawn passes
|
|
3714
|
+
// `--permission-mode plan`, which keeps the agent read-only (no edits/commits
|
|
3715
|
+
// until it exits plan mode) regardless of this allow-list. In build mode the
|
|
3716
|
+
// spawn already bypasses prompts via `--dangerously-skip-permissions`.
|
|
3717
|
+
// NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
|
|
3718
|
+
// a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
|
|
3719
|
+
permissions: {
|
|
3720
|
+
allow: ALLOW_RULES
|
|
3721
|
+
},
|
|
3722
|
+
hooks: {
|
|
3723
|
+
PreToolUse: [
|
|
3724
|
+
{
|
|
3725
|
+
matcher: "ExitPlanMode",
|
|
3726
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 120 }]
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
// Observe-only: lets the session report waiting_for_input the moment
|
|
3730
|
+
// the planning questionnaire renders. The helper fails open for this
|
|
3731
|
+
// tool, so the questionnaire is never blocked by Conveyor.
|
|
3732
|
+
matcher: "AskUserQuestion",
|
|
3733
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
3734
|
+
},
|
|
3735
|
+
{
|
|
3736
|
+
// Plan-permission spawns prompt for MCP tools even when they're in
|
|
3737
|
+
// permissions.allow — the CLI can't classify them as read-only. A
|
|
3738
|
+
// hook allow bypasses the permission engine in every mode. Loose by
|
|
3739
|
+
// design: the pod is the sandbox, and planning agents must call
|
|
3740
|
+
// update_task etc. The helper answers locally (no socket).
|
|
3741
|
+
matcher: "mcp__.*",
|
|
3742
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
3743
|
+
}
|
|
3744
|
+
],
|
|
3745
|
+
PostToolUse: [
|
|
3746
|
+
{
|
|
3747
|
+
matcher: "*",
|
|
3748
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
|
|
3749
|
+
}
|
|
3750
|
+
]
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
3754
|
+
return { settingsPath, helperPath };
|
|
3737
3755
|
}
|
|
3738
3756
|
|
|
3739
3757
|
// src/harness/pty/output-coalescer.ts
|
|
@@ -3791,7 +3809,7 @@ var PtyOutputCoalescer = class {
|
|
|
3791
3809
|
// src/harness/pty/tool-server.ts
|
|
3792
3810
|
import { createServer as createServer2 } from "http";
|
|
3793
3811
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
3794
|
-
import { join as
|
|
3812
|
+
import { join as join2 } from "path";
|
|
3795
3813
|
import { randomBytes } from "crypto";
|
|
3796
3814
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3797
3815
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -3959,7 +3977,7 @@ async function startToolServers(mcpServers, tempDir) {
|
|
|
3959
3977
|
config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
|
|
3960
3978
|
}
|
|
3961
3979
|
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
3962
|
-
const mcpConfigPath =
|
|
3980
|
+
const mcpConfigPath = join2(tempDir, "mcp-config.json");
|
|
3963
3981
|
await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
3964
3982
|
return { servers, mcpConfigPath };
|
|
3965
3983
|
}
|
|
@@ -3978,6 +3996,18 @@ var PLAN_DIALOG_INTERVAL_MS = 1500;
|
|
|
3978
3996
|
var PLAN_DIALOG_SLOW_INTERVAL_MS = 5e3;
|
|
3979
3997
|
var PLAN_DIALOG_FAST_WINDOW_MS = 1e4;
|
|
3980
3998
|
var PLAN_DIALOG_WINDOW_MS = 9e4;
|
|
3999
|
+
function resolveSubmitNudgeTiming() {
|
|
4000
|
+
const envMs = (name, fallback) => {
|
|
4001
|
+
const raw = Number(process.env[name]);
|
|
4002
|
+
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
|
4003
|
+
};
|
|
4004
|
+
return {
|
|
4005
|
+
intervalMs: envMs("CONVEYOR_PTY_NUDGE_INTERVAL_MS", SUBMIT_NUDGE_INTERVAL_MS),
|
|
4006
|
+
slowIntervalMs: envMs("CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS", SUBMIT_NUDGE_SLOW_INTERVAL_MS),
|
|
4007
|
+
maxPresses: SUBMIT_NUDGE_MAX_PRESSES,
|
|
4008
|
+
windowMs: envMs("CONVEYOR_PTY_NUDGE_WINDOW_MS", SUBMIT_NUDGE_WINDOW_MS)
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
3981
4011
|
function turnOptionsFrom(options) {
|
|
3982
4012
|
return {
|
|
3983
4013
|
canUseTool: options.canUseTool,
|
|
@@ -3986,14 +4016,14 @@ function turnOptionsFrom(options) {
|
|
|
3986
4016
|
abortController: options.abortController
|
|
3987
4017
|
};
|
|
3988
4018
|
}
|
|
3989
|
-
function
|
|
4019
|
+
function isRecord3(value) {
|
|
3990
4020
|
return typeof value === "object" && value !== null;
|
|
3991
4021
|
}
|
|
3992
4022
|
function extractSpawn(mod) {
|
|
3993
|
-
if (!
|
|
4023
|
+
if (!isRecord3(mod)) return null;
|
|
3994
4024
|
if (typeof mod.spawn === "function") return mod.spawn;
|
|
3995
4025
|
const def = mod.default;
|
|
3996
|
-
if (
|
|
4026
|
+
if (isRecord3(def) && typeof def.spawn === "function") return def.spawn;
|
|
3997
4027
|
return null;
|
|
3998
4028
|
}
|
|
3999
4029
|
async function loadPtySpawn() {
|
|
@@ -4015,6 +4045,16 @@ function inheritedEnv(socketPath) {
|
|
|
4015
4045
|
function buildPromptBytes(text) {
|
|
4016
4046
|
return `\x1B[200~${text}\x1B[201~`;
|
|
4017
4047
|
}
|
|
4048
|
+
function renderPromptContentText(content) {
|
|
4049
|
+
return content.map((block) => {
|
|
4050
|
+
const b = block;
|
|
4051
|
+
if (b?.type === "text" && typeof b.text === "string") return b.text;
|
|
4052
|
+
if (b?.type === "image") {
|
|
4053
|
+
return `[Image attachment \u2014 use list_task_files / get_attachment to view]`;
|
|
4054
|
+
}
|
|
4055
|
+
return JSON.stringify(block);
|
|
4056
|
+
}).join("\n\n");
|
|
4057
|
+
}
|
|
4018
4058
|
function sleep3(ms) {
|
|
4019
4059
|
return new Promise((resolve) => {
|
|
4020
4060
|
setTimeout(resolve, ms);
|
|
@@ -4031,9 +4071,9 @@ function parseUserQuestions(input) {
|
|
|
4031
4071
|
if (!Array.isArray(input.questions)) return [];
|
|
4032
4072
|
const questions = [];
|
|
4033
4073
|
for (const entry of input.questions) {
|
|
4034
|
-
if (!
|
|
4074
|
+
if (!isRecord3(entry)) continue;
|
|
4035
4075
|
if (typeof entry.question !== "string") continue;
|
|
4036
|
-
const options = Array.isArray(entry.options) ? entry.options.filter(
|
|
4076
|
+
const options = Array.isArray(entry.options) ? entry.options.filter(isRecord3).filter((o) => typeof o.label === "string").map((o) => ({
|
|
4037
4077
|
label: o.label,
|
|
4038
4078
|
description: typeof o.description === "string" ? o.description : ""
|
|
4039
4079
|
})) : [];
|
|
@@ -4281,8 +4321,8 @@ var PtySession = class {
|
|
|
4281
4321
|
return;
|
|
4282
4322
|
}
|
|
4283
4323
|
try {
|
|
4284
|
-
this.tempDir = await mkdtemp2(
|
|
4285
|
-
const socketPath =
|
|
4324
|
+
this.tempDir = await mkdtemp2(join3(tmpdir3(), "conveyor-pty-"));
|
|
4325
|
+
const socketPath = join3(this.tempDir, "hook.sock");
|
|
4286
4326
|
this.socket = new HookSocketServer(
|
|
4287
4327
|
socketPath,
|
|
4288
4328
|
(progress) => this.handleProgress(progress),
|
|
@@ -4294,7 +4334,14 @@ var PtySession = class {
|
|
|
4294
4334
|
const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
|
|
4295
4335
|
await mkdir2(dirname(transcriptPath), { recursive: true });
|
|
4296
4336
|
const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
|
|
4297
|
-
|
|
4337
|
+
const sendChat = this.bridge?.sendChatEvent?.bind(this.bridge);
|
|
4338
|
+
this.tailer = new JsonlTailer(
|
|
4339
|
+
transcriptPath,
|
|
4340
|
+
(event) => this.handleTranscriptEvent(event),
|
|
4341
|
+
sendChat ? (raw) => {
|
|
4342
|
+
for (const chatEvent of mapChatRecords(raw)) sendChat(chatEvent);
|
|
4343
|
+
} : void 0
|
|
4344
|
+
);
|
|
4298
4345
|
this.tailer.start(startOffset);
|
|
4299
4346
|
await this.spawn(settingsPath, socketPath);
|
|
4300
4347
|
if (signal) {
|
|
@@ -4454,7 +4501,7 @@ var PtySession = class {
|
|
|
4454
4501
|
}
|
|
4455
4502
|
for await (const message of this.turnPrompt) {
|
|
4456
4503
|
const content = message.message.content;
|
|
4457
|
-
const text = typeof content === "string" ? content :
|
|
4504
|
+
const text = typeof content === "string" ? content : renderPromptContentText(content);
|
|
4458
4505
|
await this.deliverPrompt(text);
|
|
4459
4506
|
}
|
|
4460
4507
|
}
|
|
@@ -4477,20 +4524,21 @@ var PtySession = class {
|
|
|
4477
4524
|
if (this.submitNudgeTimer) return;
|
|
4478
4525
|
this.pendingSubmitNudge = true;
|
|
4479
4526
|
const startedAt = Date.now();
|
|
4527
|
+
const { intervalMs, slowIntervalMs, maxPresses, windowMs } = resolveSubmitNudgeTiming();
|
|
4480
4528
|
let presses = 0;
|
|
4481
4529
|
const press = () => {
|
|
4482
|
-
if (this._toreDown || !this.pendingSubmitNudge || Date.now() - startedAt >=
|
|
4530
|
+
if (this._toreDown || !this.pendingSubmitNudge || Date.now() - startedAt >= windowMs) {
|
|
4483
4531
|
this.disarmSubmitNudge();
|
|
4484
4532
|
return;
|
|
4485
4533
|
}
|
|
4486
4534
|
presses++;
|
|
4487
4535
|
this.writeStdin("\r");
|
|
4488
|
-
if (presses ===
|
|
4536
|
+
if (presses === maxPresses && this.submitNudgeTimer) {
|
|
4489
4537
|
clearInterval(this.submitNudgeTimer);
|
|
4490
|
-
this.submitNudgeTimer = setInterval(press,
|
|
4538
|
+
this.submitNudgeTimer = setInterval(press, slowIntervalMs);
|
|
4491
4539
|
}
|
|
4492
4540
|
};
|
|
4493
|
-
this.submitNudgeTimer = setInterval(press,
|
|
4541
|
+
this.submitNudgeTimer = setInterval(press, intervalMs);
|
|
4494
4542
|
}
|
|
4495
4543
|
disarmSubmitNudge() {
|
|
4496
4544
|
this.pendingSubmitNudge = false;
|
|
@@ -4617,11 +4665,11 @@ var PtySession = class {
|
|
|
4617
4665
|
// src/harness/pty/credentials.ts
|
|
4618
4666
|
import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
4619
4667
|
import { homedir as homedir2 } from "os";
|
|
4620
|
-
import { join as
|
|
4668
|
+
import { join as join4 } from "path";
|
|
4621
4669
|
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
4622
4670
|
var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4623
4671
|
function claudeCredentialsPath() {
|
|
4624
|
-
return
|
|
4672
|
+
return join4(claudeConfigHome(), ".credentials.json");
|
|
4625
4673
|
}
|
|
4626
4674
|
function isConveyorCloudEnv(env = process.env) {
|
|
4627
4675
|
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
@@ -4695,7 +4743,7 @@ async function ensureClaudeCredentials(env = process.env) {
|
|
|
4695
4743
|
}
|
|
4696
4744
|
function claudeJsonPath() {
|
|
4697
4745
|
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
4698
|
-
return configDir ?
|
|
4746
|
+
return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
|
|
4699
4747
|
}
|
|
4700
4748
|
function asRecord(value) {
|
|
4701
4749
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
@@ -4951,7 +4999,7 @@ function createHarness(kind = "sdk", ptyBridge) {
|
|
|
4951
4999
|
|
|
4952
5000
|
// src/execution/query-executor.ts
|
|
4953
5001
|
import { createHash } from "crypto";
|
|
4954
|
-
import { existsSync
|
|
5002
|
+
import { existsSync as existsSync2, readFileSync, truncateSync } from "fs";
|
|
4955
5003
|
|
|
4956
5004
|
// src/execution/pack-runner-prompt.ts
|
|
4957
5005
|
function findLastAgentMessageIndex(history) {
|
|
@@ -5106,8 +5154,9 @@ function formatChatFile(file) {
|
|
|
5106
5154
|
return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]: ${file.downloadUrl}`];
|
|
5107
5155
|
}
|
|
5108
5156
|
if (file.content && file.contentEncoding === "base64") {
|
|
5157
|
+
const link = file.downloadUrl ? ` or download: ${file.downloadUrl}` : "";
|
|
5109
5158
|
return [
|
|
5110
|
-
`[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_attachment("${file.fileId}") to view]`
|
|
5159
|
+
`[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_attachment("${file.fileId}") to view${link}]`
|
|
5111
5160
|
];
|
|
5112
5161
|
}
|
|
5113
5162
|
return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`];
|
|
@@ -5119,8 +5168,9 @@ function formatTaskFile(file) {
|
|
|
5119
5168
|
}
|
|
5120
5169
|
if (file.content && file.contentEncoding === "base64") {
|
|
5121
5170
|
const size = formatFileSize(file.fileSize);
|
|
5171
|
+
const link = file.downloadUrl ? ` or download: ${file.downloadUrl}` : "";
|
|
5122
5172
|
return [
|
|
5123
|
-
`- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_attachment("${file.fileId}") to view]`
|
|
5173
|
+
`- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_attachment("${file.fileId}") to view${link}]`
|
|
5124
5174
|
];
|
|
5125
5175
|
}
|
|
5126
5176
|
if (!file.content) {
|
|
@@ -5603,7 +5653,7 @@ function buildDiscoveryPrompt(context, runnerMode) {
|
|
|
5603
5653
|
## Mode: Discovery`,
|
|
5604
5654
|
`You are in Discovery mode \u2014 helping plan and scope this task.`,
|
|
5605
5655
|
`- You have read-only codebase access (can read files, run git commands, search code)`,
|
|
5606
|
-
`- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
|
|
5656
|
+
`- You can write plan files in .claude/plans/ only \u2014 no other file writes. Plan files on disk are NOT synced to the task; save the plan via update_task_plan`,
|
|
5607
5657
|
`- Do NOT attempt to edit, write, or modify source code files \u2014 these operations will be denied`,
|
|
5608
5658
|
`- If you identify code changes needed, describe them in the plan instead of implementing them`,
|
|
5609
5659
|
`- You can create and manage subtasks`,
|
|
@@ -5653,7 +5703,7 @@ function buildDiscoveryPrompt(context, runnerMode) {
|
|
|
5653
5703
|
``,
|
|
5654
5704
|
`### Plan Verification Requirements`,
|
|
5655
5705
|
`Every plan MUST include a **Testing / Verification** section so the build agent has a clear definition of "done". Enumerate:`,
|
|
5656
|
-
`- The
|
|
5706
|
+
`- The scoped verification for the change: \`bun run check\` (lint + typecheck) plus \`bun run test:affected\` \u2014 name the specific package suites the diff will touch. Docs-only plans should state that no local gates are needed (CI validates on the PR).`,
|
|
5657
5707
|
`- Any task-specific end-to-end checks (manual UI walk-through, API smoke test, migration dry-run, etc.)`,
|
|
5658
5708
|
`You are NOT expected to run these gates yourself \u2014 discovery is read-only. Just describe them in the plan.`,
|
|
5659
5709
|
``,
|
|
@@ -5732,14 +5782,14 @@ function buildModePrompt(agentMode, context, runnerMode) {
|
|
|
5732
5782
|
`- Goal: coordinate child task execution and ensure all children complete successfully`
|
|
5733
5783
|
] : [
|
|
5734
5784
|
`- If this is a leaf task (no children): execute the plan directly`,
|
|
5735
|
-
`- Goal: implement the plan,
|
|
5785
|
+
`- Goal: implement the plan, run scoped verification, open a PR when done`,
|
|
5736
5786
|
``,
|
|
5737
5787
|
`### Pre-PR Verification Checklist`,
|
|
5738
|
-
`Before calling \`mcp__conveyor__create_pull_request\`,
|
|
5739
|
-
`1. \`bun run
|
|
5740
|
-
`2. \`bun run
|
|
5741
|
-
`
|
|
5742
|
-
`If a gate fails, fix it before opening the PR. Do NOT open PRs with known failing gates.`,
|
|
5788
|
+
`CI runs the FULL suite (lint, typecheck, all test shards) on every PR \u2014 do not duplicate it locally. Before calling \`mcp__conveyor__create_pull_request\`, scope verification to your diff:`,
|
|
5789
|
+
`1. \`bun run check\` \u2014 lint + typecheck (fast; run whenever you changed code)`,
|
|
5790
|
+
`2. \`bun run test:affected\` \u2014 runs only the tests your diff can affect (docs-only diffs run nothing; apps/api diffs run unit tests only since CI covers the int shards; shared/db diffs escalate to the full suite automatically)`,
|
|
5791
|
+
`Docs/markdown/.claude-only changes need NO local gates \u2014 open the PR and let CI validate.`,
|
|
5792
|
+
`If a gate fails, fix it before opening the PR. Do NOT open PRs with known failing gates. Never run the full \`bun run test\` for a diff confined to one package.`,
|
|
5743
5793
|
`For refactors: also run \`git diff ${context?.baseBranch ?? "dev"}..HEAD\` and confirm the public API surface (exports, function signatures) has no unintended breaking changes.`
|
|
5744
5794
|
]
|
|
5745
5795
|
];
|
|
@@ -5840,12 +5890,13 @@ function buildPmPreamble(context) {
|
|
|
5840
5890
|
`
|
|
5841
5891
|
Environment (ready, no setup required):`,
|
|
5842
5892
|
`- Repository is cloned at your current working directory.`,
|
|
5893
|
+
`- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`); never spend a tool call on a bare \`cd\`.`,
|
|
5843
5894
|
`- You can read files and run git commands to understand the codebase before writing task plans.`,
|
|
5844
5895
|
`- Check the dev branch (e.g. run: git fetch && git checkout dev || git checkout main) to understand the current state of the codebase that agents will branch off of.`,
|
|
5845
5896
|
`
|
|
5846
5897
|
Workflow:`,
|
|
5847
|
-
`- You can draft and iterate on plans in .claude/plans/*.md
|
|
5848
|
-
`-
|
|
5898
|
+
`- You can draft and iterate on plans in .claude/plans/*.md, but files on disk are NOT synced to the task.`,
|
|
5899
|
+
`- Save the plan to the task with update_task_plan \u2014 this is the only way the plan is persisted.`,
|
|
5849
5900
|
`- After saving the plan, call post_to_chat with a short summary for the team (your turn output is NOT posted to chat \u2014 they only see what you post), then end your turn. Do NOT attempt to execute the plan yourself.`,
|
|
5850
5901
|
`- A separate task agent will handle execution after the team reviews and approves your plan.`
|
|
5851
5902
|
];
|
|
@@ -5878,6 +5929,7 @@ Environment (ready, no setup required):`,
|
|
|
5878
5929
|
`- Repository is cloned at your current working directory.`,
|
|
5879
5930
|
`- You can read, write, and edit files directly.`,
|
|
5880
5931
|
`- You can run shell commands including git, build tools, and test runners.`,
|
|
5932
|
+
`- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`, \`bun run --cwd\`); never spend a tool call on a bare \`cd\`.`,
|
|
5881
5933
|
context.githubBranch ? `- You are working on branch: \`${context.githubBranch}\`` : "",
|
|
5882
5934
|
`
|
|
5883
5935
|
Safety rules:`,
|
|
@@ -5902,6 +5954,10 @@ Environment (fully ready \u2014 do NOT verify or set up):`,
|
|
|
5902
5954
|
`- Branch \`${context.githubBranch}\` is already checked out.`,
|
|
5903
5955
|
`- All dependencies are installed, database is migrated, and the dev server is running.`,
|
|
5904
5956
|
`- Git is configured. Commit and push directly to this branch.`,
|
|
5957
|
+
`- The shell cwd resets between Bash calls \u2014 always use absolute paths (or \`git -C\`, \`bun run --cwd\`); never spend a tool call on a bare \`cd\`.`,
|
|
5958
|
+
`- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output. Don't poll background jobs with sleep/ps/tail loops.`,
|
|
5959
|
+
`- The \`gh\` CLI may not be installed \u2014 use the mcp__conveyor__* tools for PR and task state.`,
|
|
5960
|
+
`- Read a file before your first Write/Edit to it, and batch multiple changes to the same file into a single call instead of many sequential edits.`,
|
|
5905
5961
|
`
|
|
5906
5962
|
IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the following:`,
|
|
5907
5963
|
`- bun/npm install, pip install, or any dependency installation`,
|
|
@@ -6164,7 +6220,7 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
|
|
|
6164
6220
|
`Your FIRST action should be reading the relevant source files mentioned in the plan, then writing code. Do NOT run install, build, lint, test, or dev server commands first \u2014 the environment is already set up.`,
|
|
6165
6221
|
`Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
|
|
6166
6222
|
`Use post_to_chat to keep the team informed (your turn output is NOT shown in chat \u2014 post_to_chat is the only way they see it): briefly post what you're doing when you begin meaningful implementation, and again when the PR is ready.`,
|
|
6167
|
-
`Before opening the PR,
|
|
6223
|
+
`Before opening the PR, verify with \`bun run check\` (lint + typecheck) and \`bun run test:affected\` (tests scoped to your diff \u2014 docs-only changes need no local gates). Fix any failures. Do NOT open a PR with known failing gates. CI runs the full suite on the PR \u2014 never run the full \`bun run test\` for a diff confined to one package.`,
|
|
6168
6224
|
`When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
|
|
6169
6225
|
];
|
|
6170
6226
|
if (isAutoMode) {
|
|
@@ -6236,7 +6292,7 @@ New messages since your last run:`,
|
|
|
6236
6292
|
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
6237
6293
|
`
|
|
6238
6294
|
Address the requested changes directly. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 go straight to implementing the feedback.`,
|
|
6239
|
-
`Before pushing or opening a PR, re-
|
|
6295
|
+
`Before pushing or opening a PR, re-verify with \`bun run check\` and \`bun run test:affected\`. Fix any failures \u2014 relaunches are the most common place verification gets skipped. CI runs the full suite on the PR; keep local runs scoped to your diff.`,
|
|
6240
6296
|
`Implement your updates and open a PR when finished.`
|
|
6241
6297
|
];
|
|
6242
6298
|
if (context.githubPRUrl) {
|
|
@@ -6679,11 +6735,11 @@ function buildCreatePullRequestTool(connection, config) {
|
|
|
6679
6735
|
async ({ title, body, branch, baseBranch, commitMessage, skipVerify }) => {
|
|
6680
6736
|
try {
|
|
6681
6737
|
const cwd = config.workspaceDir;
|
|
6682
|
-
if (hasUncommittedChanges(cwd)) {
|
|
6738
|
+
if (await hasUncommittedChanges(cwd)) {
|
|
6683
6739
|
const message = commitMessage || `${title}
|
|
6684
6740
|
|
|
6685
6741
|
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
|
|
6686
|
-
const commitHash = stageAndCommit(cwd, message);
|
|
6742
|
+
const commitHash = await stageAndCommit(cwd, message);
|
|
6687
6743
|
if (commitHash) {
|
|
6688
6744
|
connection.sendEvent({
|
|
6689
6745
|
type: "message",
|
|
@@ -6695,7 +6751,7 @@ Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>`;
|
|
|
6695
6751
|
);
|
|
6696
6752
|
}
|
|
6697
6753
|
}
|
|
6698
|
-
if (hasUnpushedCommits(cwd)) {
|
|
6754
|
+
if (await hasUnpushedCommits(cwd)) {
|
|
6699
6755
|
const pushSuccess = await pushToOrigin(
|
|
6700
6756
|
cwd,
|
|
6701
6757
|
async () => {
|
|
@@ -6894,7 +6950,7 @@ function buildMutationTools(connection, config) {
|
|
|
6894
6950
|
|
|
6895
6951
|
// src/tools/attachment-tools.ts
|
|
6896
6952
|
import { readFile as readFile3, stat as stat5 } from "fs/promises";
|
|
6897
|
-
import { basename, extname, isAbsolute, join as
|
|
6953
|
+
import { basename, extname, isAbsolute, join as join5 } from "path";
|
|
6898
6954
|
import { z as z6 } from "zod";
|
|
6899
6955
|
var IMAGE_MIME_BY_EXT = {
|
|
6900
6956
|
".png": "image/png",
|
|
@@ -6913,7 +6969,7 @@ function buildUploadAttachmentTool(connection, config) {
|
|
|
6913
6969
|
},
|
|
6914
6970
|
async ({ path: path4, title }) => {
|
|
6915
6971
|
try {
|
|
6916
|
-
const filePath = isAbsolute(path4) ? path4 :
|
|
6972
|
+
const filePath = isAbsolute(path4) ? path4 : join5(config.workspaceDir, path4);
|
|
6917
6973
|
const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
6918
6974
|
if (!mimeType) {
|
|
6919
6975
|
return textResult(
|
|
@@ -8048,7 +8104,6 @@ async function handleExitPlanMode(host, input) {
|
|
|
8048
8104
|
return { behavior: "allow", updatedInput: input };
|
|
8049
8105
|
}
|
|
8050
8106
|
try {
|
|
8051
|
-
await host.syncPlanFile();
|
|
8052
8107
|
const taskProps = await host.connection.getTaskProperties();
|
|
8053
8108
|
const missingProps = collectMissingProps(taskProps);
|
|
8054
8109
|
if (host.isParentTask) {
|
|
@@ -8318,7 +8373,7 @@ function sessionLineageKey(taskId, agentMode, runnerMode) {
|
|
|
8318
8373
|
}
|
|
8319
8374
|
function sessionFileExists(sessionUuid, cwd) {
|
|
8320
8375
|
try {
|
|
8321
|
-
return
|
|
8376
|
+
return existsSync2(sessionTranscriptPath(cwd, sessionUuid));
|
|
8322
8377
|
} catch {
|
|
8323
8378
|
return false;
|
|
8324
8379
|
}
|
|
@@ -8327,10 +8382,18 @@ function hasExistingSessionFile(taskId, cwd, lineage) {
|
|
|
8327
8382
|
const key = sessionLineageKey(taskId, lineage.agentMode, lineage.runnerMode);
|
|
8328
8383
|
return sessionFileExists(taskIdToSessionUuid(key), cwd);
|
|
8329
8384
|
}
|
|
8385
|
+
function resolveSessionStart(lineageKey, cwd) {
|
|
8386
|
+
const sessionUuid = taskIdToSessionUuid(lineageKey);
|
|
8387
|
+
if (sessionFileExists(sessionUuid, cwd)) {
|
|
8388
|
+
repairTornSessionFile(sessionTranscriptPath(cwd, sessionUuid));
|
|
8389
|
+
return { resume: sessionUuid };
|
|
8390
|
+
}
|
|
8391
|
+
return { sessionId: sessionUuid };
|
|
8392
|
+
}
|
|
8330
8393
|
function repairTornSessionFile(path4) {
|
|
8331
8394
|
try {
|
|
8332
|
-
if (!
|
|
8333
|
-
const content =
|
|
8395
|
+
if (!existsSync2(path4)) return false;
|
|
8396
|
+
const content = readFileSync(path4, "utf8");
|
|
8334
8397
|
if (content.length === 0) return false;
|
|
8335
8398
|
let keepEnd = content.length;
|
|
8336
8399
|
if (!content.endsWith("\n")) {
|
|
@@ -8474,14 +8537,21 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
|
|
|
8474
8537
|
|
|
8475
8538
|
The team says:
|
|
8476
8539
|
${followUpText}` : followUpText;
|
|
8540
|
+
const isPty = host.harnessKind === "pty";
|
|
8477
8541
|
if (isPmMode) {
|
|
8478
|
-
const prompt = buildMultimodalPrompt(textPrompt, context);
|
|
8542
|
+
const prompt = buildMultimodalPrompt(textPrompt, context, isPty);
|
|
8479
8543
|
if (followUpImages.length > 0 && Array.isArray(prompt)) {
|
|
8480
8544
|
prompt.push(...followUpImages);
|
|
8481
8545
|
}
|
|
8482
8546
|
return prompt;
|
|
8483
8547
|
}
|
|
8484
8548
|
if (followUpImages.length > 0) {
|
|
8549
|
+
if (isPty) {
|
|
8550
|
+
const refs = followUpImages.map(
|
|
8551
|
+
() => `[Image attachment \u2014 use list_task_files / get_attachment to view]`
|
|
8552
|
+
);
|
|
8553
|
+
return [textPrompt, ...refs].join("\n");
|
|
8554
|
+
}
|
|
8485
8555
|
return [{ type: "text", text: textPrompt }, ...followUpImages];
|
|
8486
8556
|
}
|
|
8487
8557
|
return textPrompt;
|
|
@@ -8522,17 +8592,11 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
|
|
|
8522
8592
|
if (host.isStopped()) return;
|
|
8523
8593
|
const mode = host.agentMode;
|
|
8524
8594
|
const isDiscoveryLike = mode === "discovery" || mode === "help";
|
|
8525
|
-
const
|
|
8526
|
-
|
|
8527
|
-
host.
|
|
8528
|
-
}
|
|
8529
|
-
const sessionUuid = taskIdToSessionUuid(
|
|
8530
|
-
sessionLineageKey(context.taskId, mode, host.config.mode)
|
|
8595
|
+
const sessionStart = resolveSessionStart(
|
|
8596
|
+
sessionLineageKey(context.taskId, mode, host.config.mode),
|
|
8597
|
+
host.config.workspaceDir
|
|
8531
8598
|
);
|
|
8532
|
-
const hasExistingSession =
|
|
8533
|
-
if (hasExistingSession) {
|
|
8534
|
-
repairTornSessionFile(sessionTranscriptPath(host.config.workspaceDir, sessionUuid));
|
|
8535
|
-
}
|
|
8599
|
+
const hasExistingSession = !!sessionStart.resume;
|
|
8536
8600
|
const promptDelivery = promptDeliveryOverride ?? resolvePromptDelivery({
|
|
8537
8601
|
harnessKind: host.harnessKind,
|
|
8538
8602
|
runnerMode: host.config.mode,
|
|
@@ -8544,19 +8608,17 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
|
|
|
8544
8608
|
const options = {
|
|
8545
8609
|
...buildQueryOptions(host, context),
|
|
8546
8610
|
promptDelivery,
|
|
8547
|
-
...
|
|
8611
|
+
...sessionStart.sessionId ? { sessionId: sessionStart.sessionId } : {}
|
|
8548
8612
|
};
|
|
8549
|
-
const resume =
|
|
8613
|
+
const resume = sessionStart.resume;
|
|
8550
8614
|
if (followUpContent) {
|
|
8551
8615
|
await runFollowUpQuery(host, context, options, resume, followUpContent);
|
|
8552
|
-
} else if (isDiscoveryLike && promptDelivery !== "prefill") {
|
|
8553
8616
|
return;
|
|
8554
|
-
} else {
|
|
8555
|
-
await runInitialQuery(host, context, options, resume, promptDelivery);
|
|
8556
8617
|
}
|
|
8557
|
-
if (
|
|
8558
|
-
|
|
8618
|
+
if (isDiscoveryLike && promptDelivery !== "prefill") {
|
|
8619
|
+
return;
|
|
8559
8620
|
}
|
|
8621
|
+
await runInitialQuery(host, context, options, resume, promptDelivery);
|
|
8560
8622
|
}
|
|
8561
8623
|
async function runFollowUpQuery(host, context, options, resume, followUpContent) {
|
|
8562
8624
|
if (options.promptDelivery === "prefill") {
|
|
@@ -8625,7 +8687,7 @@ function latestUserMessageText(context) {
|
|
|
8625
8687
|
}
|
|
8626
8688
|
return null;
|
|
8627
8689
|
}
|
|
8628
|
-
function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAppendSystemPrompt) {
|
|
8690
|
+
function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAppendSystemPrompt, harnessKind) {
|
|
8629
8691
|
if (promptDelivery === "prefill") {
|
|
8630
8692
|
return {
|
|
8631
8693
|
prompt: latestUserMessageText(context) ?? "",
|
|
@@ -8633,7 +8695,9 @@ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAp
|
|
|
8633
8695
|
};
|
|
8634
8696
|
}
|
|
8635
8697
|
return {
|
|
8636
|
-
|
|
8698
|
+
// On the PTY harness image blocks would be pasted into the TUI as text —
|
|
8699
|
+
// the prompt body already links each image (get_attachment / downloadUrl).
|
|
8700
|
+
prompt: buildMultimodalPrompt(initialPrompt, context, harnessKind === "pty"),
|
|
8637
8701
|
appendSystemPrompt: baseAppendSystemPrompt
|
|
8638
8702
|
};
|
|
8639
8703
|
}
|
|
@@ -8648,7 +8712,8 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
|
|
|
8648
8712
|
promptDelivery,
|
|
8649
8713
|
initialPrompt,
|
|
8650
8714
|
context,
|
|
8651
|
-
options.appendSystemPrompt
|
|
8715
|
+
options.appendSystemPrompt,
|
|
8716
|
+
host.harnessKind
|
|
8652
8717
|
);
|
|
8653
8718
|
const queryOptions = { ...options, appendSystemPrompt };
|
|
8654
8719
|
let agentQuery = host.harness.executeQuery({
|
|
@@ -8673,7 +8738,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
8673
8738
|
const retryPrompt = buildMultimodalPrompt(
|
|
8674
8739
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
8675
8740
|
context,
|
|
8676
|
-
lastErrorWasImage
|
|
8741
|
+
lastErrorWasImage || host.harnessKind === "pty"
|
|
8677
8742
|
);
|
|
8678
8743
|
return host.harness.executeQuery({
|
|
8679
8744
|
prompt: host.createInputStream(retryPrompt),
|
|
@@ -8701,7 +8766,8 @@ async function handleAuthError(context, host, options) {
|
|
|
8701
8766
|
host.connection.storeSessionId("");
|
|
8702
8767
|
const freshPrompt = buildMultimodalPrompt(
|
|
8703
8768
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
8704
|
-
context
|
|
8769
|
+
context,
|
|
8770
|
+
host.harnessKind === "pty"
|
|
8705
8771
|
);
|
|
8706
8772
|
const freshQuery = host.harness.executeQuery({
|
|
8707
8773
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -8715,7 +8781,8 @@ async function handleStaleSession(context, host, options) {
|
|
|
8715
8781
|
host.connection.storeSessionId("");
|
|
8716
8782
|
const freshPrompt = buildMultimodalPrompt(
|
|
8717
8783
|
await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
|
|
8718
|
-
context
|
|
8784
|
+
context,
|
|
8785
|
+
host.harnessKind === "pty"
|
|
8719
8786
|
);
|
|
8720
8787
|
const freshQuery = host.harness.executeQuery({
|
|
8721
8788
|
prompt: host.createInputStream(freshPrompt),
|
|
@@ -8941,13 +9008,14 @@ function resolveHarnessKind() {
|
|
|
8941
9008
|
function buildPtyBridge(connection) {
|
|
8942
9009
|
return {
|
|
8943
9010
|
sendOutput: (data, dims) => connection.sendPtyOutput(data, dims),
|
|
9011
|
+
sendChatEvent: (event) => connection.sendPtyChatEvent(event),
|
|
8944
9012
|
sendEnded: () => connection.sendPtyEnded(),
|
|
8945
9013
|
onInput: (handler) => connection.onPtyInput(handler),
|
|
8946
9014
|
onResize: (handler) => connection.onPtyResize(handler)
|
|
8947
9015
|
};
|
|
8948
9016
|
}
|
|
8949
9017
|
var QueryBridge = class {
|
|
8950
|
-
constructor(connection, mode, runnerConfig, callbacks
|
|
9018
|
+
constructor(connection, mode, runnerConfig, callbacks) {
|
|
8951
9019
|
this.connection = connection;
|
|
8952
9020
|
this.mode = mode;
|
|
8953
9021
|
this.runnerConfig = runnerConfig;
|
|
@@ -8958,7 +9026,6 @@ var QueryBridge = class {
|
|
|
8958
9026
|
harnessKind,
|
|
8959
9027
|
harnessKind === "pty" ? buildPtyBridge(connection) : void 0
|
|
8960
9028
|
);
|
|
8961
|
-
this.planSync = new PlanSync(workspaceDir, connection);
|
|
8962
9029
|
}
|
|
8963
9030
|
connection;
|
|
8964
9031
|
mode;
|
|
@@ -8968,7 +9035,6 @@ var QueryBridge = class {
|
|
|
8968
9035
|
/** Which harness drives this bridge ("pty" task chat; "sdk" only under the
|
|
8969
9036
|
* CONVEYOR_FORCE_SDK_CARDS maintainer override). */
|
|
8970
9037
|
harnessKind;
|
|
8971
|
-
planSync;
|
|
8972
9038
|
sessionIds = /* @__PURE__ */ new Map();
|
|
8973
9039
|
activeQuery = null;
|
|
8974
9040
|
pendingToolOutputs = [];
|
|
@@ -9169,8 +9235,6 @@ var QueryBridge = class {
|
|
|
9169
9235
|
if (bridge.onSoftStop) bridge.onSoftStop();
|
|
9170
9236
|
},
|
|
9171
9237
|
createInputStream: (prompt) => bridge.createInputStream(prompt),
|
|
9172
|
-
snapshotPlanFiles: () => bridge.planSync.snapshotPlanFiles(),
|
|
9173
|
-
syncPlanFile: () => bridge.planSync.syncPlanFile(),
|
|
9174
9238
|
onModeTransition: bridge.onModeTransition
|
|
9175
9239
|
};
|
|
9176
9240
|
}
|
|
@@ -9186,9 +9250,9 @@ var QueryBridge = class {
|
|
|
9186
9250
|
};
|
|
9187
9251
|
|
|
9188
9252
|
// src/runner/session-runner-helpers.ts
|
|
9189
|
-
import { readFileSync as
|
|
9190
|
-
import { dirname as dirname2, join as
|
|
9191
|
-
import { fileURLToPath } from "url";
|
|
9253
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
9254
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
9255
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9192
9256
|
function mapChatHistory(messages) {
|
|
9193
9257
|
if (!messages) return [];
|
|
9194
9258
|
return messages.map((m) => ({
|
|
@@ -9213,10 +9277,10 @@ function mapChatHistory(messages) {
|
|
|
9213
9277
|
}
|
|
9214
9278
|
function readAgentVersion() {
|
|
9215
9279
|
try {
|
|
9216
|
-
const here = dirname2(
|
|
9280
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
9217
9281
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
9218
9282
|
try {
|
|
9219
|
-
const pkg = JSON.parse(
|
|
9283
|
+
const pkg = JSON.parse(readFileSync2(join6(here, rel), "utf-8"));
|
|
9220
9284
|
if (pkg.version) return pkg.version;
|
|
9221
9285
|
} catch {
|
|
9222
9286
|
}
|
|
@@ -9302,7 +9366,7 @@ async function sampleKeyUsage(token) {
|
|
|
9302
9366
|
|
|
9303
9367
|
// src/runner/port-discovery.ts
|
|
9304
9368
|
import { readFile as readFile4 } from "fs/promises";
|
|
9305
|
-
import { execFile } from "child_process";
|
|
9369
|
+
import { execFile as execFile3 } from "child_process";
|
|
9306
9370
|
var PROC_TCP_LISTEN_STATE = "0A";
|
|
9307
9371
|
function parseProcNetTcpListeners(content) {
|
|
9308
9372
|
const ports = [];
|
|
@@ -9337,7 +9401,7 @@ async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
|
|
|
9337
9401
|
}
|
|
9338
9402
|
async function readNetstatListeningPorts() {
|
|
9339
9403
|
const output = await new Promise((resolve) => {
|
|
9340
|
-
|
|
9404
|
+
execFile3("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
|
|
9341
9405
|
resolve(err ? null : stdout);
|
|
9342
9406
|
});
|
|
9343
9407
|
});
|
|
@@ -9488,10 +9552,12 @@ var PortDiscovery = class {
|
|
|
9488
9552
|
};
|
|
9489
9553
|
|
|
9490
9554
|
// src/runner/parent-pull-handler.ts
|
|
9491
|
-
import {
|
|
9492
|
-
|
|
9555
|
+
import { execFile as execFile4 } from "child_process";
|
|
9556
|
+
import { promisify as promisify3 } from "util";
|
|
9557
|
+
var execFileAsync3 = promisify3(execFile4);
|
|
9558
|
+
async function handlePullBranch(workDir, branch) {
|
|
9493
9559
|
if (!branch) return;
|
|
9494
|
-
const current = getCurrentBranch(workDir);
|
|
9560
|
+
const current = await getCurrentBranch(workDir);
|
|
9495
9561
|
if (current !== branch) {
|
|
9496
9562
|
process.stderr.write(
|
|
9497
9563
|
`[conveyor-agent] pull_branch ignored \u2014 current branch ${current ?? "(detached)"} != ${branch}
|
|
@@ -9499,7 +9565,7 @@ function handlePullBranch(workDir, branch) {
|
|
|
9499
9565
|
);
|
|
9500
9566
|
return;
|
|
9501
9567
|
}
|
|
9502
|
-
if (hasUncommittedChanges(workDir)) {
|
|
9568
|
+
if (await hasUncommittedChanges(workDir)) {
|
|
9503
9569
|
process.stderr.write(
|
|
9504
9570
|
`[conveyor-agent] pull_branch ignored \u2014 uncommitted changes on ${branch}
|
|
9505
9571
|
`
|
|
@@ -9507,16 +9573,15 @@ function handlePullBranch(workDir, branch) {
|
|
|
9507
9573
|
return;
|
|
9508
9574
|
}
|
|
9509
9575
|
try {
|
|
9510
|
-
|
|
9576
|
+
await execFileAsync3("git", ["fetch", "origin", branch], { cwd: workDir, timeout: 6e4 });
|
|
9511
9577
|
} catch {
|
|
9512
9578
|
process.stderr.write(`[conveyor-agent] pull_branch: fetch failed for ${branch}
|
|
9513
9579
|
`);
|
|
9514
9580
|
return;
|
|
9515
9581
|
}
|
|
9516
9582
|
try {
|
|
9517
|
-
|
|
9583
|
+
await execFileAsync3("git", ["pull", "--ff-only", "origin", branch], {
|
|
9518
9584
|
cwd: workDir,
|
|
9519
|
-
stdio: "ignore",
|
|
9520
9585
|
timeout: 6e4
|
|
9521
9586
|
});
|
|
9522
9587
|
process.stderr.write(`[conveyor-agent] pull_branch: pulled origin/${branch}
|
|
@@ -9529,6 +9594,33 @@ function handlePullBranch(workDir, branch) {
|
|
|
9529
9594
|
}
|
|
9530
9595
|
}
|
|
9531
9596
|
|
|
9597
|
+
// src/runner/heavy-gate.ts
|
|
9598
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
9599
|
+
import path3 from "path";
|
|
9600
|
+
var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
|
|
9601
|
+
function runDir() {
|
|
9602
|
+
return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
|
|
9603
|
+
}
|
|
9604
|
+
function pidAlive(pid) {
|
|
9605
|
+
try {
|
|
9606
|
+
process.kill(pid, 0);
|
|
9607
|
+
return true;
|
|
9608
|
+
} catch {
|
|
9609
|
+
return false;
|
|
9610
|
+
}
|
|
9611
|
+
}
|
|
9612
|
+
function isHeavyGateActive() {
|
|
9613
|
+
for (const key of GATE_KEYS) {
|
|
9614
|
+
try {
|
|
9615
|
+
const raw = readFileSync3(path3.join(runDir(), `${key}.pid`), "utf8").trim();
|
|
9616
|
+
const pid = Number.parseInt(raw, 10);
|
|
9617
|
+
if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
|
|
9618
|
+
} catch {
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
return false;
|
|
9622
|
+
}
|
|
9623
|
+
|
|
9532
9624
|
// src/runner/session-runner.ts
|
|
9533
9625
|
function isPostToChatTool(name) {
|
|
9534
9626
|
return typeof name === "string" && (name === "post_to_chat" || name.endsWith("__post_to_chat"));
|
|
@@ -9566,8 +9658,14 @@ var SessionRunner = class _SessionRunner {
|
|
|
9566
9658
|
agentSpokeThisTurn = false;
|
|
9567
9659
|
/** Guards overlapping runs of the periodic git flush. */
|
|
9568
9660
|
periodicFlushInFlight = false;
|
|
9661
|
+
/** Consecutive flush ticks skipped because a heavy gate was running. */
|
|
9662
|
+
gateSkipCount = 0;
|
|
9663
|
+
/** Max consecutive gate-deferred ticks (~2 min each) before flushing anyway. */
|
|
9664
|
+
static MAX_GATE_SKIPS = 10;
|
|
9569
9665
|
/** Runtime preview-port poller (v3 dynamic port discovery). */
|
|
9570
9666
|
portDiscovery = null;
|
|
9667
|
+
/** Main event-loop lag measurement, shared with the heartbeat worker. */
|
|
9668
|
+
loopLag = new LoopLagMonitor();
|
|
9571
9669
|
constructor(config, callbacks) {
|
|
9572
9670
|
this.config = config;
|
|
9573
9671
|
this.callbacks = callbacks;
|
|
@@ -9576,7 +9674,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
9576
9674
|
this.mode = new ModeController(initialMode, config.runnerMode, config.isAuto);
|
|
9577
9675
|
const lifecycleConfig = { ...DEFAULT_LIFECYCLE_CONFIG, ...config.lifecycle };
|
|
9578
9676
|
this.lifecycle = new Lifecycle(lifecycleConfig, {
|
|
9579
|
-
onHeartbeat: () => this.connection.sendHeartbeat(),
|
|
9677
|
+
onHeartbeat: () => this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs()),
|
|
9580
9678
|
onIdleTimeout: () => {
|
|
9581
9679
|
process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
|
|
9582
9680
|
this.stopped = true;
|
|
@@ -9624,6 +9722,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
9624
9722
|
this.connection.sendEvent({ type: "connected", sessionId: this.sessionId });
|
|
9625
9723
|
this.wireConnectionCallbacks();
|
|
9626
9724
|
this.lifecycle.startHeartbeat();
|
|
9725
|
+
this.loopLag.start();
|
|
9726
|
+
this.connection.startHeartbeatWorker(this.loopLag.sharedBuffer);
|
|
9627
9727
|
this.lifecycle.startTokenRefresh();
|
|
9628
9728
|
this.lifecycle.startUsageSample();
|
|
9629
9729
|
const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
|
|
@@ -9686,10 +9786,10 @@ var SessionRunner = class _SessionRunner {
|
|
|
9686
9786
|
}
|
|
9687
9787
|
if (process.env.CONVEYOR_GIT_READY !== "1") {
|
|
9688
9788
|
if (this.fullContext?.githubBranch) {
|
|
9689
|
-
ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
|
|
9789
|
+
await ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
|
|
9690
9790
|
}
|
|
9691
9791
|
if (this.fullContext?.baseBranch) {
|
|
9692
|
-
syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
|
|
9792
|
+
await syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
|
|
9693
9793
|
}
|
|
9694
9794
|
}
|
|
9695
9795
|
if (!this.stopped) {
|
|
@@ -9709,7 +9809,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
9709
9809
|
);
|
|
9710
9810
|
}
|
|
9711
9811
|
} else {
|
|
9712
|
-
const restored = restoreWipSnapshot(
|
|
9812
|
+
const restored = await restoreWipSnapshot(
|
|
9713
9813
|
this.config.workspaceDir,
|
|
9714
9814
|
this.fullContext.githubBranch
|
|
9715
9815
|
);
|
|
@@ -10052,6 +10152,14 @@ var SessionRunner = class _SessionRunner {
|
|
|
10052
10152
|
* clean tree. Guarded so two ticks can't overlap. Never throws. */
|
|
10053
10153
|
async periodicGitFlush() {
|
|
10054
10154
|
if (this.periodicFlushInFlight || this.stopped) return;
|
|
10155
|
+
if (isHeavyGateActive() && this.gateSkipCount < _SessionRunner.MAX_GATE_SKIPS) {
|
|
10156
|
+
this.gateSkipCount++;
|
|
10157
|
+
if (this.gateSkipCount === 1) {
|
|
10158
|
+
process.stderr.write("[conveyor-agent] Periodic git flush deferred \u2014 heavy gate running\n");
|
|
10159
|
+
}
|
|
10160
|
+
return;
|
|
10161
|
+
}
|
|
10162
|
+
this.gateSkipCount = 0;
|
|
10055
10163
|
this.periodicFlushInFlight = true;
|
|
10056
10164
|
try {
|
|
10057
10165
|
const result = await flushAllPendingWork(this.config.workspaceDir, {
|
|
@@ -10150,6 +10258,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10150
10258
|
void this.queryBridge?.dispose().catch(() => {
|
|
10151
10259
|
});
|
|
10152
10260
|
this.portDiscovery?.stop();
|
|
10261
|
+
this.loopLag.stop();
|
|
10153
10262
|
this.lifecycle.destroy();
|
|
10154
10263
|
this.connection.disconnect();
|
|
10155
10264
|
if (this.inputResolver) {
|
|
@@ -10169,23 +10278,41 @@ var SessionRunner = class _SessionRunner {
|
|
|
10169
10278
|
}
|
|
10170
10279
|
// ── Auto-mode stuck detection & nudge ───────────────────────────────
|
|
10171
10280
|
static MAX_STUCK_NUDGES = 3;
|
|
10172
|
-
/** The
|
|
10173
|
-
*
|
|
10281
|
+
/** The build-phase nudge (In Progress, no PR). Offers BOTH escape routes so
|
|
10282
|
+
* the agent never just goes idle. */
|
|
10174
10283
|
static STUCK_PROMPT = "You are in Auto mode, your task is still In Progress, and there is no open pull request. Do ONE of these right now: (1) open a pull request with the create_pull_request tool, OR (2) call post_to_chat to explain exactly where you are stuck and what you need from a human. Do not finish or go idle silently.";
|
|
10284
|
+
/** The planning-phase nudge (still Planning, identification/plan unfinished).
|
|
10285
|
+
* Same two-escape-route shape, aimed at finishing discovery instead of a PR. */
|
|
10286
|
+
static STUCK_PROMPT_PLANNING = "You are in Auto mode and still in the Planning phase \u2014 identification and/or the plan are not finished. Do ONE of these right now: (1) finish identifying the task (set the story points) and save the plan with the update_task_plan tool, then call ExitPlanMode to begin building, OR (2) call post_to_chat to explain exactly where you are stuck and what you need from a human. Do not finish or go idle silently.";
|
|
10175
10287
|
/**
|
|
10176
|
-
*
|
|
10177
|
-
* post-turn, so the TUI is no longer
|
|
10178
|
-
*
|
|
10179
|
-
*
|
|
10180
|
-
*
|
|
10288
|
+
* Classify how an auto task is "stuck" after its turn ended (call sites run
|
|
10289
|
+
* post-turn, so the TUI is no longer working), or null if it isn't. Shared
|
|
10290
|
+
* guards: only auto, not rate-limited, under the nudge cap, and the agent
|
|
10291
|
+
* stayed silent this turn — if it posted to chat (explained itself / asked for
|
|
10292
|
+
* help) it's waiting on a human, not silently stuck.
|
|
10293
|
+
*
|
|
10294
|
+
* - "pr": In Progress with no open PR (build finished without shipping).
|
|
10295
|
+
* - "planning": still Planning without (identified + saved plan). A fresh auto
|
|
10296
|
+
* agent that finishes its plan turn WITHOUT ExitPlanMode would otherwise idle
|
|
10297
|
+
* → clean-exit → its session Ends → the workspace is reaped "agent_gone"
|
|
10298
|
+
* before a plan ever landed. Nudging (and, on exhaustion, going dormant)
|
|
10299
|
+
* keeps the session Active so the pod is never prematurely slept.
|
|
10181
10300
|
*/
|
|
10301
|
+
autoStuckKind() {
|
|
10302
|
+
if (!this.mode.isAuto || this.stopped) return null;
|
|
10303
|
+
if (this.queryBridge?.wasRateLimited) return null;
|
|
10304
|
+
if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) return null;
|
|
10305
|
+
if (this.agentSpokeThisTurn) return null;
|
|
10306
|
+
if (!this.taskContext) return null;
|
|
10307
|
+
const { status, storyPointId, plan, githubPRUrl } = this.taskContext;
|
|
10308
|
+
if (status === "InProgress" && !githubPRUrl) return "pr";
|
|
10309
|
+
if (status === "Planning" && !(storyPointId !== null && !!plan?.trim())) {
|
|
10310
|
+
return "planning";
|
|
10311
|
+
}
|
|
10312
|
+
return null;
|
|
10313
|
+
}
|
|
10182
10314
|
isAutoStuck() {
|
|
10183
|
-
|
|
10184
|
-
if (this.queryBridge?.wasRateLimited) return false;
|
|
10185
|
-
if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) return false;
|
|
10186
|
-
if (this.agentSpokeThisTurn) return false;
|
|
10187
|
-
if (!this.taskContext) return false;
|
|
10188
|
-
return this.taskContext.status === "InProgress" && !this.taskContext.githubPRUrl;
|
|
10315
|
+
return this.autoStuckKind() !== null;
|
|
10189
10316
|
}
|
|
10190
10317
|
/**
|
|
10191
10318
|
* Stop driving the agent and route the core loop into dormant idle —
|
|
@@ -10221,6 +10348,16 @@ var SessionRunner = class _SessionRunner {
|
|
|
10221
10348
|
} catch {
|
|
10222
10349
|
}
|
|
10223
10350
|
}
|
|
10351
|
+
/** Prompt + chat-message label for a stuck-nudge of the given kind. */
|
|
10352
|
+
stuckNudgeContent(kind) {
|
|
10353
|
+
if (kind === "planning") {
|
|
10354
|
+
return {
|
|
10355
|
+
prompt: _SessionRunner.STUCK_PROMPT_PLANNING,
|
|
10356
|
+
situation: "still Planning with no identified plan"
|
|
10357
|
+
};
|
|
10358
|
+
}
|
|
10359
|
+
return { prompt: _SessionRunner.STUCK_PROMPT, situation: "still no PR" };
|
|
10360
|
+
}
|
|
10224
10361
|
async maybeHandleStuckAuto() {
|
|
10225
10362
|
await this.refreshTaskContext();
|
|
10226
10363
|
if (!this.isAutoStuck()) return;
|
|
@@ -10228,17 +10365,18 @@ var SessionRunner = class _SessionRunner {
|
|
|
10228
10365
|
this.prNudgeCount++;
|
|
10229
10366
|
if (this.prNudgeCount > _SessionRunner.MAX_STUCK_NUDGES) {
|
|
10230
10367
|
this.enterDormantForHumanTakeover(
|
|
10231
|
-
`Auto-mode: I couldn't
|
|
10368
|
+
`Auto-mode: I couldn't finish or get unstuck after ${_SessionRunner.MAX_STUCK_NUDGES} attempts. Staying connected \u2014 reply here or in the terminal and I'll keep working.`
|
|
10232
10369
|
);
|
|
10233
10370
|
return;
|
|
10234
10371
|
}
|
|
10372
|
+
const { prompt, situation } = this.stuckNudgeContent(this.autoStuckKind());
|
|
10235
10373
|
const attempt = this.prNudgeCount;
|
|
10236
|
-
const chatMsg = attempt === 1 ?
|
|
10374
|
+
const chatMsg = attempt === 1 ? `Auto-nudge: ${situation} \u2014 prompting the agent to finish or explain where it's stuck...` : `Auto-nudge (attempt ${attempt}/${_SessionRunner.MAX_STUCK_NUDGES}): ${situation} \u2014 prompting the agent again...`;
|
|
10237
10375
|
this.connection.postChatMessage(chatMsg);
|
|
10238
10376
|
await this.setState("running");
|
|
10239
|
-
await this.callbacks.onEvent({ type: "pr_nudge", prompt
|
|
10377
|
+
await this.callbacks.onEvent({ type: "pr_nudge", prompt });
|
|
10240
10378
|
if (this.interrupted || this.stopped) return;
|
|
10241
|
-
await this.executeQuery(
|
|
10379
|
+
await this.executeQuery(prompt);
|
|
10242
10380
|
if (this.interrupted || this.stopped) return;
|
|
10243
10381
|
await this.refreshTaskContext();
|
|
10244
10382
|
if (this.agentSpokeThisTurn && !this.taskContext?.githubPRUrl) {
|
|
@@ -10296,32 +10434,26 @@ var SessionRunner = class _SessionRunner {
|
|
|
10296
10434
|
mode: this.config.runnerMode,
|
|
10297
10435
|
isAuto: this.config.isAuto
|
|
10298
10436
|
};
|
|
10299
|
-
const bridge = new QueryBridge(
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
onStatusChange: (status) => {
|
|
10305
|
-
if (status === "running" && this._state === "waiting_for_input") {
|
|
10306
|
-
this._state = "running";
|
|
10307
|
-
this.lifecycle.cancelIdleTimer();
|
|
10308
|
-
}
|
|
10309
|
-
return this.callbacks.onStatusChange(status);
|
|
10310
|
-
},
|
|
10311
|
-
onEvent: (event) => {
|
|
10312
|
-
const evt = event;
|
|
10313
|
-
if (evt.type === "tool_use" && isPostToChatTool(evt.tool)) {
|
|
10314
|
-
this.agentSpokeThisTurn = true;
|
|
10315
|
-
}
|
|
10316
|
-
if (event.type === "completed") {
|
|
10317
|
-
this.completedThisTurn = true;
|
|
10318
|
-
void this.connection.sendHeartbeat();
|
|
10319
|
-
}
|
|
10320
|
-
return this.callbacks.onEvent(event);
|
|
10437
|
+
const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
|
|
10438
|
+
onStatusChange: (status) => {
|
|
10439
|
+
if (status === "running" && this._state === "waiting_for_input") {
|
|
10440
|
+
this._state = "running";
|
|
10441
|
+
this.lifecycle.cancelIdleTimer();
|
|
10321
10442
|
}
|
|
10443
|
+
return this.callbacks.onStatusChange(status);
|
|
10322
10444
|
},
|
|
10323
|
-
|
|
10324
|
-
|
|
10445
|
+
onEvent: (event) => {
|
|
10446
|
+
const evt = event;
|
|
10447
|
+
if (evt.type === "tool_use" && isPostToChatTool(evt.tool)) {
|
|
10448
|
+
this.agentSpokeThisTurn = true;
|
|
10449
|
+
}
|
|
10450
|
+
if (event.type === "completed") {
|
|
10451
|
+
this.completedThisTurn = true;
|
|
10452
|
+
void this.connection.sendHeartbeat();
|
|
10453
|
+
}
|
|
10454
|
+
return this.callbacks.onEvent(event);
|
|
10455
|
+
}
|
|
10456
|
+
});
|
|
10325
10457
|
bridge.isParentTask = this.fullContext?.isParentTask ?? false;
|
|
10326
10458
|
bridge.onSoftStop = () => {
|
|
10327
10459
|
process.stderr.write("[conveyor-agent] Soft stop requested (discovery ExitPlanMode)\n");
|
|
@@ -10374,15 +10506,16 @@ var SessionRunner = class _SessionRunner {
|
|
|
10374
10506
|
}
|
|
10375
10507
|
});
|
|
10376
10508
|
this.connection.onPullBranch(({ branch }) => {
|
|
10377
|
-
handlePullBranch(this.config.workspaceDir, branch);
|
|
10509
|
+
void handlePullBranch(this.config.workspaceDir, branch);
|
|
10378
10510
|
});
|
|
10379
10511
|
this.connection.onFinalizeSnapshot(() => void this.finalizeSnapshotNow());
|
|
10380
10512
|
}
|
|
10381
10513
|
/** Eager finalize snapshot triggered by the reconciler's sleep signal
|
|
10382
|
-
* (session:finalizeSnapshot). Runs
|
|
10514
|
+
* (session:finalizeSnapshot). Runs a full workspace capture and uploads it
|
|
10383
10515
|
* NOW so the sleep confirms on this snapshot instead of waiting for the
|
|
10384
|
-
* ~2min periodic flush
|
|
10385
|
-
*
|
|
10516
|
+
* ~2min periodic flush. Sidecar DB state is intentionally NOT captured — it
|
|
10517
|
+
* is no longer durable across sleep/wake (#2623); only the agent's
|
|
10518
|
+
* /workspace rides the snapshot. Best-effort: on failure the
|
|
10386
10519
|
* periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
|
|
10387
10520
|
async finalizeSnapshotNow() {
|
|
10388
10521
|
const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
|
|
@@ -10403,9 +10536,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10403
10536
|
}
|
|
10404
10537
|
}
|
|
10405
10538
|
});
|
|
10406
|
-
process.stderr.write(
|
|
10407
|
-
"[conveyor-agent] Finalize snapshot (pg_dump) uploaded on sleep signal\n"
|
|
10408
|
-
);
|
|
10539
|
+
process.stderr.write("[conveyor-agent] Finalize snapshot uploaded on sleep signal\n");
|
|
10409
10540
|
} catch {
|
|
10410
10541
|
}
|
|
10411
10542
|
}
|
|
@@ -10415,7 +10546,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10415
10546
|
const res = await this.connection.call("refreshGithubToken", {
|
|
10416
10547
|
sessionId: this.connection.sessionId
|
|
10417
10548
|
});
|
|
10418
|
-
updateRemoteToken(this.config.workspaceDir, res.token);
|
|
10549
|
+
await updateRemoteToken(this.config.workspaceDir, res.token);
|
|
10419
10550
|
process.env.GITHUB_TOKEN = res.token;
|
|
10420
10551
|
process.env.GH_TOKEN = res.token;
|
|
10421
10552
|
process.stderr.write("[conveyor-agent] Proactively refreshed GitHub token\n");
|
|
@@ -10434,7 +10565,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10434
10565
|
});
|
|
10435
10566
|
if (ctx?.githubBranch && this.fullContext) {
|
|
10436
10567
|
this.fullContext.githubBranch = ctx.githubBranch;
|
|
10437
|
-
ensureOnTaskBranch(this.config.workspaceDir, ctx.githubBranch);
|
|
10568
|
+
await ensureOnTaskBranch(this.config.workspaceDir, ctx.githubBranch);
|
|
10438
10569
|
}
|
|
10439
10570
|
} catch {
|
|
10440
10571
|
process.stderr.write(
|
|
@@ -10444,6 +10575,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
10444
10575
|
}
|
|
10445
10576
|
async setState(status) {
|
|
10446
10577
|
this._state = status;
|
|
10578
|
+
const loopStatus = status === "idle" ? "idle" : "active";
|
|
10579
|
+
this.loopLag.setStatus(loopStatus);
|
|
10447
10580
|
await this.connection.emitStatus(status);
|
|
10448
10581
|
await this.callbacks.onStatusChange(status);
|
|
10449
10582
|
}
|
|
@@ -10452,6 +10585,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
10452
10585
|
`);
|
|
10453
10586
|
this.connection.sendEvent({ type: "shutdown", reason: finalState });
|
|
10454
10587
|
this.portDiscovery?.stop();
|
|
10588
|
+
this.loopLag.stop();
|
|
10455
10589
|
this.lifecycle.destroy();
|
|
10456
10590
|
try {
|
|
10457
10591
|
await this.queryBridge?.dispose();
|
|
@@ -10501,12 +10635,12 @@ var SessionRunner = class _SessionRunner {
|
|
|
10501
10635
|
|
|
10502
10636
|
// src/setup/config.ts
|
|
10503
10637
|
import { readFile as readFile5 } from "fs/promises";
|
|
10504
|
-
import { join as
|
|
10638
|
+
import { join as join7 } from "path";
|
|
10505
10639
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
10506
10640
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
10507
10641
|
async function loadForwardPorts(workspaceDir) {
|
|
10508
10642
|
try {
|
|
10509
|
-
const raw = await readFile5(
|
|
10643
|
+
const raw = await readFile5(join7(workspaceDir, DEVCONTAINER_PATH), "utf-8");
|
|
10510
10644
|
const parsed = JSON.parse(raw);
|
|
10511
10645
|
const ports = (parsed.forwardPorts ?? []).filter(
|
|
10512
10646
|
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
|
@@ -10538,19 +10672,17 @@ function buildSessionPreviewPorts(result) {
|
|
|
10538
10672
|
function loadConveyorConfig() {
|
|
10539
10673
|
const envSetup = process.env.CONVEYOR_SETUP_COMMAND;
|
|
10540
10674
|
const envStart = process.env.CONVEYOR_START_COMMAND;
|
|
10541
|
-
const envPort = process.env.CONVEYOR_PREVIEW_PORT;
|
|
10542
10675
|
if (envSetup || envStart) {
|
|
10543
10676
|
return {
|
|
10544
10677
|
setupCommand: envSetup,
|
|
10545
|
-
startCommand: envStart
|
|
10546
|
-
previewPort: envPort ? Number(envPort) : void 0
|
|
10678
|
+
startCommand: envStart
|
|
10547
10679
|
};
|
|
10548
10680
|
}
|
|
10549
10681
|
return null;
|
|
10550
10682
|
}
|
|
10551
10683
|
|
|
10552
10684
|
// src/setup/commands.ts
|
|
10553
|
-
import { spawn, execSync
|
|
10685
|
+
import { spawn, execSync } from "child_process";
|
|
10554
10686
|
function runSetupCommand(cmd, cwd, onOutput) {
|
|
10555
10687
|
return new Promise((resolve, reject) => {
|
|
10556
10688
|
const child = spawn("sh", ["-c", cmd], {
|
|
@@ -10579,7 +10711,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
|
|
|
10579
10711
|
var AUTH_TOKEN_TIMEOUT_MS = 3e4;
|
|
10580
10712
|
function runAuthTokenCommand(cmd, userEmail, cwd) {
|
|
10581
10713
|
try {
|
|
10582
|
-
const output =
|
|
10714
|
+
const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {
|
|
10583
10715
|
cwd,
|
|
10584
10716
|
timeout: AUTH_TOKEN_TIMEOUT_MS,
|
|
10585
10717
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -10609,10 +10741,10 @@ function runStartCommand(cmd, cwd, onOutput) {
|
|
|
10609
10741
|
}
|
|
10610
10742
|
|
|
10611
10743
|
// src/setup/codespace.ts
|
|
10612
|
-
import { execSync as
|
|
10744
|
+
import { execSync as execSync2 } from "child_process";
|
|
10613
10745
|
function unshallowRepo(workspaceDir) {
|
|
10614
10746
|
try {
|
|
10615
|
-
|
|
10747
|
+
execSync2("git fetch --unshallow", {
|
|
10616
10748
|
cwd: workspaceDir,
|
|
10617
10749
|
timeout: 6e4,
|
|
10618
10750
|
stdio: "ignore"
|
|
@@ -10622,10 +10754,10 @@ function unshallowRepo(workspaceDir) {
|
|
|
10622
10754
|
}
|
|
10623
10755
|
|
|
10624
10756
|
export {
|
|
10625
|
-
DEFAULT_SONNET_MODEL,
|
|
10626
10757
|
fetchBootstrap,
|
|
10627
10758
|
applyBootstrapToEnv,
|
|
10628
10759
|
AgentConnection,
|
|
10760
|
+
DEFAULT_SONNET_MODEL,
|
|
10629
10761
|
DEFAULT_LIFECYCLE_CONFIG,
|
|
10630
10762
|
Lifecycle,
|
|
10631
10763
|
PtyHarness,
|
|
@@ -10637,7 +10769,7 @@ export {
|
|
|
10637
10769
|
updateRemoteToken,
|
|
10638
10770
|
flushPendingChanges,
|
|
10639
10771
|
pushToOrigin,
|
|
10640
|
-
|
|
10772
|
+
resolveSessionStart,
|
|
10641
10773
|
sampleKeyUsage,
|
|
10642
10774
|
awaitGitReady,
|
|
10643
10775
|
uploadSnapshotToGcs,
|
|
@@ -10655,4 +10787,4 @@ export {
|
|
|
10655
10787
|
runStartCommand,
|
|
10656
10788
|
unshallowRepo
|
|
10657
10789
|
};
|
|
10658
|
-
//# sourceMappingURL=chunk-
|
|
10790
|
+
//# sourceMappingURL=chunk-DEMRCBJN.js.map
|