@rallycry/conveyor-agent 2.18.0 → 3.1.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.
@@ -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
- "agentRunner:incomingMessage",
36
- (msg) => {
37
- if (this.chatMessageCallback) {
38
- this.chatMessageCallback(msg);
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
- "agentRunner:questionAnswer",
53
- (data) => {
54
- const resolver = this.pendingQuestionResolvers.get(data.requestId);
55
- if (resolver) {
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
- { taskId: this.config.taskId, limit },
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
- { taskId: this.config.taskId },
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
- { taskId: this.config.taskId, fileId },
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
- { taskId: this.config.taskId },
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({ taskId: this.config.taskId, event });
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
- { taskId: this.config.taskId, ...params },
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
- "agentRunner:createSubtask",
267
- data,
268
- (response) => {
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
- "agentRunner:listSubtasks",
292
- {},
293
- (response) => {
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
- { taskId: this.config.taskId, slugOrId },
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/setup.ts
325
- import { execSync } from "child_process";
326
- import { spawn } from "child_process";
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} -- ${DEVCONTAINER_PATH}`);
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,10 +546,10 @@ 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} -- ${DEVCONTAINER_PATH}`);
416
- git(`git add ${DEVCONTAINER_PATH}`);
549
+ git(`git checkout origin/${baseBranch} -- ${DEVCONTAINER_PATH2}`);
550
+ git(`git add ${DEVCONTAINER_PATH2}`);
417
551
  try {
418
- git(`git diff --cached --quiet -- ${DEVCONTAINER_PATH}`);
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"`);
@@ -436,8 +570,25 @@ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
436
570
  return { cleaned: false, message: `Git cleanup failed: ${msg}` };
437
571
  }
438
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
+ }
439
590
 
440
- // src/worktree.ts
591
+ // src/runner/worktree.ts
441
592
  import { execSync as execSync2 } from "child_process";
442
593
  import { existsSync } from "fs";
443
594
  import { join as join2 } from "path";
@@ -454,39 +605,194 @@ function ensureWorktree(projectDir, taskId, branch) {
454
605
  } catch {
455
606
  }
456
607
  }
457
- return worktreePath;
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;
780
+ }
781
+ }
458
782
  }
459
- const ref = branch ? `origin/${branch}` : "HEAD";
460
- execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
461
- cwd: projectDir,
462
- stdio: "ignore"
463
- });
464
- return worktreePath;
465
- }
466
- function removeWorktree(projectDir, taskId) {
467
- const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
468
- if (!existsSync(worktreePath)) return;
469
- try {
470
- execSync2(`git worktree remove "${worktreePath}" --force`, {
471
- cwd: projectDir,
472
- stdio: "ignore"
473
- });
474
- } catch {
783
+ if (isTyping) {
784
+ host.connection.sendTypingStop();
475
785
  }
786
+ return { retriable };
476
787
  }
477
788
 
478
- // src/runner.ts
479
- import { randomUUID as randomUUID2 } from "crypto";
480
- import { execSync as execSync3 } from "child_process";
481
- import { readdirSync, statSync, readFileSync } from "fs";
482
- import { homedir } from "os";
483
- import { join as join3 } from "path";
484
-
485
- // src/query-executor.ts
789
+ // src/execution/query-executor.ts
486
790
  import { randomUUID } from "crypto";
487
- import { query } from "@anthropic-ai/claude-agent-sdk";
791
+ import {
792
+ query
793
+ } from "@anthropic-ai/claude-agent-sdk";
488
794
 
489
- // src/prompt-builder.ts
795
+ // src/execution/prompt-builder.ts
490
796
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
491
797
  function formatFileSize(bytes) {
492
798
  if (bytes === void 0) return "";
@@ -556,7 +862,7 @@ Address the requested changes. Do NOT re-investigate the codebase from scratch o
556
862
  `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
557
863
  `Run \`git log --oneline -10\` to review what you already committed.`,
558
864
  `Review the current state of the codebase and verify everything is working correctly.`,
559
- `Post a brief status update to the chat, then wait for further instructions.`
865
+ `Reply with a brief status update (visible in chat), then wait for further instructions.`
560
866
  );
561
867
  if (context.githubPRUrl) {
562
868
  parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
@@ -638,20 +944,20 @@ function buildInstructions(mode, context, scenario) {
638
944
  `You are the project manager for this task and its subtasks.`,
639
945
  `Use list_subtasks to review the current state of child tasks.`,
640
946
  `The task details are provided above. Wait for the team to provide instructions before taking action.`,
641
- `When you finish planning, save the plan with update_task, post a summary to chat, and end your turn.`
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.`
642
948
  );
643
949
  } else if (isPm) {
644
950
  parts.push(
645
951
  `You are the project manager for this task.`,
646
952
  `The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`,
647
- `When you finish planning, save the plan with update_task, post a summary to chat, and end your turn. A separate task agent will execute the plan after review.`
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.`
648
954
  );
649
955
  } else {
650
956
  parts.push(
651
957
  `Begin executing the task plan above immediately.`,
652
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.`,
653
959
  `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
654
- `Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
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.`,
655
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.`
656
962
  );
657
963
  }
@@ -667,7 +973,7 @@ function buildInstructions(mode, context, scenario) {
667
973
  `You were relaunched but no new instructions have been given since your last run.`,
668
974
  `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
669
975
  `Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
670
- `Post a brief status update to the chat summarizing where things stand.`,
976
+ `Reply with a brief status update summarizing where things stand (visible in chat).`,
671
977
  `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
672
978
  );
673
979
  if (context.githubPRUrl) {
@@ -720,8 +1026,9 @@ function buildInitialPrompt(mode, context) {
720
1026
  const instructions = buildInstructions(mode, context, scenario);
721
1027
  return [...body, ...instructions].join("\n");
722
1028
  }
723
- function buildSystemPrompt(mode, context, config, setupLog) {
1029
+ function buildSystemPrompt(mode, context, config, setupLog, pmSubMode = "planning") {
724
1030
  const isPm = mode === "pm";
1031
+ const isPmActive = isPm && pmSubMode === "active";
725
1032
  const pmParts = [
726
1033
  `You are an AI project manager helping to plan tasks for the "${context.title}" project.`,
727
1034
  `You are running locally with full access to the repository.`,
@@ -735,7 +1042,7 @@ Environment (ready, no setup required):`,
735
1042
  Workflow:`,
736
1043
  `- You can draft and iterate on plans in .claude/plans/*.md \u2014 these files are automatically synced to the task.`,
737
1044
  `- You can also use update_task directly to save the plan to the task.`,
738
- `- After saving the plan, post a summary to chat and end your turn. Do NOT attempt to execute the plan yourself.`,
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.`,
739
1046
  `- A separate task agent will handle execution after the team reviews and approves your plan.`
740
1047
  ];
741
1048
  if (isPm && context.isParentTask) {
@@ -764,7 +1071,30 @@ Project Agents:`);
764
1071
  pmParts.push(`- ${pa.agent.name} (${role}${sp})`);
765
1072
  }
766
1073
  }
767
- const parts = isPm ? pmParts : [
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 : [
768
1098
  `You are an AI agent working on a task for the "${context.title}" project.`,
769
1099
  `You are running inside a GitHub Codespace with full access to the repository.`,
770
1100
  `
@@ -810,11 +1140,12 @@ ${config.instructions}`);
810
1140
  }
811
1141
  parts.push(
812
1142
  `
813
- You have access to Conveyor MCP tools to interact with the task management system.`,
814
- `Use the post_to_chat tool to communicate progress or ask questions.`,
815
- `Use the read_task_chat tool to check for new messages from the team.`
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.`
816
1147
  );
817
- if (!isPm) {
1148
+ if (!isPm || isPmActive) {
818
1149
  parts.push(
819
1150
  `Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
820
1151
  );
@@ -822,8 +1153,11 @@ You have access to Conveyor MCP tools to interact with the task management syste
822
1153
  return parts.join("\n");
823
1154
  }
824
1155
 
825
- // src/mcp-tools.ts
826
- import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
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";
827
1161
  import { z } from "zod";
828
1162
  function buildCommonTools(connection, config) {
829
1163
  return [
@@ -849,7 +1183,7 @@ function buildCommonTools(connection, config) {
849
1183
  ),
850
1184
  tool(
851
1185
  "post_to_chat",
852
- "Post a message to the task chat visible to all team members",
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",
853
1187
  { message: z.string().describe("The message to post to the team") },
854
1188
  ({ message }) => {
855
1189
  connection.postChatMessage(message);
@@ -931,6 +1265,10 @@ function buildCommonTools(connection, config) {
931
1265
  )
932
1266
  ];
933
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";
934
1272
  function buildStoryPointDescription(storyPoints) {
935
1273
  if (storyPoints && storyPoints.length > 0) {
936
1274
  const tiers = storyPoints.map((sp) => `${sp.value}=${sp.name}`).join(", ");
@@ -941,12 +1279,12 @@ function buildStoryPointDescription(storyPoints) {
941
1279
  function buildPmTools(connection, storyPoints) {
942
1280
  const spDescription = buildStoryPointDescription(storyPoints);
943
1281
  return [
944
- tool(
1282
+ tool2(
945
1283
  "update_task",
946
1284
  "Save the finalized task plan and/or description",
947
1285
  {
948
- plan: z.string().optional().describe("The task plan in markdown"),
949
- description: z.string().optional().describe("Updated task description")
1286
+ plan: z2.string().optional().describe("The task plan in markdown"),
1287
+ description: z2.string().optional().describe("Updated task description")
950
1288
  },
951
1289
  async ({ plan, description }) => {
952
1290
  try {
@@ -957,15 +1295,15 @@ function buildPmTools(connection, storyPoints) {
957
1295
  }
958
1296
  }
959
1297
  ),
960
- tool(
1298
+ tool2(
961
1299
  "create_subtask",
962
1300
  "Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
963
1301
  {
964
- title: z.string().describe("Subtask title"),
965
- description: z.string().optional().describe("Brief description"),
966
- plan: z.string().optional().describe("Implementation plan in markdown"),
967
- ordinal: z.number().optional().describe("Step/order number (0-based)"),
968
- storyPointValue: z.number().optional().describe(spDescription)
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)
969
1307
  },
970
1308
  async (params) => {
971
1309
  try {
@@ -978,16 +1316,16 @@ function buildPmTools(connection, storyPoints) {
978
1316
  }
979
1317
  }
980
1318
  ),
981
- tool(
1319
+ tool2(
982
1320
  "update_subtask",
983
1321
  "Update an existing subtask's fields",
984
1322
  {
985
- subtaskId: z.string().describe("The subtask ID to update"),
986
- title: z.string().optional(),
987
- description: z.string().optional(),
988
- plan: z.string().optional(),
989
- ordinal: z.number().optional(),
990
- storyPointValue: z.number().optional().describe(spDescription)
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)
991
1329
  },
992
1330
  async ({ subtaskId, ...fields }) => {
993
1331
  try {
@@ -998,10 +1336,10 @@ function buildPmTools(connection, storyPoints) {
998
1336
  }
999
1337
  }
1000
1338
  ),
1001
- tool(
1339
+ tool2(
1002
1340
  "delete_subtask",
1003
1341
  "Delete a subtask",
1004
- { subtaskId: z.string().describe("The subtask ID to delete") },
1342
+ { subtaskId: z2.string().describe("The subtask ID to delete") },
1005
1343
  async ({ subtaskId }) => {
1006
1344
  try {
1007
1345
  await Promise.resolve(connection.deleteSubtask(subtaskId));
@@ -1011,7 +1349,7 @@ function buildPmTools(connection, storyPoints) {
1011
1349
  }
1012
1350
  }
1013
1351
  ),
1014
- tool(
1352
+ tool2(
1015
1353
  "list_subtasks",
1016
1354
  "List all subtasks under the current parent task",
1017
1355
  {},
@@ -1027,14 +1365,18 @@ function buildPmTools(connection, storyPoints) {
1027
1365
  )
1028
1366
  ];
1029
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";
1030
1372
  function buildTaskTools(connection) {
1031
1373
  return [
1032
- tool(
1374
+ tool3(
1033
1375
  "create_pull_request",
1034
1376
  "Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
1035
1377
  {
1036
- title: z.string().describe("The PR title"),
1037
- body: z.string().describe("The PR description/body in markdown")
1378
+ title: z3.string().describe("The PR title"),
1379
+ body: z3.string().describe("The PR description/body in markdown")
1038
1380
  },
1039
1381
  async ({ title, body }) => {
1040
1382
  try {
@@ -1053,6 +1395,8 @@ function buildTaskTools(connection) {
1053
1395
  )
1054
1396
  ];
1055
1397
  }
1398
+
1399
+ // src/tools/index.ts
1056
1400
  function textResult(text) {
1057
1401
  return { content: [{ type: "text", text }] };
1058
1402
  }
@@ -1065,144 +1409,17 @@ function createConveyorMcpServer(connection, config, context) {
1065
1409
  });
1066
1410
  }
1067
1411
 
1068
- // src/query-executor.ts
1069
- var API_ERROR_PATTERN = /API Error: [45]\d\d/;
1412
+ // src/execution/query-executor.ts
1413
+ var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
1070
1414
  var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
1071
1415
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
1072
- async function processAssistantEvent(event, host, turnToolCalls) {
1073
- const msg = event.message;
1074
- const content = msg.content;
1075
- const turnTextParts = [];
1076
- for (const block of content) {
1077
- const blockType = block.type;
1078
- if (blockType === "text") {
1079
- const text = block.text;
1080
- turnTextParts.push(text);
1081
- host.connection.sendEvent({ type: "message", content: text });
1082
- await host.callbacks.onEvent({ type: "message", content: text });
1083
- } else if (blockType === "tool_use") {
1084
- const name = block.name;
1085
- const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
1086
- const isContentTool = ["edit", "write"].includes(name.toLowerCase());
1087
- const inputLimit = isContentTool ? 1e4 : 500;
1088
- const summary = {
1089
- tool: name,
1090
- input: inputStr.slice(0, inputLimit),
1091
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1092
- };
1093
- turnToolCalls.push(summary);
1094
- host.connection.sendEvent({ type: "tool_use", tool: name, input: inputStr });
1095
- await host.callbacks.onEvent({ type: "tool_use", tool: name, input: inputStr });
1096
- }
1097
- }
1098
- if (turnTextParts.length > 0) {
1099
- host.connection.postChatMessage(turnTextParts.join("\n\n"));
1100
- }
1101
- if (turnToolCalls.length > 0) {
1102
- host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
1103
- turnToolCalls.length = 0;
1104
- }
1105
- }
1106
- function handleResultEvent(event, host, context, startTime) {
1107
- const resultEvent = event;
1108
- let totalCostUsd = 0;
1109
- let retriable = false;
1110
- if (resultEvent.subtype === "success") {
1111
- totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
1112
- const durationMs = Date.now() - startTime;
1113
- const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
1114
- if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
1115
- retriable = true;
1116
- }
1117
- host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
1118
- if (totalCostUsd > 0 && context.agentId && context._runnerSessionId) {
1119
- host.connection.trackSpending({
1120
- agentId: context.agentId,
1121
- sessionId: context._runnerSessionId,
1122
- totalCostUsd,
1123
- onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
1124
- });
1125
- }
1126
- } else {
1127
- const errors = "errors" in resultEvent ? resultEvent.errors : [];
1128
- const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
1129
- if (API_ERROR_PATTERN.test(errorMsg)) {
1130
- retriable = true;
1131
- }
1132
- host.connection.sendEvent({ type: "error", message: errorMsg });
1133
- }
1134
- return { totalCostUsd, retriable };
1135
- }
1136
- async function emitResultEvent(event, host, context, startTime) {
1137
- const result = handleResultEvent(event, host, context, startTime);
1138
- const durationMs = Date.now() - startTime;
1139
- const resultEvent = event;
1140
- if (resultEvent.subtype === "success") {
1141
- const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
1142
- await host.callbacks.onEvent({
1143
- type: "completed",
1144
- summary,
1145
- costUsd: result.totalCostUsd,
1146
- durationMs
1147
- });
1148
- } else {
1149
- const errors = "errors" in resultEvent ? resultEvent.errors : [];
1150
- const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
1151
- await host.callbacks.onEvent({ type: "error", message: errorMsg });
1152
- }
1153
- return result.retriable;
1154
- }
1155
- async function processEvents(events, context, host) {
1156
- const startTime = Date.now();
1157
- let sessionIdStored = false;
1158
- let isTyping = false;
1159
- let retriable = false;
1160
- const turnToolCalls = [];
1161
- for await (const event of events) {
1162
- if (host.isStopped()) break;
1163
- switch (event.type) {
1164
- case "system": {
1165
- if (event.subtype === "init") {
1166
- const sessionId = event.session_id;
1167
- if (sessionId && !sessionIdStored) {
1168
- sessionIdStored = true;
1169
- host.connection.storeSessionId(sessionId);
1170
- context.claudeSessionId = sessionId;
1171
- }
1172
- await host.callbacks.onEvent({
1173
- type: "thinking",
1174
- message: `Agent initialized (model: ${event.model})`
1175
- });
1176
- }
1177
- break;
1178
- }
1179
- case "assistant": {
1180
- if (!isTyping) {
1181
- setTimeout(() => host.connection.sendTypingStart(), 200);
1182
- isTyping = true;
1183
- }
1184
- await processAssistantEvent(event, host, turnToolCalls);
1185
- break;
1186
- }
1187
- case "result": {
1188
- if (isTyping) {
1189
- host.connection.sendTypingStop();
1190
- isTyping = false;
1191
- }
1192
- retriable = await emitResultEvent(event, host, context, startTime);
1193
- break;
1194
- }
1195
- }
1196
- }
1197
- if (isTyping) {
1198
- host.connection.sendTypingStop();
1199
- }
1200
- return { retriable };
1201
- }
1416
+ var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
1202
1417
  function buildCanUseTool(host) {
1203
1418
  const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
1204
1419
  return async (toolName, input) => {
1205
- if (host.config.mode === "pm" && PM_PLAN_FILE_TOOLS.has(toolName)) {
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)) {
1206
1423
  const filePath = String(input.file_path ?? input.path ?? "");
1207
1424
  if (filePath.includes(".claude/plans/")) {
1208
1425
  return { behavior: "allow", updatedInput: input };
@@ -1212,6 +1429,15 @@ function buildCanUseTool(host) {
1212
1429
  message: "File write tools are only available for plan files in PM mode."
1213
1430
  };
1214
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
+ }
1215
1441
  if (toolName !== "AskUserQuestion") {
1216
1442
  return { behavior: "allow", updatedInput: input };
1217
1443
  }
@@ -1240,13 +1466,41 @@ function buildCanUseTool(host) {
1240
1466
  }
1241
1467
  function buildQueryOptions(host, context) {
1242
1468
  const settings = context.agentSettings ?? host.config.agentSettings ?? {};
1243
- const systemPromptText = buildSystemPrompt(host.config.mode, context, host.config, host.setupLog);
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
+ );
1244
1477
  const conveyorMcp = createConveyorMcpServer(host.connection, host.config, context);
1245
1478
  const isPm = host.config.mode === "pm";
1246
- const pmDisallowedTools = isPm ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
1479
+ const pmDisallowedTools = isPm && !isPmActive ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
1247
1480
  const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
1248
1481
  const settingSources = settings.settingSources ?? ["user", "project"];
1249
- return {
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 = {
1250
1504
  model: context.model || host.config.model,
1251
1505
  systemPrompt: {
1252
1506
  type: "preset",
@@ -1255,11 +1509,12 @@ function buildQueryOptions(host, context) {
1255
1509
  },
1256
1510
  settingSources,
1257
1511
  cwd: host.config.workspaceDir,
1258
- permissionMode: "bypassPermissions",
1259
- allowDangerouslySkipPermissions: true,
1512
+ permissionMode: isPmActive ? "acceptEdits" : "bypassPermissions",
1513
+ allowDangerouslySkipPermissions: !isPmActive,
1260
1514
  canUseTool: buildCanUseTool(host),
1261
1515
  tools: { type: "preset", preset: "claude_code" },
1262
1516
  mcpServers: { conveyor: conveyorMcp },
1517
+ hooks,
1263
1518
  maxTurns: settings.maxTurns,
1264
1519
  effort: settings.effort,
1265
1520
  thinking: settings.thinking,
@@ -1268,6 +1523,25 @@ function buildQueryOptions(host, context) {
1268
1523
  disallowedTools: disallowedTools.length > 0 ? disallowedTools : void 0,
1269
1524
  enableFileCheckpointing: settings.enableFileCheckpointing
1270
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;
1271
1545
  }
1272
1546
  function buildMultimodalPrompt(textPrompt, context) {
1273
1547
  const taskImages = (context.files ?? []).filter(
@@ -1316,25 +1590,40 @@ function buildMultimodalPrompt(textPrompt, context) {
1316
1590
  async function runSdkQuery(host, context, followUpContent) {
1317
1591
  if (host.isStopped()) return;
1318
1592
  const isPm = host.config.mode === "pm";
1319
- if (isPm) {
1593
+ const isPmPlanning = isPm && host.activeMode === "planning";
1594
+ if (isPmPlanning) {
1320
1595
  host.snapshotPlanFiles();
1321
1596
  }
1322
1597
  const options = buildQueryOptions(host, context);
1323
1598
  const resume = context.claudeSessionId ?? void 0;
1324
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
+ );
1325
1604
  const textPrompt = isPm ? `${buildInitialPrompt(host.config.mode, context)}
1326
1605
 
1327
1606
  ---
1328
1607
 
1329
1608
  The team says:
1330
- ${followUpContent}` : followUpContent;
1331
- const prompt = isPm ? buildMultimodalPrompt(textPrompt, context) : textPrompt;
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
+ }
1332
1621
  const agentQuery = query({
1333
1622
  prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
1334
1623
  options: { ...options, resume }
1335
1624
  });
1336
1625
  await runWithRetry(agentQuery, context, host, options);
1337
- } else if (isPm) {
1626
+ } else if (isPmPlanning) {
1338
1627
  return;
1339
1628
  } else {
1340
1629
  const initialPrompt = buildInitialPrompt(host.config.mode, context);
@@ -1345,7 +1634,7 @@ ${followUpContent}` : followUpContent;
1345
1634
  });
1346
1635
  await runWithRetry(agentQuery, context, host, options);
1347
1636
  }
1348
- if (isPm) {
1637
+ if (isPmPlanning) {
1349
1638
  host.syncPlanFile();
1350
1639
  }
1351
1640
  }
@@ -1380,7 +1669,7 @@ async function runWithRetry(initialQuery, context, host, options) {
1380
1669
  });
1381
1670
  return runWithRetry(freshQuery, context, host, options);
1382
1671
  }
1383
- const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
1672
+ const isApiError = error instanceof Error && API_ERROR_PATTERN2.test(error.message);
1384
1673
  if (!isApiError) throw error;
1385
1674
  }
1386
1675
  if (attempt >= RETRY_DELAYS_MS.length) {
@@ -1416,7 +1705,129 @@ async function runWithRetry(initialQuery, context, host, options) {
1416
1705
  }
1417
1706
  }
1418
1707
 
1419
- // src/runner.ts
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
+ };
1829
+
1830
+ // src/runner/agent-runner.ts
1420
1831
  var HEARTBEAT_INTERVAL_MS = 3e4;
1421
1832
  var AgentRunner = class _AgentRunner {
1422
1833
  config;
@@ -1428,15 +1839,17 @@ var AgentRunner = class _AgentRunner {
1428
1839
  pendingMessages = [];
1429
1840
  setupLog = [];
1430
1841
  heartbeatTimer = null;
1431
- taskContext = null;
1432
- planFileSnapshot = /* @__PURE__ */ new Map();
1433
- lockedPlanFile = null;
1842
+ taskContext = null;
1843
+ planSync;
1844
+ costTracker = new CostTracker();
1434
1845
  worktreeActive = false;
1846
+ activeMode = "planning";
1435
1847
  static MAX_SETUP_LOG_LINES = 50;
1436
1848
  constructor(config, callbacks) {
1437
1849
  this.config = config;
1438
1850
  this.connection = new ConveyorConnection(config);
1439
1851
  this.callbacks = callbacks;
1852
+ this.planSync = new PlanSync(config.workspaceDir, this.connection);
1440
1853
  }
1441
1854
  get state() {
1442
1855
  return this._state;
@@ -1466,6 +1879,7 @@ var AgentRunner = class _AgentRunner {
1466
1879
  this.connection.onChatMessage(
1467
1880
  (message) => this.injectHumanMessage(message.content, message.files)
1468
1881
  );
1882
+ this.connection.onModeChange((data) => this.handleModeChange(data.mode));
1469
1883
  await this.setState("connected");
1470
1884
  this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
1471
1885
  this.startHeartbeat();
@@ -1478,11 +1892,12 @@ var AgentRunner = class _AgentRunner {
1478
1892
  return;
1479
1893
  }
1480
1894
  }
1481
- this.initRtk();
1482
- if (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true") {
1895
+ initRtk();
1896
+ if (process.env.CONVEYOR_USE_WORKTREE !== "false" && (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true")) {
1483
1897
  try {
1484
1898
  const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
1485
1899
  this.config = { ...this.config, workspaceDir: worktreePath };
1900
+ this.planSync.updateWorkspaceDir(worktreePath);
1486
1901
  this.worktreeActive = true;
1487
1902
  this.setupLog.push(`[conveyor] Using worktree: ${worktreePath}`);
1488
1903
  } catch (error) {
@@ -1503,15 +1918,9 @@ var AgentRunner = class _AgentRunner {
1503
1918
  return;
1504
1919
  }
1505
1920
  this.taskContext._runnerSessionId = randomUUID2();
1921
+ this.logEffectiveSettings();
1506
1922
  if (process.env.CODESPACES === "true") {
1507
- try {
1508
- execSync3("git fetch --unshallow", {
1509
- cwd: this.config.workspaceDir,
1510
- stdio: "ignore",
1511
- timeout: 6e4
1512
- });
1513
- } catch {
1514
- }
1923
+ unshallowRepo(this.config.workspaceDir);
1515
1924
  }
1516
1925
  if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
1517
1926
  const result = cleanDevcontainerFromGit(
@@ -1527,6 +1936,7 @@ var AgentRunner = class _AgentRunner {
1527
1936
  try {
1528
1937
  const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
1529
1938
  this.config = { ...this.config, workspaceDir: worktreePath };
1939
+ this.planSync.updateWorkspaceDir(worktreePath);
1530
1940
  this.worktreeActive = true;
1531
1941
  this.setupLog.push(`[conveyor] Using worktree (from task config): ${worktreePath}`);
1532
1942
  } catch (error) {
@@ -1549,7 +1959,7 @@ var AgentRunner = class _AgentRunner {
1549
1959
  await this.setState("idle");
1550
1960
  } else {
1551
1961
  await this.setState("running");
1552
- await runSdkQuery(this.asQueryHost(), this.taskContext);
1962
+ await this.runQuerySafe(this.taskContext);
1553
1963
  if (!this.stopped) await this.setState("idle");
1554
1964
  }
1555
1965
  await this.runCoreLoop();
@@ -1557,6 +1967,16 @@ var AgentRunner = class _AgentRunner {
1557
1967
  await this.setState("finished");
1558
1968
  this.connection.disconnect();
1559
1969
  }
1970
+ async runQuerySafe(context, followUp) {
1971
+ try {
1972
+ await runSdkQuery(this.asQueryHost(), context, followUp);
1973
+ } catch (error) {
1974
+ const message = error instanceof Error ? error.message : "Unknown error";
1975
+ this.connection.sendEvent({ type: "error", message });
1976
+ await this.callbacks.onEvent({ type: "error", message });
1977
+ await this.setState("error");
1978
+ }
1979
+ }
1560
1980
  async runCoreLoop() {
1561
1981
  if (!this.taskContext) return;
1562
1982
  while (!this.stopped) {
@@ -1564,8 +1984,8 @@ var AgentRunner = class _AgentRunner {
1564
1984
  const msg = await this.waitForUserContent();
1565
1985
  if (!msg) break;
1566
1986
  await this.setState("running");
1567
- await runSdkQuery(this.asQueryHost(), this.taskContext, msg);
1568
- if (!this.stopped) await this.setState("idle");
1987
+ await this.runQuerySafe(this.taskContext, msg);
1988
+ if (!this.stopped && this._state !== "error") await this.setState("idle");
1569
1989
  } else if (this._state === "error") {
1570
1990
  await this.setState("idle");
1571
1991
  } else {
@@ -1610,6 +2030,24 @@ The agent cannot start until this is resolved.`
1610
2030
  return false;
1611
2031
  }
1612
2032
  }
2033
+ logEffectiveSettings() {
2034
+ if (!this.taskContext) return;
2035
+ const s = this.taskContext.agentSettings ?? this.config.agentSettings ?? {};
2036
+ const model = this.taskContext.model || this.config.model;
2037
+ const thinking = s.thinking?.type === "enabled" ? `enabled(${s.thinking.budgetTokens ?? "?"})` : s.thinking?.type ?? "default";
2038
+ const parts = [
2039
+ `model=${model}`,
2040
+ `mode=${this.config.mode ?? "task"}`,
2041
+ `effort=${s.effort ?? "default"}`,
2042
+ `thinking=${thinking}`,
2043
+ `maxBudget=$${s.maxBudgetUsd ?? 50}`,
2044
+ `maxTurns=${s.maxTurns ?? "unlimited"}`
2045
+ ];
2046
+ if (s.betas?.length) parts.push(`betas=${s.betas.join(",")}`);
2047
+ if (s.disallowedTools?.length) parts.push(`disallowed=[${s.disallowedTools.join(",")}]`);
2048
+ if (s.enableFileCheckpointing) parts.push(`checkpointing=on`);
2049
+ console.log(`[conveyor-agent] ${parts.join(", ")}`);
2050
+ }
1613
2051
  pushSetupLog(line) {
1614
2052
  this.setupLog.push(line);
1615
2053
  if (this.setupLog.length > _AgentRunner.MAX_SETUP_LOG_LINES) {
@@ -1634,13 +2072,6 @@ The agent cannot start until this is resolved.`
1634
2072
  });
1635
2073
  }
1636
2074
  }
1637
- initRtk() {
1638
- try {
1639
- execSync3("rtk --version", { stdio: "ignore" });
1640
- execSync3("rtk init --global --auto-patch", { stdio: "ignore" });
1641
- } catch {
1642
- }
1643
- }
1644
2075
  injectHumanMessage(content, files) {
1645
2076
  let messageContent;
1646
2077
  if (files?.length) {
@@ -1706,11 +2137,15 @@ ${f.content}
1706
2137
  async waitForUserContent() {
1707
2138
  if (this.pendingMessages.length > 0) {
1708
2139
  const next = this.pendingMessages.shift();
1709
- return next?.message?.content ?? null;
2140
+ const content2 = next?.message.content;
2141
+ if (!content2) return null;
2142
+ return content2;
1710
2143
  }
1711
2144
  const msg = await this.waitForMessage();
1712
2145
  if (!msg) return null;
1713
- return msg.message.content;
2146
+ const content = msg.message.content;
2147
+ if (!content) return null;
2148
+ return content;
1714
2149
  }
1715
2150
  async *createInputStream(initialPrompt) {
1716
2151
  const makeUserMessage = (content) => ({
@@ -1735,93 +2170,31 @@ ${f.content}
1735
2170
  yield msg;
1736
2171
  }
1737
2172
  }
1738
- getPlanDirs() {
1739
- return [
1740
- join3(homedir(), ".claude", "plans"),
1741
- join3(this.config.workspaceDir, ".claude", "plans")
1742
- ];
1743
- }
1744
- /**
1745
- * Snapshot current plan files so syncPlanFile can distinguish files created
1746
- * by THIS session from ones created by a concurrent agent.
1747
- */
1748
- snapshotPlanFiles() {
1749
- this.planFileSnapshot.clear();
1750
- this.lockedPlanFile = null;
1751
- for (const plansDir of this.getPlanDirs()) {
1752
- try {
1753
- for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
1754
- try {
1755
- const fullPath = join3(plansDir, file);
1756
- const stat = statSync(fullPath);
1757
- this.planFileSnapshot.set(fullPath, stat.mtimeMs);
1758
- } catch {
1759
- continue;
1760
- }
1761
- }
1762
- } catch {
1763
- }
1764
- }
1765
- }
1766
- syncPlanFile() {
1767
- if (this.lockedPlanFile) {
1768
- try {
1769
- const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
1770
- if (content) {
1771
- this.connection.updateTaskFields({ plan: content });
1772
- const fileName = this.lockedPlanFile.split("/").pop();
1773
- this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
1774
- }
1775
- } catch {
1776
- }
1777
- return;
1778
- }
1779
- let newest = null;
1780
- for (const plansDir of this.getPlanDirs()) {
1781
- let files;
1782
- try {
1783
- files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1784
- } catch {
1785
- continue;
1786
- }
1787
- for (const file of files) {
1788
- const fullPath = join3(plansDir, file);
1789
- try {
1790
- const stat = statSync(fullPath);
1791
- const prevMtime = this.planFileSnapshot.get(fullPath);
1792
- const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
1793
- if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
1794
- newest = { path: fullPath, mtime: stat.mtimeMs };
1795
- }
1796
- } catch {
1797
- continue;
1798
- }
1799
- }
1800
- }
1801
- if (newest) {
1802
- this.lockedPlanFile = newest.path;
1803
- const content = readFileSync(newest.path, "utf-8").trim();
1804
- if (content) {
1805
- this.connection.updateTaskFields({ plan: content });
1806
- const fileName = newest.path.split("/").pop();
1807
- this.connection.postChatMessage(
1808
- `Detected local plan file (${fileName}) and synced it to the task plan.`
1809
- );
1810
- }
1811
- }
1812
- }
1813
2173
  asQueryHost() {
2174
+ const getActiveMode = () => this.activeMode;
1814
2175
  return {
1815
2176
  config: this.config,
1816
2177
  connection: this.connection,
1817
2178
  callbacks: this.callbacks,
1818
2179
  setupLog: this.setupLog,
2180
+ costTracker: this.costTracker,
2181
+ get activeMode() {
2182
+ return getActiveMode();
2183
+ },
1819
2184
  isStopped: () => this.stopped,
1820
2185
  createInputStream: (prompt) => this.createInputStream(prompt),
1821
- snapshotPlanFiles: () => this.snapshotPlanFiles(),
1822
- syncPlanFile: () => this.syncPlanFile()
2186
+ snapshotPlanFiles: () => this.planSync.snapshotPlanFiles(),
2187
+ syncPlanFile: () => this.planSync.syncPlanFile()
1823
2188
  };
1824
2189
  }
2190
+ handleModeChange(mode) {
2191
+ if (this.config.mode !== "pm") return;
2192
+ this.activeMode = mode;
2193
+ this.connection.emitModeChanged(mode);
2194
+ this.connection.postChatMessage(
2195
+ `Mode switched to **${mode}**${mode === "active" ? " \u2014 I now have direct coding access." : " \u2014 back to planning only."}`
2196
+ );
2197
+ }
1825
2198
  stop() {
1826
2199
  this.stopped = true;
1827
2200
  if (this.inputResolver) {
@@ -1831,163 +2204,13 @@ ${f.content}
1831
2204
  }
1832
2205
  };
1833
2206
 
1834
- // src/project-connection.ts
1835
- import { io as io2 } from "socket.io-client";
1836
- var ProjectConnection = class {
1837
- socket = null;
1838
- config;
1839
- taskAssignmentCallback = null;
1840
- stopTaskCallback = null;
1841
- shutdownCallback = null;
1842
- chatMessageCallback = null;
1843
- earlyChatMessages = [];
1844
- constructor(config) {
1845
- this.config = config;
1846
- }
1847
- connect() {
1848
- return new Promise((resolve2, reject) => {
1849
- let settled = false;
1850
- let attempts = 0;
1851
- const maxInitialAttempts = 30;
1852
- this.socket = io2(this.config.apiUrl, {
1853
- auth: { projectToken: this.config.projectToken },
1854
- transports: ["websocket"],
1855
- reconnection: true,
1856
- reconnectionAttempts: Infinity,
1857
- reconnectionDelay: 2e3,
1858
- reconnectionDelayMax: 3e4,
1859
- randomizationFactor: 0.3,
1860
- extraHeaders: {
1861
- "ngrok-skip-browser-warning": "true"
1862
- }
1863
- });
1864
- this.socket.on("projectRunner:assignTask", (data) => {
1865
- if (this.taskAssignmentCallback) {
1866
- this.taskAssignmentCallback(data);
1867
- }
1868
- });
1869
- this.socket.on("projectRunner:stopTask", (data) => {
1870
- if (this.stopTaskCallback) {
1871
- this.stopTaskCallback(data);
1872
- }
1873
- });
1874
- this.socket.on("projectRunner:shutdown", () => {
1875
- if (this.shutdownCallback) {
1876
- this.shutdownCallback();
1877
- }
1878
- });
1879
- this.socket.on("projectRunner:incomingChatMessage", (msg) => {
1880
- if (this.chatMessageCallback) {
1881
- this.chatMessageCallback(msg);
1882
- } else {
1883
- this.earlyChatMessages.push(msg);
1884
- }
1885
- });
1886
- this.socket.on("connect", () => {
1887
- if (!settled) {
1888
- settled = true;
1889
- resolve2();
1890
- }
1891
- });
1892
- this.socket.io.on("reconnect_attempt", () => {
1893
- attempts++;
1894
- if (!settled && attempts >= maxInitialAttempts) {
1895
- settled = true;
1896
- reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
1897
- }
1898
- });
1899
- });
1900
- }
1901
- onTaskAssignment(callback) {
1902
- this.taskAssignmentCallback = callback;
1903
- }
1904
- onStopTask(callback) {
1905
- this.stopTaskCallback = callback;
1906
- }
1907
- onShutdown(callback) {
1908
- this.shutdownCallback = callback;
1909
- }
1910
- onChatMessage(callback) {
1911
- this.chatMessageCallback = callback;
1912
- for (const msg of this.earlyChatMessages) {
1913
- callback(msg);
1914
- }
1915
- this.earlyChatMessages = [];
1916
- }
1917
- sendHeartbeat() {
1918
- if (!this.socket) return;
1919
- this.socket.emit("projectRunner:heartbeat", {});
1920
- }
1921
- emitTaskStarted(taskId) {
1922
- if (!this.socket) return;
1923
- this.socket.emit("projectRunner:taskStarted", { taskId });
1924
- }
1925
- emitTaskStopped(taskId, reason) {
1926
- if (!this.socket) return;
1927
- this.socket.emit("projectRunner:taskStopped", { taskId, reason });
1928
- }
1929
- emitEvent(event) {
1930
- if (!this.socket) return;
1931
- this.socket.emit("conveyor:projectAgentEvent", event);
1932
- }
1933
- emitChatMessage(content) {
1934
- const socket = this.socket;
1935
- if (!socket) return Promise.reject(new Error("Not connected"));
1936
- return new Promise((resolve2, reject) => {
1937
- socket.emit(
1938
- "conveyor:projectAgentChatMessage",
1939
- { content },
1940
- (response) => {
1941
- if (response.success) resolve2();
1942
- else reject(new Error(response.error ?? "Failed to send chat message"));
1943
- }
1944
- );
1945
- });
1946
- }
1947
- emitAgentStatus(status) {
1948
- if (!this.socket) return;
1949
- this.socket.emit("conveyor:projectAgentStatus", { status });
1950
- }
1951
- fetchAgentContext() {
1952
- const socket = this.socket;
1953
- if (!socket) return Promise.reject(new Error("Not connected"));
1954
- return new Promise((resolve2, reject) => {
1955
- socket.emit(
1956
- "projectRunner:getAgentContext",
1957
- (response) => {
1958
- if (response.success) resolve2(response.data ?? null);
1959
- else reject(new Error(response.error ?? "Failed to fetch agent context"));
1960
- }
1961
- );
1962
- });
1963
- }
1964
- fetchChatHistory(limit) {
1965
- const socket = this.socket;
1966
- if (!socket) return Promise.reject(new Error("Not connected"));
1967
- return new Promise((resolve2, reject) => {
1968
- socket.emit(
1969
- "projectRunner:getChatHistory",
1970
- { limit },
1971
- (response) => {
1972
- if (response.success && response.data) resolve2(response.data);
1973
- else reject(new Error(response.error ?? "Failed to fetch chat history"));
1974
- }
1975
- );
1976
- });
1977
- }
1978
- disconnect() {
1979
- this.socket?.disconnect();
1980
- this.socket = null;
1981
- }
1982
- };
1983
-
1984
- // src/project-runner.ts
2207
+ // src/runner/project-runner.ts
1985
2208
  import { fork } from "child_process";
1986
2209
  import { execSync as execSync4 } from "child_process";
1987
2210
  import * as path from "path";
1988
2211
  import { fileURLToPath } from "url";
1989
2212
 
1990
- // src/project-chat-handler.ts
2213
+ // src/runner/project-chat-handler.ts
1991
2214
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1992
2215
  var FALLBACK_MODEL = "claude-sonnet-4-20250514";
1993
2216
  function buildSystemPrompt2(projectDir, agentCtx) {
@@ -2073,33 +2296,31 @@ async function handleProjectChatMessage(message, connection, projectDir) {
2073
2296
  const turnToolCalls = [];
2074
2297
  let isTyping = false;
2075
2298
  for await (const event of events) {
2076
- const eventType = event.type;
2077
- if (eventType === "assistant") {
2299
+ if (event.type === "assistant") {
2078
2300
  if (!isTyping) {
2079
2301
  setTimeout(() => connection.emitEvent({ type: "agent_typing_start" }), 200);
2080
2302
  isTyping = true;
2081
2303
  }
2082
- const msg = event.message;
2083
- const content = msg.content;
2304
+ const assistantEvent = event;
2305
+ const { content } = assistantEvent.message;
2084
2306
  for (const block of content) {
2085
2307
  if (block.type === "text") {
2086
2308
  responseParts.push(block.text);
2087
2309
  } else if (block.type === "tool_use") {
2088
- const name = block.name;
2089
2310
  const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
2090
2311
  turnToolCalls.push({
2091
- tool: name,
2312
+ tool: block.name,
2092
2313
  input: inputStr.slice(0, 1e4),
2093
2314
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2094
2315
  });
2095
- console.log(`[project-chat] [tool_use] ${name}`);
2316
+ console.log(`[project-chat] [tool_use] ${block.name}`);
2096
2317
  }
2097
2318
  }
2098
2319
  if (turnToolCalls.length > 0) {
2099
2320
  connection.emitEvent({ type: "activity_block", events: [...turnToolCalls] });
2100
2321
  turnToolCalls.length = 0;
2101
2322
  }
2102
- } else if (eventType === "result") {
2323
+ } else if (event.type === "result") {
2103
2324
  if (isTyping) {
2104
2325
  connection.emitEvent({ type: "agent_typing_stop" });
2105
2326
  isTyping = false;
@@ -2130,7 +2351,7 @@ async function handleProjectChatMessage(message, connection, projectDir) {
2130
2351
  }
2131
2352
  }
2132
2353
 
2133
- // src/project-runner.ts
2354
+ // src/runner/project-runner.ts
2134
2355
  var __filename = fileURLToPath(import.meta.url);
2135
2356
  var __dirname = path.dirname(__filename);
2136
2357
  var HEARTBEAT_INTERVAL_MS2 = 3e4;
@@ -2178,7 +2399,7 @@ var ProjectRunner = class {
2178
2399
  });
2179
2400
  }
2180
2401
  async handleAssignment(assignment) {
2181
- const { taskId, taskToken, apiUrl, mode, branch, devBranch } = assignment;
2402
+ const { taskId, taskToken, apiUrl, mode, branch, devBranch, useWorktree } = assignment;
2182
2403
  const shortId = taskId.slice(0, 8);
2183
2404
  if (this.activeAgents.has(taskId)) {
2184
2405
  console.log(`[project-runner] Task ${shortId} already running, skipping`);
@@ -2197,13 +2418,19 @@ var ProjectRunner = class {
2197
2418
  } catch {
2198
2419
  console.log(`[task:${shortId}] Warning: git fetch failed`);
2199
2420
  }
2200
- const worktreePath = ensureWorktree(this.projectDir, taskId, devBranch);
2421
+ let workDir;
2422
+ const shouldWorktree = useWorktree !== false;
2423
+ if (shouldWorktree) {
2424
+ workDir = ensureWorktree(this.projectDir, taskId, devBranch);
2425
+ } else {
2426
+ workDir = this.projectDir;
2427
+ }
2201
2428
  if (branch && branch !== devBranch) {
2202
2429
  try {
2203
- execSync4(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
2430
+ execSync4(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
2204
2431
  } catch {
2205
2432
  try {
2206
- execSync4(`git checkout -b ${branch}`, { cwd: worktreePath, stdio: "ignore" });
2433
+ execSync4(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
2207
2434
  } catch {
2208
2435
  console.log(`[task:${shortId}] Warning: could not checkout branch ${branch}`);
2209
2436
  }
@@ -2220,10 +2447,10 @@ var ProjectRunner = class {
2220
2447
  CONVEYOR_TASK_TOKEN: taskToken,
2221
2448
  CONVEYOR_TASK_ID: taskId,
2222
2449
  CONVEYOR_MODE: mode,
2223
- CONVEYOR_WORKSPACE: worktreePath,
2450
+ CONVEYOR_WORKSPACE: workDir,
2224
2451
  CONVEYOR_USE_WORKTREE: "false"
2225
2452
  },
2226
- cwd: worktreePath,
2453
+ cwd: workDir,
2227
2454
  stdio: ["pipe", "pipe", "pipe", "ipc"]
2228
2455
  });
2229
2456
  child.stdout?.on("data", (data) => {
@@ -2238,9 +2465,9 @@ var ProjectRunner = class {
2238
2465
  console.error(`[task:${shortId}] ${line}`);
2239
2466
  }
2240
2467
  });
2241
- this.activeAgents.set(taskId, { process: child, worktreePath, mode });
2468
+ this.activeAgents.set(taskId, { process: child, worktreePath: workDir, mode });
2242
2469
  this.connection.emitTaskStarted(taskId);
2243
- console.log(`[project-runner] Started task ${shortId} in ${mode} mode at ${worktreePath}`);
2470
+ console.log(`[project-runner] Started task ${shortId} in ${mode} mode at ${workDir}`);
2244
2471
  child.on("exit", (code) => {
2245
2472
  this.activeAgents.delete(taskId);
2246
2473
  const reason = code === 0 ? "completed" : `exited with code ${code}`;
@@ -2317,15 +2544,80 @@ var ProjectRunner = class {
2317
2544
  }
2318
2545
  };
2319
2546
 
2547
+ // src/runner/file-cache.ts
2548
+ var DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024;
2549
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
2550
+ var FileCache = class {
2551
+ cache = /* @__PURE__ */ new Map();
2552
+ currentSize = 0;
2553
+ maxSizeBytes;
2554
+ ttlMs;
2555
+ constructor(maxSizeBytes = DEFAULT_MAX_SIZE_BYTES, ttlMs = DEFAULT_TTL_MS) {
2556
+ this.maxSizeBytes = maxSizeBytes;
2557
+ this.ttlMs = ttlMs;
2558
+ }
2559
+ get(fileId) {
2560
+ const entry = this.cache.get(fileId);
2561
+ if (!entry) return null;
2562
+ if (Date.now() - entry.createdAt > this.ttlMs) {
2563
+ this.delete(fileId);
2564
+ return null;
2565
+ }
2566
+ this.cache.delete(fileId);
2567
+ this.cache.set(fileId, entry);
2568
+ return entry;
2569
+ }
2570
+ set(fileId, content, mimeType, fileName) {
2571
+ if (this.cache.has(fileId)) {
2572
+ this.delete(fileId);
2573
+ }
2574
+ const size = content.byteLength;
2575
+ if (size > this.maxSizeBytes) return;
2576
+ while (this.currentSize + size > this.maxSizeBytes && this.cache.size > 0) {
2577
+ const oldestKey = this.cache.keys().next().value;
2578
+ if (oldestKey !== void 0) {
2579
+ this.delete(oldestKey);
2580
+ }
2581
+ }
2582
+ this.cache.set(fileId, {
2583
+ content,
2584
+ mimeType,
2585
+ fileName,
2586
+ createdAt: Date.now(),
2587
+ size
2588
+ });
2589
+ this.currentSize += size;
2590
+ }
2591
+ delete(fileId) {
2592
+ const entry = this.cache.get(fileId);
2593
+ if (entry) {
2594
+ this.currentSize -= entry.size;
2595
+ this.cache.delete(fileId);
2596
+ }
2597
+ }
2598
+ clear() {
2599
+ this.cache.clear();
2600
+ this.currentSize = 0;
2601
+ }
2602
+ get stats() {
2603
+ return {
2604
+ entries: this.cache.size,
2605
+ sizeBytes: this.currentSize,
2606
+ maxSizeBytes: this.maxSizeBytes
2607
+ };
2608
+ }
2609
+ };
2610
+
2320
2611
  export {
2321
2612
  ConveyorConnection,
2613
+ ProjectConnection,
2322
2614
  loadConveyorConfig,
2323
2615
  runSetupCommand,
2324
2616
  runStartCommand,
2325
2617
  ensureWorktree,
2326
2618
  removeWorktree,
2327
2619
  AgentRunner,
2328
- ProjectConnection,
2329
- ProjectRunner
2620
+ ProjectRunner,
2621
+ FileCache
2330
2622
  };
2331
- //# sourceMappingURL=chunk-I7ETGLPA.js.map
2623
+ //# sourceMappingURL=chunk-22ME6AB3.js.map