@rallycry/conveyor-agent 2.17.1 → 3.0.0
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-3BXPSYXX.js → chunk-5UYKPYFQ.js} +868 -575
- package/dist/chunk-5UYKPYFQ.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +55 -302
- package/dist/index.js +2 -65
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-3BXPSYXX.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/connection.ts
|
|
1
|
+
// src/connection/task-connection.ts
|
|
2
2
|
import { io } from "socket.io-client";
|
|
3
3
|
var ConveyorConnection = class _ConveyorConnection {
|
|
4
4
|
socket = null;
|
|
@@ -8,8 +8,10 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
8
8
|
static EVENT_BATCH_MS = 500;
|
|
9
9
|
earlyMessages = [];
|
|
10
10
|
earlyStop = false;
|
|
11
|
+
earlyModeChanges = [];
|
|
11
12
|
chatMessageCallback = null;
|
|
12
13
|
stopCallback = null;
|
|
14
|
+
modeChangeCallback = null;
|
|
13
15
|
pendingQuestionResolvers = /* @__PURE__ */ new Map();
|
|
14
16
|
constructor(config) {
|
|
15
17
|
this.config = config;
|
|
@@ -31,16 +33,13 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
31
33
|
"ngrok-skip-browser-warning": "true"
|
|
32
34
|
}
|
|
33
35
|
});
|
|
34
|
-
this.socket.on(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} else {
|
|
40
|
-
this.earlyMessages.push(msg);
|
|
41
|
-
}
|
|
36
|
+
this.socket.on("agentRunner:incomingMessage", (msg) => {
|
|
37
|
+
if (this.chatMessageCallback) {
|
|
38
|
+
this.chatMessageCallback(msg);
|
|
39
|
+
} else {
|
|
40
|
+
this.earlyMessages.push(msg);
|
|
42
41
|
}
|
|
43
|
-
);
|
|
42
|
+
});
|
|
44
43
|
this.socket.on("agentRunner:stop", () => {
|
|
45
44
|
if (this.stopCallback) {
|
|
46
45
|
this.stopCallback();
|
|
@@ -48,16 +47,20 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
48
47
|
this.earlyStop = true;
|
|
49
48
|
}
|
|
50
49
|
});
|
|
51
|
-
this.socket.on(
|
|
52
|
-
|
|
53
|
-
(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.pendingQuestionResolvers.delete(data.requestId);
|
|
57
|
-
resolver(data.answers);
|
|
58
|
-
}
|
|
50
|
+
this.socket.on("agentRunner:questionAnswer", (data) => {
|
|
51
|
+
const resolver = this.pendingQuestionResolvers.get(data.requestId);
|
|
52
|
+
if (resolver) {
|
|
53
|
+
this.pendingQuestionResolvers.delete(data.requestId);
|
|
54
|
+
resolver(data.answers);
|
|
59
55
|
}
|
|
60
|
-
);
|
|
56
|
+
});
|
|
57
|
+
this.socket.on("agentRunner:setMode", (data) => {
|
|
58
|
+
if (this.modeChangeCallback) {
|
|
59
|
+
this.modeChangeCallback(data);
|
|
60
|
+
} else {
|
|
61
|
+
this.earlyModeChanges.push(data);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
61
64
|
this.socket.on("connect", () => {
|
|
62
65
|
if (!settled) {
|
|
63
66
|
settled = true;
|
|
@@ -79,7 +82,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
79
82
|
return new Promise((resolve2, reject) => {
|
|
80
83
|
socket.emit(
|
|
81
84
|
"agentRunner:getChatMessages",
|
|
82
|
-
{
|
|
85
|
+
{ limit },
|
|
83
86
|
(response) => {
|
|
84
87
|
if (response.success && response.data) {
|
|
85
88
|
resolve2(response.data);
|
|
@@ -96,7 +99,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
96
99
|
return new Promise((resolve2, reject) => {
|
|
97
100
|
socket.emit(
|
|
98
101
|
"agentRunner:getTaskFiles",
|
|
99
|
-
{
|
|
102
|
+
{},
|
|
100
103
|
(response) => {
|
|
101
104
|
if (response.success && response.data) {
|
|
102
105
|
resolve2(response.data);
|
|
@@ -113,7 +116,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
113
116
|
return new Promise((resolve2, reject) => {
|
|
114
117
|
socket.emit(
|
|
115
118
|
"agentRunner:getTaskFile",
|
|
116
|
-
{
|
|
119
|
+
{ fileId },
|
|
117
120
|
(response) => {
|
|
118
121
|
if (response.success && response.data) {
|
|
119
122
|
resolve2(response.data);
|
|
@@ -130,7 +133,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
130
133
|
return new Promise((resolve2, reject) => {
|
|
131
134
|
socket.emit(
|
|
132
135
|
"agentRunner:getTaskContext",
|
|
133
|
-
{
|
|
136
|
+
{},
|
|
134
137
|
(response) => {
|
|
135
138
|
if (response.success && response.data) {
|
|
136
139
|
resolve2(response.data);
|
|
@@ -143,7 +146,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
143
146
|
}
|
|
144
147
|
sendEvent(event) {
|
|
145
148
|
if (!this.socket) throw new Error("Not connected");
|
|
146
|
-
this.eventBuffer.push({
|
|
149
|
+
this.eventBuffer.push({ event });
|
|
147
150
|
if (!this.flushTimer) {
|
|
148
151
|
this.flushTimer = setTimeout(() => this.flushEvents(), _ConveyorConnection.EVENT_BATCH_MS);
|
|
149
152
|
}
|
|
@@ -161,17 +164,11 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
161
164
|
}
|
|
162
165
|
updateStatus(status) {
|
|
163
166
|
if (!this.socket) throw new Error("Not connected");
|
|
164
|
-
this.socket.emit("agentRunner:statusUpdate", {
|
|
165
|
-
taskId: this.config.taskId,
|
|
166
|
-
status
|
|
167
|
-
});
|
|
167
|
+
this.socket.emit("agentRunner:statusUpdate", { status });
|
|
168
168
|
}
|
|
169
169
|
postChatMessage(content) {
|
|
170
170
|
if (!this.socket) throw new Error("Not connected");
|
|
171
|
-
this.socket.emit("agentRunner:chatMessage", {
|
|
172
|
-
taskId: this.config.taskId,
|
|
173
|
-
content
|
|
174
|
-
});
|
|
171
|
+
this.socket.emit("agentRunner:chatMessage", { content });
|
|
175
172
|
}
|
|
176
173
|
createPR(params) {
|
|
177
174
|
const socket = this.socket;
|
|
@@ -179,7 +176,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
179
176
|
return new Promise((resolve2, reject) => {
|
|
180
177
|
socket.emit(
|
|
181
178
|
"agentRunner:createPR",
|
|
182
|
-
|
|
179
|
+
params,
|
|
183
180
|
(response) => {
|
|
184
181
|
if (response.success && response.data) {
|
|
185
182
|
resolve2(response.data);
|
|
@@ -192,11 +189,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
192
189
|
}
|
|
193
190
|
askUserQuestion(requestId, questions) {
|
|
194
191
|
if (!this.socket) throw new Error("Not connected");
|
|
195
|
-
this.socket.emit("agentRunner:askUserQuestion", {
|
|
196
|
-
taskId: this.config.taskId,
|
|
197
|
-
requestId,
|
|
198
|
-
questions
|
|
199
|
-
});
|
|
192
|
+
this.socket.emit("agentRunner:askUserQuestion", { requestId, questions });
|
|
200
193
|
return new Promise((resolve2) => {
|
|
201
194
|
this.pendingQuestionResolvers.set(requestId, resolve2);
|
|
202
195
|
});
|
|
@@ -206,17 +199,11 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
206
199
|
}
|
|
207
200
|
storeSessionId(sessionId) {
|
|
208
201
|
if (!this.socket) return;
|
|
209
|
-
this.socket.emit("agentRunner:storeSessionId", {
|
|
210
|
-
taskId: this.config.taskId,
|
|
211
|
-
sessionId
|
|
212
|
-
});
|
|
202
|
+
this.socket.emit("agentRunner:storeSessionId", { sessionId });
|
|
213
203
|
}
|
|
214
204
|
updateTaskFields(fields) {
|
|
215
205
|
if (!this.socket) throw new Error("Not connected");
|
|
216
|
-
this.socket.emit("agentRunner:updateTaskFields", {
|
|
217
|
-
taskId: this.config.taskId,
|
|
218
|
-
fields
|
|
219
|
-
});
|
|
206
|
+
this.socket.emit("agentRunner:updateTaskFields", { fields });
|
|
220
207
|
}
|
|
221
208
|
onChatMessage(callback) {
|
|
222
209
|
this.chatMessageCallback = callback;
|
|
@@ -232,25 +219,28 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
232
219
|
this.earlyStop = false;
|
|
233
220
|
}
|
|
234
221
|
}
|
|
222
|
+
onModeChange(callback) {
|
|
223
|
+
this.modeChangeCallback = callback;
|
|
224
|
+
for (const data of this.earlyModeChanges) {
|
|
225
|
+
callback(data);
|
|
226
|
+
}
|
|
227
|
+
this.earlyModeChanges = [];
|
|
228
|
+
}
|
|
229
|
+
emitModeChanged(mode) {
|
|
230
|
+
if (!this.socket) return;
|
|
231
|
+
this.socket.emit("agentRunner:modeChanged", { mode });
|
|
232
|
+
}
|
|
235
233
|
trackSpending(params) {
|
|
236
234
|
if (!this.socket) throw new Error("Not connected");
|
|
237
|
-
this.socket.emit("agentRunner:trackSpending",
|
|
238
|
-
taskId: this.config.taskId,
|
|
239
|
-
...params
|
|
240
|
-
});
|
|
235
|
+
this.socket.emit("agentRunner:trackSpending", params);
|
|
241
236
|
}
|
|
242
237
|
emitStatus(status) {
|
|
243
238
|
if (!this.socket) return;
|
|
244
|
-
this.socket.emit("agentRunner:statusUpdate", {
|
|
245
|
-
taskId: this.config.taskId,
|
|
246
|
-
status
|
|
247
|
-
});
|
|
239
|
+
this.socket.emit("agentRunner:statusUpdate", { status });
|
|
248
240
|
}
|
|
249
241
|
sendHeartbeat() {
|
|
250
242
|
if (!this.socket) return;
|
|
251
|
-
this.socket.emit("agentRunner:heartbeat", {
|
|
252
|
-
taskId: this.config.taskId
|
|
253
|
-
});
|
|
243
|
+
this.socket.emit("agentRunner:heartbeat", {});
|
|
254
244
|
}
|
|
255
245
|
sendTypingStart() {
|
|
256
246
|
this.sendEvent({ type: "agent_typing_start" });
|
|
@@ -262,22 +252,15 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
262
252
|
const socket = this.socket;
|
|
263
253
|
if (!socket) throw new Error("Not connected");
|
|
264
254
|
return new Promise((resolve2, reject) => {
|
|
265
|
-
socket.emit(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (response.success && response.data) resolve2(response.data);
|
|
270
|
-
else reject(new Error(response.error ?? "Failed to create subtask"));
|
|
271
|
-
}
|
|
272
|
-
);
|
|
255
|
+
socket.emit("agentRunner:createSubtask", data, (response) => {
|
|
256
|
+
if (response.success && response.data) resolve2(response.data);
|
|
257
|
+
else reject(new Error(response.error ?? "Failed to create subtask"));
|
|
258
|
+
});
|
|
273
259
|
});
|
|
274
260
|
}
|
|
275
261
|
updateSubtask(subtaskId, fields) {
|
|
276
262
|
if (!this.socket) throw new Error("Not connected");
|
|
277
|
-
this.socket.emit("agentRunner:updateSubtask", {
|
|
278
|
-
subtaskId,
|
|
279
|
-
fields
|
|
280
|
-
});
|
|
263
|
+
this.socket.emit("agentRunner:updateSubtask", { subtaskId, fields });
|
|
281
264
|
}
|
|
282
265
|
deleteSubtask(subtaskId) {
|
|
283
266
|
if (!this.socket) throw new Error("Not connected");
|
|
@@ -287,14 +270,10 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
287
270
|
const socket = this.socket;
|
|
288
271
|
if (!socket) throw new Error("Not connected");
|
|
289
272
|
return new Promise((resolve2, reject) => {
|
|
290
|
-
socket.emit(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (response.success && response.data) resolve2(response.data);
|
|
295
|
-
else reject(new Error(response.error ?? "Failed to list subtasks"));
|
|
296
|
-
}
|
|
297
|
-
);
|
|
273
|
+
socket.emit("agentRunner:listSubtasks", {}, (response) => {
|
|
274
|
+
if (response.success && response.data) resolve2(response.data);
|
|
275
|
+
else reject(new Error(response.error ?? "Failed to list subtasks"));
|
|
276
|
+
});
|
|
298
277
|
});
|
|
299
278
|
}
|
|
300
279
|
fetchTask(slugOrId) {
|
|
@@ -303,7 +282,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
303
282
|
return new Promise((resolve2, reject) => {
|
|
304
283
|
socket.emit(
|
|
305
284
|
"agentRunner:getTask",
|
|
306
|
-
{
|
|
285
|
+
{ slugOrId },
|
|
307
286
|
(response) => {
|
|
308
287
|
if (response.success && response.data) {
|
|
309
288
|
resolve2(response.data);
|
|
@@ -321,9 +300,157 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
321
300
|
}
|
|
322
301
|
};
|
|
323
302
|
|
|
324
|
-
// src/
|
|
325
|
-
import {
|
|
326
|
-
|
|
303
|
+
// src/connection/project-connection.ts
|
|
304
|
+
import { io as io2 } from "socket.io-client";
|
|
305
|
+
var ProjectConnection = class {
|
|
306
|
+
socket = null;
|
|
307
|
+
config;
|
|
308
|
+
taskAssignmentCallback = null;
|
|
309
|
+
stopTaskCallback = null;
|
|
310
|
+
shutdownCallback = null;
|
|
311
|
+
chatMessageCallback = null;
|
|
312
|
+
earlyChatMessages = [];
|
|
313
|
+
constructor(config) {
|
|
314
|
+
this.config = config;
|
|
315
|
+
}
|
|
316
|
+
connect() {
|
|
317
|
+
return new Promise((resolve2, reject) => {
|
|
318
|
+
let settled = false;
|
|
319
|
+
let attempts = 0;
|
|
320
|
+
const maxInitialAttempts = 30;
|
|
321
|
+
this.socket = io2(this.config.apiUrl, {
|
|
322
|
+
auth: { projectToken: this.config.projectToken },
|
|
323
|
+
transports: ["websocket"],
|
|
324
|
+
reconnection: true,
|
|
325
|
+
reconnectionAttempts: Infinity,
|
|
326
|
+
reconnectionDelay: 2e3,
|
|
327
|
+
reconnectionDelayMax: 3e4,
|
|
328
|
+
randomizationFactor: 0.3,
|
|
329
|
+
extraHeaders: {
|
|
330
|
+
"ngrok-skip-browser-warning": "true"
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
this.socket.on("projectRunner:assignTask", (data) => {
|
|
334
|
+
if (this.taskAssignmentCallback) {
|
|
335
|
+
this.taskAssignmentCallback(data);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
this.socket.on("projectRunner:stopTask", (data) => {
|
|
339
|
+
if (this.stopTaskCallback) {
|
|
340
|
+
this.stopTaskCallback(data);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
this.socket.on("projectRunner:shutdown", () => {
|
|
344
|
+
if (this.shutdownCallback) {
|
|
345
|
+
this.shutdownCallback();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
this.socket.on("projectRunner:incomingChatMessage", (msg) => {
|
|
349
|
+
if (this.chatMessageCallback) {
|
|
350
|
+
this.chatMessageCallback(msg);
|
|
351
|
+
} else {
|
|
352
|
+
this.earlyChatMessages.push(msg);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
this.socket.on("connect", () => {
|
|
356
|
+
if (!settled) {
|
|
357
|
+
settled = true;
|
|
358
|
+
resolve2();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
this.socket.io.on("reconnect_attempt", () => {
|
|
362
|
+
attempts++;
|
|
363
|
+
if (!settled && attempts >= maxInitialAttempts) {
|
|
364
|
+
settled = true;
|
|
365
|
+
reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
onTaskAssignment(callback) {
|
|
371
|
+
this.taskAssignmentCallback = callback;
|
|
372
|
+
}
|
|
373
|
+
onStopTask(callback) {
|
|
374
|
+
this.stopTaskCallback = callback;
|
|
375
|
+
}
|
|
376
|
+
onShutdown(callback) {
|
|
377
|
+
this.shutdownCallback = callback;
|
|
378
|
+
}
|
|
379
|
+
onChatMessage(callback) {
|
|
380
|
+
this.chatMessageCallback = callback;
|
|
381
|
+
for (const msg of this.earlyChatMessages) {
|
|
382
|
+
callback(msg);
|
|
383
|
+
}
|
|
384
|
+
this.earlyChatMessages = [];
|
|
385
|
+
}
|
|
386
|
+
sendHeartbeat() {
|
|
387
|
+
if (!this.socket) return;
|
|
388
|
+
this.socket.emit("projectRunner:heartbeat", {});
|
|
389
|
+
}
|
|
390
|
+
emitTaskStarted(taskId) {
|
|
391
|
+
if (!this.socket) return;
|
|
392
|
+
this.socket.emit("projectRunner:taskStarted", { taskId });
|
|
393
|
+
}
|
|
394
|
+
emitTaskStopped(taskId, reason) {
|
|
395
|
+
if (!this.socket) return;
|
|
396
|
+
this.socket.emit("projectRunner:taskStopped", { taskId, reason });
|
|
397
|
+
}
|
|
398
|
+
emitEvent(event) {
|
|
399
|
+
if (!this.socket) return;
|
|
400
|
+
this.socket.emit("conveyor:projectAgentEvent", event);
|
|
401
|
+
}
|
|
402
|
+
emitChatMessage(content) {
|
|
403
|
+
const socket = this.socket;
|
|
404
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
405
|
+
return new Promise((resolve2, reject) => {
|
|
406
|
+
socket.emit(
|
|
407
|
+
"conveyor:projectAgentChatMessage",
|
|
408
|
+
{ content },
|
|
409
|
+
(response) => {
|
|
410
|
+
if (response.success) resolve2();
|
|
411
|
+
else reject(new Error(response.error ?? "Failed to send chat message"));
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
emitAgentStatus(status) {
|
|
417
|
+
if (!this.socket) return;
|
|
418
|
+
this.socket.emit("conveyor:projectAgentStatus", { status });
|
|
419
|
+
}
|
|
420
|
+
fetchAgentContext() {
|
|
421
|
+
const socket = this.socket;
|
|
422
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
423
|
+
return new Promise((resolve2, reject) => {
|
|
424
|
+
socket.emit(
|
|
425
|
+
"projectRunner:getAgentContext",
|
|
426
|
+
(response) => {
|
|
427
|
+
if (response.success) resolve2(response.data ?? null);
|
|
428
|
+
else reject(new Error(response.error ?? "Failed to fetch agent context"));
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
fetchChatHistory(limit) {
|
|
434
|
+
const socket = this.socket;
|
|
435
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
436
|
+
return new Promise((resolve2, reject) => {
|
|
437
|
+
socket.emit(
|
|
438
|
+
"projectRunner:getChatHistory",
|
|
439
|
+
{ limit },
|
|
440
|
+
(response) => {
|
|
441
|
+
if (response.success && response.data) resolve2(response.data);
|
|
442
|
+
else reject(new Error(response.error ?? "Failed to fetch chat history"));
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
disconnect() {
|
|
448
|
+
this.socket?.disconnect();
|
|
449
|
+
this.socket = null;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/setup/config.ts
|
|
327
454
|
import { readFile } from "fs/promises";
|
|
328
455
|
import { join } from "path";
|
|
329
456
|
var CONVEYOR_CONFIG_PATH = ".conveyor/config.json";
|
|
@@ -354,6 +481,9 @@ async function loadConveyorConfig(workspaceDir) {
|
|
|
354
481
|
}
|
|
355
482
|
return null;
|
|
356
483
|
}
|
|
484
|
+
|
|
485
|
+
// src/setup/commands.ts
|
|
486
|
+
import { spawn } from "child_process";
|
|
357
487
|
function runSetupCommand(cmd, cwd, onOutput) {
|
|
358
488
|
return new Promise((resolve2, reject) => {
|
|
359
489
|
const child = spawn("sh", ["-c", cmd], {
|
|
@@ -395,6 +525,10 @@ function runStartCommand(cmd, cwd, onOutput) {
|
|
|
395
525
|
child.unref();
|
|
396
526
|
return child;
|
|
397
527
|
}
|
|
528
|
+
|
|
529
|
+
// src/setup/codespace.ts
|
|
530
|
+
import { execSync } from "child_process";
|
|
531
|
+
var DEVCONTAINER_PATH2 = ".devcontainer/conveyor/devcontainer.json";
|
|
398
532
|
function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
399
533
|
const git = (cmd) => execSync(cmd, { cwd: workspaceDir, encoding: "utf-8", timeout: 3e4 }).trim();
|
|
400
534
|
try {
|
|
@@ -403,7 +537,7 @@ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
|
403
537
|
return { cleaned: false, message: `Failed to fetch origin/${baseBranch}` };
|
|
404
538
|
}
|
|
405
539
|
try {
|
|
406
|
-
git(`git diff --quiet origin/${baseBranch} -- ${
|
|
540
|
+
git(`git diff --quiet origin/${baseBranch} -- ${DEVCONTAINER_PATH2}`);
|
|
407
541
|
return { cleaned: false, message: "devcontainer.json already matches base" };
|
|
408
542
|
} catch {
|
|
409
543
|
}
|
|
@@ -412,73 +546,253 @@ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
|
412
546
|
if (ahead <= 1) {
|
|
413
547
|
git(`git reset --hard origin/${baseBranch}`);
|
|
414
548
|
} else {
|
|
415
|
-
git(`git checkout origin/${baseBranch} -- ${
|
|
416
|
-
git(`git add ${
|
|
549
|
+
git(`git checkout origin/${baseBranch} -- ${DEVCONTAINER_PATH2}`);
|
|
550
|
+
git(`git add ${DEVCONTAINER_PATH2}`);
|
|
417
551
|
try {
|
|
418
|
-
git(`git diff --cached --quiet -- ${
|
|
552
|
+
git(`git diff --cached --quiet -- ${DEVCONTAINER_PATH2}`);
|
|
419
553
|
return { cleaned: false, message: "devcontainer.json already clean in working tree" };
|
|
420
554
|
} catch {
|
|
421
555
|
git(`git commit -m "chore: reset devcontainer config"`);
|
|
422
556
|
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
git(`git push --force-with-lease origin ${taskBranch}`);
|
|
560
|
+
} catch {
|
|
561
|
+
git(`git push --force origin ${taskBranch}`);
|
|
562
|
+
}
|
|
563
|
+
return { cleaned: true, message: "devcontainer.json cleaned from git history" };
|
|
564
|
+
} catch (err) {
|
|
565
|
+
try {
|
|
566
|
+
git(`git checkout ${taskBranch}`);
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
570
|
+
return { cleaned: false, message: `Git cleanup failed: ${msg}` };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function initRtk() {
|
|
574
|
+
try {
|
|
575
|
+
execSync("rtk --version", { stdio: "ignore" });
|
|
576
|
+
execSync("rtk init --global --auto-patch", { stdio: "ignore" });
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function unshallowRepo(workspaceDir) {
|
|
581
|
+
try {
|
|
582
|
+
execSync("git fetch --unshallow", {
|
|
583
|
+
cwd: workspaceDir,
|
|
584
|
+
stdio: "ignore",
|
|
585
|
+
timeout: 6e4
|
|
586
|
+
});
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/runner/worktree.ts
|
|
592
|
+
import { execSync as execSync2 } from "child_process";
|
|
593
|
+
import { existsSync } from "fs";
|
|
594
|
+
import { join as join2 } from "path";
|
|
595
|
+
var WORKTREE_DIR = ".worktrees";
|
|
596
|
+
function ensureWorktree(projectDir, taskId, branch) {
|
|
597
|
+
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
598
|
+
if (existsSync(worktreePath)) {
|
|
599
|
+
if (branch) {
|
|
600
|
+
try {
|
|
601
|
+
execSync2(`git checkout --detach origin/${branch}`, {
|
|
602
|
+
cwd: worktreePath,
|
|
603
|
+
stdio: "ignore"
|
|
604
|
+
});
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return worktreePath;
|
|
609
|
+
}
|
|
610
|
+
const ref = branch ? `origin/${branch}` : "HEAD";
|
|
611
|
+
execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
|
|
612
|
+
cwd: projectDir,
|
|
613
|
+
stdio: "ignore"
|
|
614
|
+
});
|
|
615
|
+
return worktreePath;
|
|
616
|
+
}
|
|
617
|
+
function removeWorktree(projectDir, taskId) {
|
|
618
|
+
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
619
|
+
if (!existsSync(worktreePath)) return;
|
|
620
|
+
try {
|
|
621
|
+
execSync2(`git worktree remove "${worktreePath}" --force`, {
|
|
622
|
+
cwd: projectDir,
|
|
623
|
+
stdio: "ignore"
|
|
624
|
+
});
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/runner/agent-runner.ts
|
|
630
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
631
|
+
import { execSync as execSync3 } from "child_process";
|
|
632
|
+
|
|
633
|
+
// src/execution/event-processor.ts
|
|
634
|
+
async function processAssistantEvent(event, host, turnToolCalls) {
|
|
635
|
+
const { content } = event.message;
|
|
636
|
+
const turnTextParts = [];
|
|
637
|
+
for (const block of content) {
|
|
638
|
+
if (block.type === "text") {
|
|
639
|
+
turnTextParts.push(block.text);
|
|
640
|
+
host.connection.sendEvent({ type: "message", content: block.text });
|
|
641
|
+
await host.callbacks.onEvent({ type: "message", content: block.text });
|
|
642
|
+
} else if (block.type === "tool_use") {
|
|
643
|
+
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
644
|
+
const isContentTool = ["edit", "write"].includes(block.name.toLowerCase());
|
|
645
|
+
const inputLimit = isContentTool ? 1e4 : 500;
|
|
646
|
+
const summary = {
|
|
647
|
+
tool: block.name,
|
|
648
|
+
input: inputStr.slice(0, inputLimit),
|
|
649
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
650
|
+
};
|
|
651
|
+
turnToolCalls.push(summary);
|
|
652
|
+
host.connection.sendEvent({ type: "tool_use", tool: block.name, input: inputStr });
|
|
653
|
+
await host.callbacks.onEvent({ type: "tool_use", tool: block.name, input: inputStr });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (turnTextParts.length > 0) {
|
|
657
|
+
host.connection.postChatMessage(turnTextParts.join("\n\n"));
|
|
658
|
+
}
|
|
659
|
+
if (turnToolCalls.length > 0) {
|
|
660
|
+
host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
|
|
661
|
+
turnToolCalls.length = 0;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
var API_ERROR_PATTERN = /API Error: [45]\d\d/;
|
|
665
|
+
function handleResultEvent(event, host, context, startTime) {
|
|
666
|
+
let totalCostUsd = 0;
|
|
667
|
+
let retriable = false;
|
|
668
|
+
if (event.subtype === "success") {
|
|
669
|
+
const successEvent = event;
|
|
670
|
+
const queryCostUsd = successEvent.total_cost_usd;
|
|
671
|
+
const durationMs = Date.now() - startTime;
|
|
672
|
+
const summary = successEvent.result || "Task completed.";
|
|
673
|
+
if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
|
|
674
|
+
retriable = true;
|
|
675
|
+
}
|
|
676
|
+
const cumulativeTotal = host.costTracker.addQueryCost(queryCostUsd);
|
|
677
|
+
totalCostUsd = cumulativeTotal;
|
|
678
|
+
const { modelUsage } = successEvent;
|
|
679
|
+
if (modelUsage && typeof modelUsage === "object") {
|
|
680
|
+
host.costTracker.addModelUsage(modelUsage);
|
|
681
|
+
}
|
|
682
|
+
host.connection.sendEvent({ type: "completed", summary, costUsd: cumulativeTotal, durationMs });
|
|
683
|
+
if (cumulativeTotal > 0 && context.agentId && context._runnerSessionId) {
|
|
684
|
+
const breakdown = host.costTracker.modelBreakdown;
|
|
685
|
+
host.connection.trackSpending({
|
|
686
|
+
agentId: context.agentId,
|
|
687
|
+
sessionId: context._runnerSessionId,
|
|
688
|
+
totalCostUsd: cumulativeTotal,
|
|
689
|
+
onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
690
|
+
modelUsage: breakdown.length > 0 ? breakdown : void 0
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
const errorEvent = event;
|
|
695
|
+
const errorMsg = errorEvent.errors.length > 0 ? errorEvent.errors.join(", ") : `Agent stopped: ${errorEvent.subtype}`;
|
|
696
|
+
if (API_ERROR_PATTERN.test(errorMsg)) {
|
|
697
|
+
retriable = true;
|
|
698
|
+
}
|
|
699
|
+
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
700
|
+
}
|
|
701
|
+
return { totalCostUsd, retriable };
|
|
702
|
+
}
|
|
703
|
+
async function emitResultEvent(event, host, context, startTime) {
|
|
704
|
+
const result = handleResultEvent(event, host, context, startTime);
|
|
705
|
+
const durationMs = Date.now() - startTime;
|
|
706
|
+
if (event.subtype === "success") {
|
|
707
|
+
const successEvent = event;
|
|
708
|
+
const summary = successEvent.result || "Task completed.";
|
|
709
|
+
await host.callbacks.onEvent({
|
|
710
|
+
type: "completed",
|
|
711
|
+
summary,
|
|
712
|
+
costUsd: result.totalCostUsd,
|
|
713
|
+
durationMs
|
|
714
|
+
});
|
|
715
|
+
} else {
|
|
716
|
+
const errorEvent = event;
|
|
717
|
+
const errorMsg = errorEvent.errors.length > 0 ? errorEvent.errors.join(", ") : `Agent stopped: ${errorEvent.subtype}`;
|
|
718
|
+
await host.callbacks.onEvent({ type: "error", message: errorMsg });
|
|
719
|
+
}
|
|
720
|
+
return result.retriable;
|
|
721
|
+
}
|
|
722
|
+
function handleRateLimitEvent(event, host) {
|
|
723
|
+
const { rate_limit_info } = event;
|
|
724
|
+
const status = rate_limit_info.status;
|
|
725
|
+
if (status === "rejected") {
|
|
726
|
+
const resetsAt = rate_limit_info.resetsAt ? new Date(rate_limit_info.resetsAt).toISOString() : "unknown";
|
|
727
|
+
const message = `Rate limit rejected (type: ${rate_limit_info.rateLimitType ?? "unknown"}, resets at: ${resetsAt})`;
|
|
728
|
+
host.connection.sendEvent({ type: "error", message });
|
|
729
|
+
void host.callbacks.onEvent({ type: "error", message });
|
|
730
|
+
} else if (status === "allowed_warning") {
|
|
731
|
+
const utilization = rate_limit_info.utilization ? `${Math.round(rate_limit_info.utilization * 100)}%` : "high";
|
|
732
|
+
const message = `Rate limit warning: ${utilization} utilization (type: ${rate_limit_info.rateLimitType ?? "unknown"})`;
|
|
733
|
+
host.connection.sendEvent({ type: "thinking", message });
|
|
734
|
+
void host.callbacks.onEvent({ type: "thinking", message });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async function processEvents(events, context, host) {
|
|
738
|
+
const startTime = Date.now();
|
|
739
|
+
let sessionIdStored = false;
|
|
740
|
+
let isTyping = false;
|
|
741
|
+
let retriable = false;
|
|
742
|
+
const turnToolCalls = [];
|
|
743
|
+
for await (const event of events) {
|
|
744
|
+
if (host.isStopped()) break;
|
|
745
|
+
switch (event.type) {
|
|
746
|
+
case "system": {
|
|
747
|
+
const systemEvent = event;
|
|
748
|
+
if (systemEvent.subtype === "init") {
|
|
749
|
+
if (systemEvent.session_id && !sessionIdStored) {
|
|
750
|
+
sessionIdStored = true;
|
|
751
|
+
host.connection.storeSessionId(systemEvent.session_id);
|
|
752
|
+
context.claudeSessionId = systemEvent.session_id;
|
|
753
|
+
}
|
|
754
|
+
await host.callbacks.onEvent({
|
|
755
|
+
type: "thinking",
|
|
756
|
+
message: `Agent initialized (model: ${systemEvent.model})`
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
case "assistant": {
|
|
762
|
+
if (!isTyping) {
|
|
763
|
+
setTimeout(() => host.connection.sendTypingStart(), 200);
|
|
764
|
+
isTyping = true;
|
|
765
|
+
}
|
|
766
|
+
await processAssistantEvent(event, host, turnToolCalls);
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "result": {
|
|
770
|
+
if (isTyping) {
|
|
771
|
+
host.connection.sendTypingStop();
|
|
772
|
+
isTyping = false;
|
|
773
|
+
}
|
|
774
|
+
retriable = await emitResultEvent(event, host, context, startTime);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
case "rate_limit_event": {
|
|
778
|
+
handleRateLimitEvent(event, host);
|
|
779
|
+
break;
|
|
447
780
|
}
|
|
448
781
|
}
|
|
449
|
-
return worktreePath;
|
|
450
782
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
cwd: projectDir,
|
|
454
|
-
stdio: "ignore"
|
|
455
|
-
});
|
|
456
|
-
return worktreePath;
|
|
457
|
-
}
|
|
458
|
-
function removeWorktree(projectDir, taskId) {
|
|
459
|
-
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
460
|
-
if (!existsSync(worktreePath)) return;
|
|
461
|
-
try {
|
|
462
|
-
execSync2(`git worktree remove "${worktreePath}" --force`, {
|
|
463
|
-
cwd: projectDir,
|
|
464
|
-
stdio: "ignore"
|
|
465
|
-
});
|
|
466
|
-
} catch {
|
|
783
|
+
if (isTyping) {
|
|
784
|
+
host.connection.sendTypingStop();
|
|
467
785
|
}
|
|
786
|
+
return { retriable };
|
|
468
787
|
}
|
|
469
788
|
|
|
470
|
-
// src/
|
|
471
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
472
|
-
import { execSync as execSync3 } from "child_process";
|
|
473
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
474
|
-
import { homedir } from "os";
|
|
475
|
-
import { join as join3 } from "path";
|
|
476
|
-
|
|
477
|
-
// src/query-executor.ts
|
|
789
|
+
// src/execution/query-executor.ts
|
|
478
790
|
import { randomUUID } from "crypto";
|
|
479
|
-
import {
|
|
791
|
+
import {
|
|
792
|
+
query
|
|
793
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
480
794
|
|
|
481
|
-
// src/prompt-builder.ts
|
|
795
|
+
// src/execution/prompt-builder.ts
|
|
482
796
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
|
|
483
797
|
function formatFileSize(bytes) {
|
|
484
798
|
if (bytes === void 0) return "";
|
|
@@ -548,7 +862,7 @@ Address the requested changes. Do NOT re-investigate the codebase from scratch o
|
|
|
548
862
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
549
863
|
`Run \`git log --oneline -10\` to review what you already committed.`,
|
|
550
864
|
`Review the current state of the codebase and verify everything is working correctly.`,
|
|
551
|
-
`
|
|
865
|
+
`Reply with a brief status update (visible in chat), then wait for further instructions.`
|
|
552
866
|
);
|
|
553
867
|
if (context.githubPRUrl) {
|
|
554
868
|
parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
|
|
@@ -630,21 +944,21 @@ function buildInstructions(mode, context, scenario) {
|
|
|
630
944
|
`You are the project manager for this task and its subtasks.`,
|
|
631
945
|
`Use list_subtasks to review the current state of child tasks.`,
|
|
632
946
|
`The task details are provided above. Wait for the team to provide instructions before taking action.`,
|
|
633
|
-
`When you finish planning, save the plan with update_task
|
|
947
|
+
`When you finish planning, save the plan with update_task and end your turn. Your reply will be visible to the team in chat.`
|
|
634
948
|
);
|
|
635
949
|
} else if (isPm) {
|
|
636
950
|
parts.push(
|
|
637
951
|
`You are the project manager for this task.`,
|
|
638
952
|
`The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`,
|
|
639
|
-
`When you finish planning, save the plan with update_task
|
|
953
|
+
`When you finish planning, save the plan with update_task and end your turn. Your reply summarizing the plan will be visible in chat. A separate task agent will execute the plan after review.`
|
|
640
954
|
);
|
|
641
955
|
} else {
|
|
642
956
|
parts.push(
|
|
643
957
|
`Begin executing the task plan above immediately.`,
|
|
644
958
|
`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.`,
|
|
645
959
|
`Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
|
|
646
|
-
`
|
|
647
|
-
`When finished, commit your changes,
|
|
960
|
+
`Your replies are visible to the team in chat \u2014 briefly describe what you're doing when you begin meaningful implementation, and again when the PR is ready.`,
|
|
961
|
+
`When finished, commit your changes, then run \`git fetch origin ${context.githubBranch}\` and \`git push origin ${context.githubBranch}\` (use --force-with-lease if push fails). Then use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
|
|
648
962
|
);
|
|
649
963
|
}
|
|
650
964
|
} else if (scenario === "idle_relaunch") {
|
|
@@ -659,7 +973,7 @@ function buildInstructions(mode, context, scenario) {
|
|
|
659
973
|
`You were relaunched but no new instructions have been given since your last run.`,
|
|
660
974
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
661
975
|
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
|
|
662
|
-
`
|
|
976
|
+
`Reply with a brief status update summarizing where things stand (visible in chat).`,
|
|
663
977
|
`Then wait for further instructions \u2014 do NOT redo work that was already completed.`
|
|
664
978
|
);
|
|
665
979
|
if (context.githubPRUrl) {
|
|
@@ -712,8 +1026,9 @@ function buildInitialPrompt(mode, context) {
|
|
|
712
1026
|
const instructions = buildInstructions(mode, context, scenario);
|
|
713
1027
|
return [...body, ...instructions].join("\n");
|
|
714
1028
|
}
|
|
715
|
-
function buildSystemPrompt(mode, context, config, setupLog) {
|
|
1029
|
+
function buildSystemPrompt(mode, context, config, setupLog, pmSubMode = "planning") {
|
|
716
1030
|
const isPm = mode === "pm";
|
|
1031
|
+
const isPmActive = isPm && pmSubMode === "active";
|
|
717
1032
|
const pmParts = [
|
|
718
1033
|
`You are an AI project manager helping to plan tasks for the "${context.title}" project.`,
|
|
719
1034
|
`You are running locally with full access to the repository.`,
|
|
@@ -727,7 +1042,7 @@ Environment (ready, no setup required):`,
|
|
|
727
1042
|
Workflow:`,
|
|
728
1043
|
`- You can draft and iterate on plans in .claude/plans/*.md \u2014 these files are automatically synced to the task.`,
|
|
729
1044
|
`- You can also use update_task directly to save the plan to the task.`,
|
|
730
|
-
`- After saving the plan,
|
|
1045
|
+
`- After saving the plan, end your turn with a summary reply (the team sees your responses in chat automatically). Do NOT attempt to execute the plan yourself.`,
|
|
731
1046
|
`- A separate task agent will handle execution after the team reviews and approves your plan.`
|
|
732
1047
|
];
|
|
733
1048
|
if (isPm && context.isParentTask) {
|
|
@@ -756,7 +1071,30 @@ Project Agents:`);
|
|
|
756
1071
|
pmParts.push(`- ${pa.agent.name} (${role}${sp})`);
|
|
757
1072
|
}
|
|
758
1073
|
}
|
|
759
|
-
const
|
|
1074
|
+
const activeParts = [
|
|
1075
|
+
`You are an AI project manager in ACTIVE mode for the "${context.title}" project.`,
|
|
1076
|
+
`You have direct coding access to the repository at ${config.workspaceDir}.`,
|
|
1077
|
+
`You can edit files, run tests, and make commits.`,
|
|
1078
|
+
`You still have access to all PM tools (subtasks, update_task, chat).`,
|
|
1079
|
+
`
|
|
1080
|
+
Environment (ready, no setup required):`,
|
|
1081
|
+
`- Repository is cloned at your current working directory.`,
|
|
1082
|
+
`- You can read, write, and edit files directly.`,
|
|
1083
|
+
`- You can run shell commands including git, build tools, and test runners.`,
|
|
1084
|
+
context.githubBranch ? `- You are working on branch: \`${context.githubBranch}\`` : "",
|
|
1085
|
+
`
|
|
1086
|
+
Safety rules:`,
|
|
1087
|
+
`- Stay within the project directory (${config.workspaceDir}).`,
|
|
1088
|
+
`- Do NOT run \`git push --force\` or \`git reset --hard\`. Use \`--force-with-lease\` if needed.`,
|
|
1089
|
+
`- Do NOT delete \`.env\` files or modify \`node_modules\`.`,
|
|
1090
|
+
`- Do NOT run destructive commands like \`rm -rf /\`.`,
|
|
1091
|
+
`
|
|
1092
|
+
Workflow:`,
|
|
1093
|
+
`- You can make code changes, fix bugs, run tests, and commit directly.`,
|
|
1094
|
+
`- When done with changes, summarize what you did in your reply.`,
|
|
1095
|
+
`- If you toggled into active mode temporarily, mention when you're done so the team can switch you back to planning mode.`
|
|
1096
|
+
].filter(Boolean);
|
|
1097
|
+
const parts = isPmActive ? activeParts : isPm ? pmParts : [
|
|
760
1098
|
`You are an AI agent working on a task for the "${context.title}" project.`,
|
|
761
1099
|
`You are running inside a GitHub Codespace with full access to the repository.`,
|
|
762
1100
|
`
|
|
@@ -779,8 +1117,7 @@ Git safety \u2014 STRICT rules:`,
|
|
|
779
1117
|
`- NEVER run \`git checkout main\`, \`git checkout dev\`, or switch to any branch other than \`${context.githubBranch}\`.`,
|
|
780
1118
|
`- NEVER create new branches (no \`git checkout -b\`, \`git switch -c\`, etc.).`,
|
|
781
1119
|
`- This branch was created from \`${context.baseBranch}\`. PRs will automatically target that branch.`,
|
|
782
|
-
`- If \`git push\` fails with "non-fast-forward"
|
|
783
|
-
`- If you encounter merge conflicts during rebase, resolve them in place \u2014 do NOT abandon the branch.`
|
|
1120
|
+
`- If \`git push\` fails with "non-fast-forward", run \`git push --force-with-lease origin ${context.githubBranch}\`. This branch is exclusively yours \u2014 force-with-lease is safe.`
|
|
784
1121
|
];
|
|
785
1122
|
if (setupLog.length > 0) {
|
|
786
1123
|
parts.push(
|
|
@@ -803,11 +1140,12 @@ ${config.instructions}`);
|
|
|
803
1140
|
}
|
|
804
1141
|
parts.push(
|
|
805
1142
|
`
|
|
806
|
-
|
|
807
|
-
`
|
|
808
|
-
`
|
|
1143
|
+
Your responses are sent directly to the task chat \u2014 the team sees everything you say.`,
|
|
1144
|
+
`Do NOT call the post_to_chat tool for your own task; your replies already appear in chat automatically.`,
|
|
1145
|
+
`Only use post_to_chat if you need to message a different task's chat (e.g. a parent task via get_task).`,
|
|
1146
|
+
`Use read_task_chat only if you need to re-read earlier messages beyond the chat context above.`
|
|
809
1147
|
);
|
|
810
|
-
if (!isPm) {
|
|
1148
|
+
if (!isPm || isPmActive) {
|
|
811
1149
|
parts.push(
|
|
812
1150
|
`Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
|
|
813
1151
|
);
|
|
@@ -815,8 +1153,11 @@ You have access to Conveyor MCP tools to interact with the task management syste
|
|
|
815
1153
|
return parts.join("\n");
|
|
816
1154
|
}
|
|
817
1155
|
|
|
818
|
-
// src/
|
|
819
|
-
import {
|
|
1156
|
+
// src/tools/index.ts
|
|
1157
|
+
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
1158
|
+
|
|
1159
|
+
// src/tools/common-tools.ts
|
|
1160
|
+
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
820
1161
|
import { z } from "zod";
|
|
821
1162
|
function buildCommonTools(connection, config) {
|
|
822
1163
|
return [
|
|
@@ -842,7 +1183,7 @@ function buildCommonTools(connection, config) {
|
|
|
842
1183
|
),
|
|
843
1184
|
tool(
|
|
844
1185
|
"post_to_chat",
|
|
845
|
-
"Post a message to the task chat
|
|
1186
|
+
"Post a message to the task chat. Your normal replies already appear in chat \u2014 only use this for explicit out-of-band updates or posting to a different task's chat",
|
|
846
1187
|
{ message: z.string().describe("The message to post to the team") },
|
|
847
1188
|
({ message }) => {
|
|
848
1189
|
connection.postChatMessage(message);
|
|
@@ -924,6 +1265,10 @@ function buildCommonTools(connection, config) {
|
|
|
924
1265
|
)
|
|
925
1266
|
];
|
|
926
1267
|
}
|
|
1268
|
+
|
|
1269
|
+
// src/tools/pm-tools.ts
|
|
1270
|
+
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1271
|
+
import { z as z2 } from "zod";
|
|
927
1272
|
function buildStoryPointDescription(storyPoints) {
|
|
928
1273
|
if (storyPoints && storyPoints.length > 0) {
|
|
929
1274
|
const tiers = storyPoints.map((sp) => `${sp.value}=${sp.name}`).join(", ");
|
|
@@ -934,12 +1279,12 @@ function buildStoryPointDescription(storyPoints) {
|
|
|
934
1279
|
function buildPmTools(connection, storyPoints) {
|
|
935
1280
|
const spDescription = buildStoryPointDescription(storyPoints);
|
|
936
1281
|
return [
|
|
937
|
-
|
|
1282
|
+
tool2(
|
|
938
1283
|
"update_task",
|
|
939
1284
|
"Save the finalized task plan and/or description",
|
|
940
1285
|
{
|
|
941
|
-
plan:
|
|
942
|
-
description:
|
|
1286
|
+
plan: z2.string().optional().describe("The task plan in markdown"),
|
|
1287
|
+
description: z2.string().optional().describe("Updated task description")
|
|
943
1288
|
},
|
|
944
1289
|
async ({ plan, description }) => {
|
|
945
1290
|
try {
|
|
@@ -950,15 +1295,15 @@ function buildPmTools(connection, storyPoints) {
|
|
|
950
1295
|
}
|
|
951
1296
|
}
|
|
952
1297
|
),
|
|
953
|
-
|
|
1298
|
+
tool2(
|
|
954
1299
|
"create_subtask",
|
|
955
1300
|
"Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
|
|
956
1301
|
{
|
|
957
|
-
title:
|
|
958
|
-
description:
|
|
959
|
-
plan:
|
|
960
|
-
ordinal:
|
|
961
|
-
storyPointValue:
|
|
1302
|
+
title: z2.string().describe("Subtask title"),
|
|
1303
|
+
description: z2.string().optional().describe("Brief description"),
|
|
1304
|
+
plan: z2.string().optional().describe("Implementation plan in markdown"),
|
|
1305
|
+
ordinal: z2.number().optional().describe("Step/order number (0-based)"),
|
|
1306
|
+
storyPointValue: z2.number().optional().describe(spDescription)
|
|
962
1307
|
},
|
|
963
1308
|
async (params) => {
|
|
964
1309
|
try {
|
|
@@ -971,16 +1316,16 @@ function buildPmTools(connection, storyPoints) {
|
|
|
971
1316
|
}
|
|
972
1317
|
}
|
|
973
1318
|
),
|
|
974
|
-
|
|
1319
|
+
tool2(
|
|
975
1320
|
"update_subtask",
|
|
976
1321
|
"Update an existing subtask's fields",
|
|
977
1322
|
{
|
|
978
|
-
subtaskId:
|
|
979
|
-
title:
|
|
980
|
-
description:
|
|
981
|
-
plan:
|
|
982
|
-
ordinal:
|
|
983
|
-
storyPointValue:
|
|
1323
|
+
subtaskId: z2.string().describe("The subtask ID to update"),
|
|
1324
|
+
title: z2.string().optional(),
|
|
1325
|
+
description: z2.string().optional(),
|
|
1326
|
+
plan: z2.string().optional(),
|
|
1327
|
+
ordinal: z2.number().optional(),
|
|
1328
|
+
storyPointValue: z2.number().optional().describe(spDescription)
|
|
984
1329
|
},
|
|
985
1330
|
async ({ subtaskId, ...fields }) => {
|
|
986
1331
|
try {
|
|
@@ -991,10 +1336,10 @@ function buildPmTools(connection, storyPoints) {
|
|
|
991
1336
|
}
|
|
992
1337
|
}
|
|
993
1338
|
),
|
|
994
|
-
|
|
1339
|
+
tool2(
|
|
995
1340
|
"delete_subtask",
|
|
996
1341
|
"Delete a subtask",
|
|
997
|
-
{ subtaskId:
|
|
1342
|
+
{ subtaskId: z2.string().describe("The subtask ID to delete") },
|
|
998
1343
|
async ({ subtaskId }) => {
|
|
999
1344
|
try {
|
|
1000
1345
|
await Promise.resolve(connection.deleteSubtask(subtaskId));
|
|
@@ -1004,7 +1349,7 @@ function buildPmTools(connection, storyPoints) {
|
|
|
1004
1349
|
}
|
|
1005
1350
|
}
|
|
1006
1351
|
),
|
|
1007
|
-
|
|
1352
|
+
tool2(
|
|
1008
1353
|
"list_subtasks",
|
|
1009
1354
|
"List all subtasks under the current parent task",
|
|
1010
1355
|
{},
|
|
@@ -1020,14 +1365,18 @@ function buildPmTools(connection, storyPoints) {
|
|
|
1020
1365
|
)
|
|
1021
1366
|
];
|
|
1022
1367
|
}
|
|
1368
|
+
|
|
1369
|
+
// src/tools/task-tools.ts
|
|
1370
|
+
import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
1371
|
+
import { z as z3 } from "zod";
|
|
1023
1372
|
function buildTaskTools(connection) {
|
|
1024
1373
|
return [
|
|
1025
|
-
|
|
1374
|
+
tool3(
|
|
1026
1375
|
"create_pull_request",
|
|
1027
1376
|
"Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
|
|
1028
1377
|
{
|
|
1029
|
-
title:
|
|
1030
|
-
body:
|
|
1378
|
+
title: z3.string().describe("The PR title"),
|
|
1379
|
+
body: z3.string().describe("The PR description/body in markdown")
|
|
1031
1380
|
},
|
|
1032
1381
|
async ({ title, body }) => {
|
|
1033
1382
|
try {
|
|
@@ -1046,6 +1395,8 @@ function buildTaskTools(connection) {
|
|
|
1046
1395
|
)
|
|
1047
1396
|
];
|
|
1048
1397
|
}
|
|
1398
|
+
|
|
1399
|
+
// src/tools/index.ts
|
|
1049
1400
|
function textResult(text) {
|
|
1050
1401
|
return { content: [{ type: "text", text }] };
|
|
1051
1402
|
}
|
|
@@ -1058,144 +1409,17 @@ function createConveyorMcpServer(connection, config, context) {
|
|
|
1058
1409
|
});
|
|
1059
1410
|
}
|
|
1060
1411
|
|
|
1061
|
-
// src/query-executor.ts
|
|
1062
|
-
var
|
|
1412
|
+
// src/execution/query-executor.ts
|
|
1413
|
+
var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
|
|
1063
1414
|
var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
|
|
1064
1415
|
var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
|
|
1065
|
-
|
|
1066
|
-
const msg = event.message;
|
|
1067
|
-
const content = msg.content;
|
|
1068
|
-
const turnTextParts = [];
|
|
1069
|
-
for (const block of content) {
|
|
1070
|
-
const blockType = block.type;
|
|
1071
|
-
if (blockType === "text") {
|
|
1072
|
-
const text = block.text;
|
|
1073
|
-
turnTextParts.push(text);
|
|
1074
|
-
host.connection.sendEvent({ type: "message", content: text });
|
|
1075
|
-
await host.callbacks.onEvent({ type: "message", content: text });
|
|
1076
|
-
} else if (blockType === "tool_use") {
|
|
1077
|
-
const name = block.name;
|
|
1078
|
-
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
1079
|
-
const isContentTool = ["edit", "write"].includes(name.toLowerCase());
|
|
1080
|
-
const inputLimit = isContentTool ? 1e4 : 500;
|
|
1081
|
-
const summary = {
|
|
1082
|
-
tool: name,
|
|
1083
|
-
input: inputStr.slice(0, inputLimit),
|
|
1084
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1085
|
-
};
|
|
1086
|
-
turnToolCalls.push(summary);
|
|
1087
|
-
host.connection.sendEvent({ type: "tool_use", tool: name, input: inputStr });
|
|
1088
|
-
await host.callbacks.onEvent({ type: "tool_use", tool: name, input: inputStr });
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (turnTextParts.length > 0) {
|
|
1092
|
-
host.connection.postChatMessage(turnTextParts.join("\n\n"));
|
|
1093
|
-
}
|
|
1094
|
-
if (turnToolCalls.length > 0) {
|
|
1095
|
-
host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
|
|
1096
|
-
turnToolCalls.length = 0;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
function handleResultEvent(event, host, context, startTime) {
|
|
1100
|
-
const resultEvent = event;
|
|
1101
|
-
let totalCostUsd = 0;
|
|
1102
|
-
let retriable = false;
|
|
1103
|
-
if (resultEvent.subtype === "success") {
|
|
1104
|
-
totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
|
|
1105
|
-
const durationMs = Date.now() - startTime;
|
|
1106
|
-
const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
|
|
1107
|
-
if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
|
|
1108
|
-
retriable = true;
|
|
1109
|
-
}
|
|
1110
|
-
host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
|
|
1111
|
-
if (totalCostUsd > 0 && context.agentId && context._runnerSessionId) {
|
|
1112
|
-
host.connection.trackSpending({
|
|
1113
|
-
agentId: context.agentId,
|
|
1114
|
-
sessionId: context._runnerSessionId,
|
|
1115
|
-
totalCostUsd,
|
|
1116
|
-
onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
} else {
|
|
1120
|
-
const errors = "errors" in resultEvent ? resultEvent.errors : [];
|
|
1121
|
-
const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
|
|
1122
|
-
if (API_ERROR_PATTERN.test(errorMsg)) {
|
|
1123
|
-
retriable = true;
|
|
1124
|
-
}
|
|
1125
|
-
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
1126
|
-
}
|
|
1127
|
-
return { totalCostUsd, retriable };
|
|
1128
|
-
}
|
|
1129
|
-
async function emitResultEvent(event, host, context, startTime) {
|
|
1130
|
-
const result = handleResultEvent(event, host, context, startTime);
|
|
1131
|
-
const durationMs = Date.now() - startTime;
|
|
1132
|
-
const resultEvent = event;
|
|
1133
|
-
if (resultEvent.subtype === "success") {
|
|
1134
|
-
const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
|
|
1135
|
-
await host.callbacks.onEvent({
|
|
1136
|
-
type: "completed",
|
|
1137
|
-
summary,
|
|
1138
|
-
costUsd: result.totalCostUsd,
|
|
1139
|
-
durationMs
|
|
1140
|
-
});
|
|
1141
|
-
} else {
|
|
1142
|
-
const errors = "errors" in resultEvent ? resultEvent.errors : [];
|
|
1143
|
-
const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
|
|
1144
|
-
await host.callbacks.onEvent({ type: "error", message: errorMsg });
|
|
1145
|
-
}
|
|
1146
|
-
return result.retriable;
|
|
1147
|
-
}
|
|
1148
|
-
async function processEvents(events, context, host) {
|
|
1149
|
-
const startTime = Date.now();
|
|
1150
|
-
let sessionIdStored = false;
|
|
1151
|
-
let isTyping = false;
|
|
1152
|
-
let retriable = false;
|
|
1153
|
-
const turnToolCalls = [];
|
|
1154
|
-
for await (const event of events) {
|
|
1155
|
-
if (host.isStopped()) break;
|
|
1156
|
-
switch (event.type) {
|
|
1157
|
-
case "system": {
|
|
1158
|
-
if (event.subtype === "init") {
|
|
1159
|
-
const sessionId = event.session_id;
|
|
1160
|
-
if (sessionId && !sessionIdStored) {
|
|
1161
|
-
sessionIdStored = true;
|
|
1162
|
-
host.connection.storeSessionId(sessionId);
|
|
1163
|
-
context.claudeSessionId = sessionId;
|
|
1164
|
-
}
|
|
1165
|
-
await host.callbacks.onEvent({
|
|
1166
|
-
type: "thinking",
|
|
1167
|
-
message: `Agent initialized (model: ${event.model})`
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
break;
|
|
1171
|
-
}
|
|
1172
|
-
case "assistant": {
|
|
1173
|
-
if (!isTyping) {
|
|
1174
|
-
setTimeout(() => host.connection.sendTypingStart(), 200);
|
|
1175
|
-
isTyping = true;
|
|
1176
|
-
}
|
|
1177
|
-
await processAssistantEvent(event, host, turnToolCalls);
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
1180
|
-
case "result": {
|
|
1181
|
-
if (isTyping) {
|
|
1182
|
-
host.connection.sendTypingStop();
|
|
1183
|
-
isTyping = false;
|
|
1184
|
-
}
|
|
1185
|
-
retriable = await emitResultEvent(event, host, context, startTime);
|
|
1186
|
-
break;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
if (isTyping) {
|
|
1191
|
-
host.connection.sendTypingStop();
|
|
1192
|
-
}
|
|
1193
|
-
return { retriable };
|
|
1194
|
-
}
|
|
1416
|
+
var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
|
|
1195
1417
|
function buildCanUseTool(host) {
|
|
1196
1418
|
const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1197
1419
|
return async (toolName, input) => {
|
|
1198
|
-
|
|
1420
|
+
const isPmPlanning = host.config.mode === "pm" && host.activeMode === "planning";
|
|
1421
|
+
const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
|
|
1422
|
+
if (isPmPlanning && PM_PLAN_FILE_TOOLS.has(toolName)) {
|
|
1199
1423
|
const filePath = String(input.file_path ?? input.path ?? "");
|
|
1200
1424
|
if (filePath.includes(".claude/plans/")) {
|
|
1201
1425
|
return { behavior: "allow", updatedInput: input };
|
|
@@ -1205,6 +1429,15 @@ function buildCanUseTool(host) {
|
|
|
1205
1429
|
message: "File write tools are only available for plan files in PM mode."
|
|
1206
1430
|
};
|
|
1207
1431
|
}
|
|
1432
|
+
if (isPmActive && toolName === "Bash") {
|
|
1433
|
+
const cmd = String(input.command ?? "");
|
|
1434
|
+
if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
|
|
1435
|
+
return {
|
|
1436
|
+
behavior: "deny",
|
|
1437
|
+
message: "Destructive operation blocked in active mode. Use safer alternatives."
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1208
1441
|
if (toolName !== "AskUserQuestion") {
|
|
1209
1442
|
return { behavior: "allow", updatedInput: input };
|
|
1210
1443
|
}
|
|
@@ -1233,13 +1466,41 @@ function buildCanUseTool(host) {
|
|
|
1233
1466
|
}
|
|
1234
1467
|
function buildQueryOptions(host, context) {
|
|
1235
1468
|
const settings = context.agentSettings ?? host.config.agentSettings ?? {};
|
|
1236
|
-
const
|
|
1469
|
+
const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
|
|
1470
|
+
const systemPromptText = buildSystemPrompt(
|
|
1471
|
+
host.config.mode,
|
|
1472
|
+
context,
|
|
1473
|
+
host.config,
|
|
1474
|
+
host.setupLog,
|
|
1475
|
+
isPmActive ? "active" : "planning"
|
|
1476
|
+
);
|
|
1237
1477
|
const conveyorMcp = createConveyorMcpServer(host.connection, host.config, context);
|
|
1238
1478
|
const isPm = host.config.mode === "pm";
|
|
1239
|
-
const pmDisallowedTools = isPm ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
|
|
1479
|
+
const pmDisallowedTools = isPm && !isPmActive ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
|
|
1240
1480
|
const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
|
|
1241
1481
|
const settingSources = settings.settingSources ?? ["user", "project"];
|
|
1242
|
-
|
|
1482
|
+
const hooks = {
|
|
1483
|
+
PostToolUse: [
|
|
1484
|
+
{
|
|
1485
|
+
hooks: [
|
|
1486
|
+
async (input) => {
|
|
1487
|
+
if (input.hook_event_name === "PostToolUse") {
|
|
1488
|
+
const output = typeof input.tool_response === "string" ? input.tool_response.slice(0, 500) : JSON.stringify(input.tool_response).slice(0, 500);
|
|
1489
|
+
host.connection.sendEvent({
|
|
1490
|
+
type: "tool_result",
|
|
1491
|
+
tool: input.tool_name,
|
|
1492
|
+
output,
|
|
1493
|
+
isError: false
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
return { continue: true };
|
|
1497
|
+
}
|
|
1498
|
+
],
|
|
1499
|
+
timeout: 5
|
|
1500
|
+
}
|
|
1501
|
+
]
|
|
1502
|
+
};
|
|
1503
|
+
const baseOptions = {
|
|
1243
1504
|
model: context.model || host.config.model,
|
|
1244
1505
|
systemPrompt: {
|
|
1245
1506
|
type: "preset",
|
|
@@ -1248,11 +1509,12 @@ function buildQueryOptions(host, context) {
|
|
|
1248
1509
|
},
|
|
1249
1510
|
settingSources,
|
|
1250
1511
|
cwd: host.config.workspaceDir,
|
|
1251
|
-
permissionMode: "bypassPermissions",
|
|
1252
|
-
allowDangerouslySkipPermissions:
|
|
1512
|
+
permissionMode: isPmActive ? "acceptEdits" : "bypassPermissions",
|
|
1513
|
+
allowDangerouslySkipPermissions: !isPmActive,
|
|
1253
1514
|
canUseTool: buildCanUseTool(host),
|
|
1254
1515
|
tools: { type: "preset", preset: "claude_code" },
|
|
1255
1516
|
mcpServers: { conveyor: conveyorMcp },
|
|
1517
|
+
hooks,
|
|
1256
1518
|
maxTurns: settings.maxTurns,
|
|
1257
1519
|
effort: settings.effort,
|
|
1258
1520
|
thinking: settings.thinking,
|
|
@@ -1261,6 +1523,25 @@ function buildQueryOptions(host, context) {
|
|
|
1261
1523
|
disallowedTools: disallowedTools.length > 0 ? disallowedTools : void 0,
|
|
1262
1524
|
enableFileCheckpointing: settings.enableFileCheckpointing
|
|
1263
1525
|
};
|
|
1526
|
+
if (isPmActive) {
|
|
1527
|
+
const apiHostname = new URL(host.config.conveyorApiUrl).hostname;
|
|
1528
|
+
baseOptions.sandbox = {
|
|
1529
|
+
enabled: true,
|
|
1530
|
+
autoAllowBashIfSandboxed: true,
|
|
1531
|
+
allowUnsandboxedCommands: false,
|
|
1532
|
+
filesystem: {
|
|
1533
|
+
allowWrite: [`${host.config.workspaceDir}/**`],
|
|
1534
|
+
denyRead: ["/etc/shadow", "/etc/passwd", "**/.env", "**/.env.*"],
|
|
1535
|
+
denyWrite: ["**/.env", "**/.env.*", "**/node_modules/**"]
|
|
1536
|
+
},
|
|
1537
|
+
network: {
|
|
1538
|
+
allowedDomains: [apiHostname, "api.anthropic.com"],
|
|
1539
|
+
allowManagedDomainsOnly: true,
|
|
1540
|
+
allowLocalBinding: true
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
return baseOptions;
|
|
1264
1545
|
}
|
|
1265
1546
|
function buildMultimodalPrompt(textPrompt, context) {
|
|
1266
1547
|
const taskImages = (context.files ?? []).filter(
|
|
@@ -1309,25 +1590,40 @@ function buildMultimodalPrompt(textPrompt, context) {
|
|
|
1309
1590
|
async function runSdkQuery(host, context, followUpContent) {
|
|
1310
1591
|
if (host.isStopped()) return;
|
|
1311
1592
|
const isPm = host.config.mode === "pm";
|
|
1312
|
-
|
|
1593
|
+
const isPmPlanning = isPm && host.activeMode === "planning";
|
|
1594
|
+
if (isPmPlanning) {
|
|
1313
1595
|
host.snapshotPlanFiles();
|
|
1314
1596
|
}
|
|
1315
1597
|
const options = buildQueryOptions(host, context);
|
|
1316
1598
|
const resume = context.claudeSessionId ?? void 0;
|
|
1317
1599
|
if (followUpContent) {
|
|
1600
|
+
const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
1601
|
+
const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
|
|
1602
|
+
(b) => b.type === "image"
|
|
1603
|
+
);
|
|
1318
1604
|
const textPrompt = isPm ? `${buildInitialPrompt(host.config.mode, context)}
|
|
1319
1605
|
|
|
1320
1606
|
---
|
|
1321
1607
|
|
|
1322
1608
|
The team says:
|
|
1323
|
-
${
|
|
1324
|
-
|
|
1609
|
+
${followUpText}` : followUpText;
|
|
1610
|
+
let prompt;
|
|
1611
|
+
if (isPm) {
|
|
1612
|
+
prompt = buildMultimodalPrompt(textPrompt, context);
|
|
1613
|
+
if (followUpImages.length > 0 && Array.isArray(prompt)) {
|
|
1614
|
+
prompt.push(...followUpImages);
|
|
1615
|
+
}
|
|
1616
|
+
} else if (followUpImages.length > 0) {
|
|
1617
|
+
prompt = [{ type: "text", text: textPrompt }, ...followUpImages];
|
|
1618
|
+
} else {
|
|
1619
|
+
prompt = textPrompt;
|
|
1620
|
+
}
|
|
1325
1621
|
const agentQuery = query({
|
|
1326
1622
|
prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
|
|
1327
1623
|
options: { ...options, resume }
|
|
1328
1624
|
});
|
|
1329
1625
|
await runWithRetry(agentQuery, context, host, options);
|
|
1330
|
-
} else if (
|
|
1626
|
+
} else if (isPmPlanning) {
|
|
1331
1627
|
return;
|
|
1332
1628
|
} else {
|
|
1333
1629
|
const initialPrompt = buildInitialPrompt(host.config.mode, context);
|
|
@@ -1338,7 +1634,7 @@ ${followUpContent}` : followUpContent;
|
|
|
1338
1634
|
});
|
|
1339
1635
|
await runWithRetry(agentQuery, context, host, options);
|
|
1340
1636
|
}
|
|
1341
|
-
if (
|
|
1637
|
+
if (isPmPlanning) {
|
|
1342
1638
|
host.syncPlanFile();
|
|
1343
1639
|
}
|
|
1344
1640
|
}
|
|
@@ -1373,7 +1669,7 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
1373
1669
|
});
|
|
1374
1670
|
return runWithRetry(freshQuery, context, host, options);
|
|
1375
1671
|
}
|
|
1376
|
-
const isApiError = error instanceof Error &&
|
|
1672
|
+
const isApiError = error instanceof Error && API_ERROR_PATTERN2.test(error.message);
|
|
1377
1673
|
if (!isApiError) throw error;
|
|
1378
1674
|
}
|
|
1379
1675
|
if (attempt >= RETRY_DELAYS_MS.length) {
|
|
@@ -1407,9 +1703,131 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
1407
1703
|
host.connection.emitStatus("running");
|
|
1408
1704
|
await host.callbacks.onStatusChange("running");
|
|
1409
1705
|
}
|
|
1410
|
-
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/execution/cost-tracker.ts
|
|
1709
|
+
var CostTracker = class {
|
|
1710
|
+
cumulativeCostUsd = 0;
|
|
1711
|
+
modelUsage = /* @__PURE__ */ new Map();
|
|
1712
|
+
/** Add cost from a completed query and return the running total */
|
|
1713
|
+
addQueryCost(queryCostUsd) {
|
|
1714
|
+
this.cumulativeCostUsd += queryCostUsd;
|
|
1715
|
+
return this.cumulativeCostUsd;
|
|
1716
|
+
}
|
|
1717
|
+
/** Merge per-model usage from a completed query */
|
|
1718
|
+
addModelUsage(usage) {
|
|
1719
|
+
for (const [model, data] of Object.entries(usage)) {
|
|
1720
|
+
const existing = this.modelUsage.get(model) ?? {
|
|
1721
|
+
model,
|
|
1722
|
+
inputTokens: 0,
|
|
1723
|
+
outputTokens: 0,
|
|
1724
|
+
cacheReadInputTokens: 0,
|
|
1725
|
+
cacheCreationInputTokens: 0,
|
|
1726
|
+
costUSD: 0
|
|
1727
|
+
};
|
|
1728
|
+
existing.inputTokens += data.inputTokens ?? 0;
|
|
1729
|
+
existing.outputTokens += data.outputTokens ?? 0;
|
|
1730
|
+
existing.cacheReadInputTokens += data.cacheReadInputTokens ?? 0;
|
|
1731
|
+
existing.cacheCreationInputTokens += data.cacheCreationInputTokens ?? 0;
|
|
1732
|
+
existing.costUSD += data.costUSD ?? 0;
|
|
1733
|
+
this.modelUsage.set(model, existing);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
get totalCostUsd() {
|
|
1737
|
+
return this.cumulativeCostUsd;
|
|
1738
|
+
}
|
|
1739
|
+
get modelBreakdown() {
|
|
1740
|
+
return [...this.modelUsage.values()];
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
// src/runner/plan-sync.ts
|
|
1745
|
+
import { readdirSync, statSync, readFileSync } from "fs";
|
|
1746
|
+
import { homedir } from "os";
|
|
1747
|
+
import { join as join3 } from "path";
|
|
1748
|
+
var PlanSync = class {
|
|
1749
|
+
planFileSnapshot = /* @__PURE__ */ new Map();
|
|
1750
|
+
lockedPlanFile = null;
|
|
1751
|
+
workspaceDir;
|
|
1752
|
+
connection;
|
|
1753
|
+
constructor(workspaceDir, connection) {
|
|
1754
|
+
this.workspaceDir = workspaceDir;
|
|
1755
|
+
this.connection = connection;
|
|
1756
|
+
}
|
|
1757
|
+
updateWorkspaceDir(workspaceDir) {
|
|
1758
|
+
this.workspaceDir = workspaceDir;
|
|
1759
|
+
}
|
|
1760
|
+
getPlanDirs() {
|
|
1761
|
+
return [join3(homedir(), ".claude", "plans"), join3(this.workspaceDir, ".claude", "plans")];
|
|
1762
|
+
}
|
|
1763
|
+
snapshotPlanFiles() {
|
|
1764
|
+
this.planFileSnapshot.clear();
|
|
1765
|
+
this.lockedPlanFile = null;
|
|
1766
|
+
for (const plansDir of this.getPlanDirs()) {
|
|
1767
|
+
try {
|
|
1768
|
+
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1769
|
+
try {
|
|
1770
|
+
const fullPath = join3(plansDir, file);
|
|
1771
|
+
const stat = statSync(fullPath);
|
|
1772
|
+
this.planFileSnapshot.set(fullPath, stat.mtimeMs);
|
|
1773
|
+
} catch {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
syncPlanFile() {
|
|
1782
|
+
if (this.lockedPlanFile) {
|
|
1783
|
+
try {
|
|
1784
|
+
const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
|
|
1785
|
+
if (content) {
|
|
1786
|
+
this.connection.updateTaskFields({ plan: content });
|
|
1787
|
+
const fileName = this.lockedPlanFile.split("/").pop();
|
|
1788
|
+
this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
|
|
1789
|
+
}
|
|
1790
|
+
} catch {
|
|
1791
|
+
}
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
let newest = null;
|
|
1795
|
+
for (const plansDir of this.getPlanDirs()) {
|
|
1796
|
+
let files;
|
|
1797
|
+
try {
|
|
1798
|
+
files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
|
|
1799
|
+
} catch {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
for (const file of files) {
|
|
1803
|
+
const fullPath = join3(plansDir, file);
|
|
1804
|
+
try {
|
|
1805
|
+
const stat = statSync(fullPath);
|
|
1806
|
+
const prevMtime = this.planFileSnapshot.get(fullPath);
|
|
1807
|
+
const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
|
|
1808
|
+
if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
|
|
1809
|
+
newest = { path: fullPath, mtime: stat.mtimeMs };
|
|
1810
|
+
}
|
|
1811
|
+
} catch {
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (newest) {
|
|
1817
|
+
this.lockedPlanFile = newest.path;
|
|
1818
|
+
const content = readFileSync(newest.path, "utf-8").trim();
|
|
1819
|
+
if (content) {
|
|
1820
|
+
this.connection.updateTaskFields({ plan: content });
|
|
1821
|
+
const fileName = newest.path.split("/").pop();
|
|
1822
|
+
this.connection.postChatMessage(
|
|
1823
|
+
`Detected local plan file (${fileName}) and synced it to the task plan.`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1411
1829
|
|
|
1412
|
-
// src/runner.ts
|
|
1830
|
+
// src/runner/agent-runner.ts
|
|
1413
1831
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1414
1832
|
var AgentRunner = class _AgentRunner {
|
|
1415
1833
|
config;
|
|
@@ -1422,14 +1840,16 @@ var AgentRunner = class _AgentRunner {
|
|
|
1422
1840
|
setupLog = [];
|
|
1423
1841
|
heartbeatTimer = null;
|
|
1424
1842
|
taskContext = null;
|
|
1425
|
-
|
|
1426
|
-
|
|
1843
|
+
planSync;
|
|
1844
|
+
costTracker = new CostTracker();
|
|
1427
1845
|
worktreeActive = false;
|
|
1846
|
+
activeMode = "planning";
|
|
1428
1847
|
static MAX_SETUP_LOG_LINES = 50;
|
|
1429
1848
|
constructor(config, callbacks) {
|
|
1430
1849
|
this.config = config;
|
|
1431
1850
|
this.connection = new ConveyorConnection(config);
|
|
1432
1851
|
this.callbacks = callbacks;
|
|
1852
|
+
this.planSync = new PlanSync(config.workspaceDir, this.connection);
|
|
1433
1853
|
}
|
|
1434
1854
|
get state() {
|
|
1435
1855
|
return this._state;
|
|
@@ -1459,6 +1879,7 @@ var AgentRunner = class _AgentRunner {
|
|
|
1459
1879
|
this.connection.onChatMessage(
|
|
1460
1880
|
(message) => this.injectHumanMessage(message.content, message.files)
|
|
1461
1881
|
);
|
|
1882
|
+
this.connection.onModeChange((data) => this.handleModeChange(data.mode));
|
|
1462
1883
|
await this.setState("connected");
|
|
1463
1884
|
this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
|
|
1464
1885
|
this.startHeartbeat();
|
|
@@ -1471,11 +1892,12 @@ var AgentRunner = class _AgentRunner {
|
|
|
1471
1892
|
return;
|
|
1472
1893
|
}
|
|
1473
1894
|
}
|
|
1474
|
-
|
|
1895
|
+
initRtk();
|
|
1475
1896
|
if (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true") {
|
|
1476
1897
|
try {
|
|
1477
1898
|
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
1478
1899
|
this.config = { ...this.config, workspaceDir: worktreePath };
|
|
1900
|
+
this.planSync.updateWorkspaceDir(worktreePath);
|
|
1479
1901
|
this.worktreeActive = true;
|
|
1480
1902
|
this.setupLog.push(`[conveyor] Using worktree: ${worktreePath}`);
|
|
1481
1903
|
} catch (error) {
|
|
@@ -1496,6 +1918,10 @@ var AgentRunner = class _AgentRunner {
|
|
|
1496
1918
|
return;
|
|
1497
1919
|
}
|
|
1498
1920
|
this.taskContext._runnerSessionId = randomUUID2();
|
|
1921
|
+
this.logEffectiveSettings();
|
|
1922
|
+
if (process.env.CODESPACES === "true") {
|
|
1923
|
+
unshallowRepo(this.config.workspaceDir);
|
|
1924
|
+
}
|
|
1499
1925
|
if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
|
|
1500
1926
|
const result = cleanDevcontainerFromGit(
|
|
1501
1927
|
this.config.workspaceDir,
|
|
@@ -1510,6 +1936,7 @@ var AgentRunner = class _AgentRunner {
|
|
|
1510
1936
|
try {
|
|
1511
1937
|
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
1512
1938
|
this.config = { ...this.config, workspaceDir: worktreePath };
|
|
1939
|
+
this.planSync.updateWorkspaceDir(worktreePath);
|
|
1513
1940
|
this.worktreeActive = true;
|
|
1514
1941
|
this.setupLog.push(`[conveyor] Using worktree (from task config): ${worktreePath}`);
|
|
1515
1942
|
} catch (error) {
|
|
@@ -1593,6 +2020,24 @@ The agent cannot start until this is resolved.`
|
|
|
1593
2020
|
return false;
|
|
1594
2021
|
}
|
|
1595
2022
|
}
|
|
2023
|
+
logEffectiveSettings() {
|
|
2024
|
+
if (!this.taskContext) return;
|
|
2025
|
+
const s = this.taskContext.agentSettings ?? this.config.agentSettings ?? {};
|
|
2026
|
+
const model = this.taskContext.model || this.config.model;
|
|
2027
|
+
const thinking = s.thinking?.type === "enabled" ? `enabled(${s.thinking.budgetTokens ?? "?"})` : s.thinking?.type ?? "default";
|
|
2028
|
+
const parts = [
|
|
2029
|
+
`model=${model}`,
|
|
2030
|
+
`mode=${this.config.mode ?? "task"}`,
|
|
2031
|
+
`effort=${s.effort ?? "default"}`,
|
|
2032
|
+
`thinking=${thinking}`,
|
|
2033
|
+
`maxBudget=$${s.maxBudgetUsd ?? 50}`,
|
|
2034
|
+
`maxTurns=${s.maxTurns ?? "unlimited"}`
|
|
2035
|
+
];
|
|
2036
|
+
if (s.betas?.length) parts.push(`betas=${s.betas.join(",")}`);
|
|
2037
|
+
if (s.disallowedTools?.length) parts.push(`disallowed=[${s.disallowedTools.join(",")}]`);
|
|
2038
|
+
if (s.enableFileCheckpointing) parts.push(`checkpointing=on`);
|
|
2039
|
+
console.log(`[conveyor-agent] ${parts.join(", ")}`);
|
|
2040
|
+
}
|
|
1596
2041
|
pushSetupLog(line) {
|
|
1597
2042
|
this.setupLog.push(line);
|
|
1598
2043
|
if (this.setupLog.length > _AgentRunner.MAX_SETUP_LOG_LINES) {
|
|
@@ -1617,13 +2062,6 @@ The agent cannot start until this is resolved.`
|
|
|
1617
2062
|
});
|
|
1618
2063
|
}
|
|
1619
2064
|
}
|
|
1620
|
-
initRtk() {
|
|
1621
|
-
try {
|
|
1622
|
-
execSync3("rtk --version", { stdio: "ignore" });
|
|
1623
|
-
execSync3("rtk init --global --auto-patch", { stdio: "ignore" });
|
|
1624
|
-
} catch {
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
2065
|
injectHumanMessage(content, files) {
|
|
1628
2066
|
let messageContent;
|
|
1629
2067
|
if (files?.length) {
|
|
@@ -1689,11 +2127,15 @@ ${f.content}
|
|
|
1689
2127
|
async waitForUserContent() {
|
|
1690
2128
|
if (this.pendingMessages.length > 0) {
|
|
1691
2129
|
const next = this.pendingMessages.shift();
|
|
1692
|
-
|
|
2130
|
+
const content2 = next?.message.content;
|
|
2131
|
+
if (!content2) return null;
|
|
2132
|
+
return content2;
|
|
1693
2133
|
}
|
|
1694
2134
|
const msg = await this.waitForMessage();
|
|
1695
2135
|
if (!msg) return null;
|
|
1696
|
-
|
|
2136
|
+
const content = msg.message.content;
|
|
2137
|
+
if (!content) return null;
|
|
2138
|
+
return content;
|
|
1697
2139
|
}
|
|
1698
2140
|
async *createInputStream(initialPrompt) {
|
|
1699
2141
|
const makeUserMessage = (content) => ({
|
|
@@ -1718,93 +2160,31 @@ ${f.content}
|
|
|
1718
2160
|
yield msg;
|
|
1719
2161
|
}
|
|
1720
2162
|
}
|
|
1721
|
-
getPlanDirs() {
|
|
1722
|
-
return [
|
|
1723
|
-
join3(homedir(), ".claude", "plans"),
|
|
1724
|
-
join3(this.config.workspaceDir, ".claude", "plans")
|
|
1725
|
-
];
|
|
1726
|
-
}
|
|
1727
|
-
/**
|
|
1728
|
-
* Snapshot current plan files so syncPlanFile can distinguish files created
|
|
1729
|
-
* by THIS session from ones created by a concurrent agent.
|
|
1730
|
-
*/
|
|
1731
|
-
snapshotPlanFiles() {
|
|
1732
|
-
this.planFileSnapshot.clear();
|
|
1733
|
-
this.lockedPlanFile = null;
|
|
1734
|
-
for (const plansDir of this.getPlanDirs()) {
|
|
1735
|
-
try {
|
|
1736
|
-
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1737
|
-
try {
|
|
1738
|
-
const fullPath = join3(plansDir, file);
|
|
1739
|
-
const stat = statSync(fullPath);
|
|
1740
|
-
this.planFileSnapshot.set(fullPath, stat.mtimeMs);
|
|
1741
|
-
} catch {
|
|
1742
|
-
continue;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
} catch {
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
syncPlanFile() {
|
|
1750
|
-
if (this.lockedPlanFile) {
|
|
1751
|
-
try {
|
|
1752
|
-
const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
|
|
1753
|
-
if (content) {
|
|
1754
|
-
this.connection.updateTaskFields({ plan: content });
|
|
1755
|
-
const fileName = this.lockedPlanFile.split("/").pop();
|
|
1756
|
-
this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
|
|
1757
|
-
}
|
|
1758
|
-
} catch {
|
|
1759
|
-
}
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
let newest = null;
|
|
1763
|
-
for (const plansDir of this.getPlanDirs()) {
|
|
1764
|
-
let files;
|
|
1765
|
-
try {
|
|
1766
|
-
files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
|
|
1767
|
-
} catch {
|
|
1768
|
-
continue;
|
|
1769
|
-
}
|
|
1770
|
-
for (const file of files) {
|
|
1771
|
-
const fullPath = join3(plansDir, file);
|
|
1772
|
-
try {
|
|
1773
|
-
const stat = statSync(fullPath);
|
|
1774
|
-
const prevMtime = this.planFileSnapshot.get(fullPath);
|
|
1775
|
-
const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
|
|
1776
|
-
if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
|
|
1777
|
-
newest = { path: fullPath, mtime: stat.mtimeMs };
|
|
1778
|
-
}
|
|
1779
|
-
} catch {
|
|
1780
|
-
continue;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
if (newest) {
|
|
1785
|
-
this.lockedPlanFile = newest.path;
|
|
1786
|
-
const content = readFileSync(newest.path, "utf-8").trim();
|
|
1787
|
-
if (content) {
|
|
1788
|
-
this.connection.updateTaskFields({ plan: content });
|
|
1789
|
-
const fileName = newest.path.split("/").pop();
|
|
1790
|
-
this.connection.postChatMessage(
|
|
1791
|
-
`Detected local plan file (${fileName}) and synced it to the task plan.`
|
|
1792
|
-
);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
2163
|
asQueryHost() {
|
|
2164
|
+
const getActiveMode = () => this.activeMode;
|
|
1797
2165
|
return {
|
|
1798
2166
|
config: this.config,
|
|
1799
2167
|
connection: this.connection,
|
|
1800
2168
|
callbacks: this.callbacks,
|
|
1801
2169
|
setupLog: this.setupLog,
|
|
2170
|
+
costTracker: this.costTracker,
|
|
2171
|
+
get activeMode() {
|
|
2172
|
+
return getActiveMode();
|
|
2173
|
+
},
|
|
1802
2174
|
isStopped: () => this.stopped,
|
|
1803
2175
|
createInputStream: (prompt) => this.createInputStream(prompt),
|
|
1804
|
-
snapshotPlanFiles: () => this.snapshotPlanFiles(),
|
|
1805
|
-
syncPlanFile: () => this.syncPlanFile()
|
|
2176
|
+
snapshotPlanFiles: () => this.planSync.snapshotPlanFiles(),
|
|
2177
|
+
syncPlanFile: () => this.planSync.syncPlanFile()
|
|
1806
2178
|
};
|
|
1807
2179
|
}
|
|
2180
|
+
handleModeChange(mode) {
|
|
2181
|
+
if (this.config.mode !== "pm") return;
|
|
2182
|
+
this.activeMode = mode;
|
|
2183
|
+
this.connection.emitModeChanged(mode);
|
|
2184
|
+
this.connection.postChatMessage(
|
|
2185
|
+
`Mode switched to **${mode}**${mode === "active" ? " \u2014 I now have direct coding access." : " \u2014 back to planning only."}`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
1808
2188
|
stop() {
|
|
1809
2189
|
this.stopped = true;
|
|
1810
2190
|
if (this.inputResolver) {
|
|
@@ -1814,163 +2194,13 @@ ${f.content}
|
|
|
1814
2194
|
}
|
|
1815
2195
|
};
|
|
1816
2196
|
|
|
1817
|
-
// src/project-
|
|
1818
|
-
import { io as io2 } from "socket.io-client";
|
|
1819
|
-
var ProjectConnection = class {
|
|
1820
|
-
socket = null;
|
|
1821
|
-
config;
|
|
1822
|
-
taskAssignmentCallback = null;
|
|
1823
|
-
stopTaskCallback = null;
|
|
1824
|
-
shutdownCallback = null;
|
|
1825
|
-
chatMessageCallback = null;
|
|
1826
|
-
earlyChatMessages = [];
|
|
1827
|
-
constructor(config) {
|
|
1828
|
-
this.config = config;
|
|
1829
|
-
}
|
|
1830
|
-
connect() {
|
|
1831
|
-
return new Promise((resolve2, reject) => {
|
|
1832
|
-
let settled = false;
|
|
1833
|
-
let attempts = 0;
|
|
1834
|
-
const maxInitialAttempts = 30;
|
|
1835
|
-
this.socket = io2(this.config.apiUrl, {
|
|
1836
|
-
auth: { projectToken: this.config.projectToken },
|
|
1837
|
-
transports: ["websocket"],
|
|
1838
|
-
reconnection: true,
|
|
1839
|
-
reconnectionAttempts: Infinity,
|
|
1840
|
-
reconnectionDelay: 2e3,
|
|
1841
|
-
reconnectionDelayMax: 3e4,
|
|
1842
|
-
randomizationFactor: 0.3,
|
|
1843
|
-
extraHeaders: {
|
|
1844
|
-
"ngrok-skip-browser-warning": "true"
|
|
1845
|
-
}
|
|
1846
|
-
});
|
|
1847
|
-
this.socket.on("projectRunner:assignTask", (data) => {
|
|
1848
|
-
if (this.taskAssignmentCallback) {
|
|
1849
|
-
this.taskAssignmentCallback(data);
|
|
1850
|
-
}
|
|
1851
|
-
});
|
|
1852
|
-
this.socket.on("projectRunner:stopTask", (data) => {
|
|
1853
|
-
if (this.stopTaskCallback) {
|
|
1854
|
-
this.stopTaskCallback(data);
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
this.socket.on("projectRunner:shutdown", () => {
|
|
1858
|
-
if (this.shutdownCallback) {
|
|
1859
|
-
this.shutdownCallback();
|
|
1860
|
-
}
|
|
1861
|
-
});
|
|
1862
|
-
this.socket.on("projectRunner:incomingChatMessage", (msg) => {
|
|
1863
|
-
if (this.chatMessageCallback) {
|
|
1864
|
-
this.chatMessageCallback(msg);
|
|
1865
|
-
} else {
|
|
1866
|
-
this.earlyChatMessages.push(msg);
|
|
1867
|
-
}
|
|
1868
|
-
});
|
|
1869
|
-
this.socket.on("connect", () => {
|
|
1870
|
-
if (!settled) {
|
|
1871
|
-
settled = true;
|
|
1872
|
-
resolve2();
|
|
1873
|
-
}
|
|
1874
|
-
});
|
|
1875
|
-
this.socket.io.on("reconnect_attempt", () => {
|
|
1876
|
-
attempts++;
|
|
1877
|
-
if (!settled && attempts >= maxInitialAttempts) {
|
|
1878
|
-
settled = true;
|
|
1879
|
-
reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
onTaskAssignment(callback) {
|
|
1885
|
-
this.taskAssignmentCallback = callback;
|
|
1886
|
-
}
|
|
1887
|
-
onStopTask(callback) {
|
|
1888
|
-
this.stopTaskCallback = callback;
|
|
1889
|
-
}
|
|
1890
|
-
onShutdown(callback) {
|
|
1891
|
-
this.shutdownCallback = callback;
|
|
1892
|
-
}
|
|
1893
|
-
onChatMessage(callback) {
|
|
1894
|
-
this.chatMessageCallback = callback;
|
|
1895
|
-
for (const msg of this.earlyChatMessages) {
|
|
1896
|
-
callback(msg);
|
|
1897
|
-
}
|
|
1898
|
-
this.earlyChatMessages = [];
|
|
1899
|
-
}
|
|
1900
|
-
sendHeartbeat() {
|
|
1901
|
-
if (!this.socket) return;
|
|
1902
|
-
this.socket.emit("projectRunner:heartbeat", {});
|
|
1903
|
-
}
|
|
1904
|
-
emitTaskStarted(taskId) {
|
|
1905
|
-
if (!this.socket) return;
|
|
1906
|
-
this.socket.emit("projectRunner:taskStarted", { taskId });
|
|
1907
|
-
}
|
|
1908
|
-
emitTaskStopped(taskId, reason) {
|
|
1909
|
-
if (!this.socket) return;
|
|
1910
|
-
this.socket.emit("projectRunner:taskStopped", { taskId, reason });
|
|
1911
|
-
}
|
|
1912
|
-
emitEvent(event) {
|
|
1913
|
-
if (!this.socket) return;
|
|
1914
|
-
this.socket.emit("conveyor:projectAgentEvent", event);
|
|
1915
|
-
}
|
|
1916
|
-
emitChatMessage(content) {
|
|
1917
|
-
const socket = this.socket;
|
|
1918
|
-
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
1919
|
-
return new Promise((resolve2, reject) => {
|
|
1920
|
-
socket.emit(
|
|
1921
|
-
"conveyor:projectAgentChatMessage",
|
|
1922
|
-
{ content },
|
|
1923
|
-
(response) => {
|
|
1924
|
-
if (response.success) resolve2();
|
|
1925
|
-
else reject(new Error(response.error ?? "Failed to send chat message"));
|
|
1926
|
-
}
|
|
1927
|
-
);
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
emitAgentStatus(status) {
|
|
1931
|
-
if (!this.socket) return;
|
|
1932
|
-
this.socket.emit("conveyor:projectAgentStatus", { status });
|
|
1933
|
-
}
|
|
1934
|
-
fetchAgentContext() {
|
|
1935
|
-
const socket = this.socket;
|
|
1936
|
-
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
1937
|
-
return new Promise((resolve2, reject) => {
|
|
1938
|
-
socket.emit(
|
|
1939
|
-
"projectRunner:getAgentContext",
|
|
1940
|
-
(response) => {
|
|
1941
|
-
if (response.success) resolve2(response.data ?? null);
|
|
1942
|
-
else reject(new Error(response.error ?? "Failed to fetch agent context"));
|
|
1943
|
-
}
|
|
1944
|
-
);
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
fetchChatHistory(limit) {
|
|
1948
|
-
const socket = this.socket;
|
|
1949
|
-
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
1950
|
-
return new Promise((resolve2, reject) => {
|
|
1951
|
-
socket.emit(
|
|
1952
|
-
"projectRunner:getChatHistory",
|
|
1953
|
-
{ limit },
|
|
1954
|
-
(response) => {
|
|
1955
|
-
if (response.success && response.data) resolve2(response.data);
|
|
1956
|
-
else reject(new Error(response.error ?? "Failed to fetch chat history"));
|
|
1957
|
-
}
|
|
1958
|
-
);
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
disconnect() {
|
|
1962
|
-
this.socket?.disconnect();
|
|
1963
|
-
this.socket = null;
|
|
1964
|
-
}
|
|
1965
|
-
};
|
|
1966
|
-
|
|
1967
|
-
// src/project-runner.ts
|
|
2197
|
+
// src/runner/project-runner.ts
|
|
1968
2198
|
import { fork } from "child_process";
|
|
1969
2199
|
import { execSync as execSync4 } from "child_process";
|
|
1970
2200
|
import * as path from "path";
|
|
1971
2201
|
import { fileURLToPath } from "url";
|
|
1972
2202
|
|
|
1973
|
-
// src/project-chat-handler.ts
|
|
2203
|
+
// src/runner/project-chat-handler.ts
|
|
1974
2204
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1975
2205
|
var FALLBACK_MODEL = "claude-sonnet-4-20250514";
|
|
1976
2206
|
function buildSystemPrompt2(projectDir, agentCtx) {
|
|
@@ -2056,33 +2286,31 @@ async function handleProjectChatMessage(message, connection, projectDir) {
|
|
|
2056
2286
|
const turnToolCalls = [];
|
|
2057
2287
|
let isTyping = false;
|
|
2058
2288
|
for await (const event of events) {
|
|
2059
|
-
|
|
2060
|
-
if (eventType === "assistant") {
|
|
2289
|
+
if (event.type === "assistant") {
|
|
2061
2290
|
if (!isTyping) {
|
|
2062
2291
|
setTimeout(() => connection.emitEvent({ type: "agent_typing_start" }), 200);
|
|
2063
2292
|
isTyping = true;
|
|
2064
2293
|
}
|
|
2065
|
-
const
|
|
2066
|
-
const content =
|
|
2294
|
+
const assistantEvent = event;
|
|
2295
|
+
const { content } = assistantEvent.message;
|
|
2067
2296
|
for (const block of content) {
|
|
2068
2297
|
if (block.type === "text") {
|
|
2069
2298
|
responseParts.push(block.text);
|
|
2070
2299
|
} else if (block.type === "tool_use") {
|
|
2071
|
-
const name = block.name;
|
|
2072
2300
|
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
2073
2301
|
turnToolCalls.push({
|
|
2074
|
-
tool: name,
|
|
2302
|
+
tool: block.name,
|
|
2075
2303
|
input: inputStr.slice(0, 1e4),
|
|
2076
2304
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2077
2305
|
});
|
|
2078
|
-
console.log(`[project-chat] [tool_use] ${name}`);
|
|
2306
|
+
console.log(`[project-chat] [tool_use] ${block.name}`);
|
|
2079
2307
|
}
|
|
2080
2308
|
}
|
|
2081
2309
|
if (turnToolCalls.length > 0) {
|
|
2082
2310
|
connection.emitEvent({ type: "activity_block", events: [...turnToolCalls] });
|
|
2083
2311
|
turnToolCalls.length = 0;
|
|
2084
2312
|
}
|
|
2085
|
-
} else if (
|
|
2313
|
+
} else if (event.type === "result") {
|
|
2086
2314
|
if (isTyping) {
|
|
2087
2315
|
connection.emitEvent({ type: "agent_typing_stop" });
|
|
2088
2316
|
isTyping = false;
|
|
@@ -2113,7 +2341,7 @@ async function handleProjectChatMessage(message, connection, projectDir) {
|
|
|
2113
2341
|
}
|
|
2114
2342
|
}
|
|
2115
2343
|
|
|
2116
|
-
// src/project-runner.ts
|
|
2344
|
+
// src/runner/project-runner.ts
|
|
2117
2345
|
var __filename = fileURLToPath(import.meta.url);
|
|
2118
2346
|
var __dirname = path.dirname(__filename);
|
|
2119
2347
|
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
@@ -2300,15 +2528,80 @@ var ProjectRunner = class {
|
|
|
2300
2528
|
}
|
|
2301
2529
|
};
|
|
2302
2530
|
|
|
2531
|
+
// src/runner/file-cache.ts
|
|
2532
|
+
var DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024;
|
|
2533
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
2534
|
+
var FileCache = class {
|
|
2535
|
+
cache = /* @__PURE__ */ new Map();
|
|
2536
|
+
currentSize = 0;
|
|
2537
|
+
maxSizeBytes;
|
|
2538
|
+
ttlMs;
|
|
2539
|
+
constructor(maxSizeBytes = DEFAULT_MAX_SIZE_BYTES, ttlMs = DEFAULT_TTL_MS) {
|
|
2540
|
+
this.maxSizeBytes = maxSizeBytes;
|
|
2541
|
+
this.ttlMs = ttlMs;
|
|
2542
|
+
}
|
|
2543
|
+
get(fileId) {
|
|
2544
|
+
const entry = this.cache.get(fileId);
|
|
2545
|
+
if (!entry) return null;
|
|
2546
|
+
if (Date.now() - entry.createdAt > this.ttlMs) {
|
|
2547
|
+
this.delete(fileId);
|
|
2548
|
+
return null;
|
|
2549
|
+
}
|
|
2550
|
+
this.cache.delete(fileId);
|
|
2551
|
+
this.cache.set(fileId, entry);
|
|
2552
|
+
return entry;
|
|
2553
|
+
}
|
|
2554
|
+
set(fileId, content, mimeType, fileName) {
|
|
2555
|
+
if (this.cache.has(fileId)) {
|
|
2556
|
+
this.delete(fileId);
|
|
2557
|
+
}
|
|
2558
|
+
const size = content.byteLength;
|
|
2559
|
+
if (size > this.maxSizeBytes) return;
|
|
2560
|
+
while (this.currentSize + size > this.maxSizeBytes && this.cache.size > 0) {
|
|
2561
|
+
const oldestKey = this.cache.keys().next().value;
|
|
2562
|
+
if (oldestKey !== void 0) {
|
|
2563
|
+
this.delete(oldestKey);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
this.cache.set(fileId, {
|
|
2567
|
+
content,
|
|
2568
|
+
mimeType,
|
|
2569
|
+
fileName,
|
|
2570
|
+
createdAt: Date.now(),
|
|
2571
|
+
size
|
|
2572
|
+
});
|
|
2573
|
+
this.currentSize += size;
|
|
2574
|
+
}
|
|
2575
|
+
delete(fileId) {
|
|
2576
|
+
const entry = this.cache.get(fileId);
|
|
2577
|
+
if (entry) {
|
|
2578
|
+
this.currentSize -= entry.size;
|
|
2579
|
+
this.cache.delete(fileId);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
clear() {
|
|
2583
|
+
this.cache.clear();
|
|
2584
|
+
this.currentSize = 0;
|
|
2585
|
+
}
|
|
2586
|
+
get stats() {
|
|
2587
|
+
return {
|
|
2588
|
+
entries: this.cache.size,
|
|
2589
|
+
sizeBytes: this.currentSize,
|
|
2590
|
+
maxSizeBytes: this.maxSizeBytes
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2303
2595
|
export {
|
|
2304
2596
|
ConveyorConnection,
|
|
2597
|
+
ProjectConnection,
|
|
2305
2598
|
loadConveyorConfig,
|
|
2306
2599
|
runSetupCommand,
|
|
2307
2600
|
runStartCommand,
|
|
2308
2601
|
ensureWorktree,
|
|
2309
2602
|
removeWorktree,
|
|
2310
2603
|
AgentRunner,
|
|
2311
|
-
|
|
2312
|
-
|
|
2604
|
+
ProjectRunner,
|
|
2605
|
+
FileCache
|
|
2313
2606
|
};
|
|
2314
|
-
//# sourceMappingURL=chunk-
|
|
2607
|
+
//# sourceMappingURL=chunk-5UYKPYFQ.js.map
|