@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.
@@ -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,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} -- ${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"`);
422
556
  }
423
- }
424
- git(`git push --force-with-lease origin ${taskBranch}`);
425
- return { cleaned: true, message: "devcontainer.json cleaned from git history" };
426
- } catch (err) {
427
- const msg = err instanceof Error ? err.message : "Unknown error";
428
- return { cleaned: false, message: `Git cleanup failed: ${msg}` };
429
- }
430
- }
431
-
432
- // src/worktree.ts
433
- import { execSync as execSync2 } from "child_process";
434
- import { existsSync } from "fs";
435
- import { join as join2 } from "path";
436
- var WORKTREE_DIR = ".worktrees";
437
- function ensureWorktree(projectDir, taskId, branch) {
438
- const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
439
- if (existsSync(worktreePath)) {
440
- if (branch) {
441
- try {
442
- execSync2(`git checkout --detach origin/${branch}`, {
443
- cwd: worktreePath,
444
- stdio: "ignore"
445
- });
446
- } catch {
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
- const ref = branch ? `origin/${branch}` : "HEAD";
452
- execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
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/runner.ts
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 { query } from "@anthropic-ai/claude-agent-sdk";
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
- `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.`
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, 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.`
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, 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.`
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
- `Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
647
- `When finished, commit your changes, push the branch, and use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
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
- `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).`,
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, 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.`,
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 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 : [
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" or similar, run \`git pull --rebase origin ${context.githubBranch}\` and retry. If that also fails, use \`git push --force-with-lease origin ${context.githubBranch}\`.`,
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
- You have access to Conveyor MCP tools to interact with the task management system.`,
807
- `Use the post_to_chat tool to communicate progress or ask questions.`,
808
- `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.`
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/mcp-tools.ts
819
- 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";
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 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",
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
- tool(
1282
+ tool2(
938
1283
  "update_task",
939
1284
  "Save the finalized task plan and/or description",
940
1285
  {
941
- plan: z.string().optional().describe("The task plan in markdown"),
942
- 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")
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
- tool(
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: z.string().describe("Subtask title"),
958
- description: z.string().optional().describe("Brief description"),
959
- plan: z.string().optional().describe("Implementation plan in markdown"),
960
- ordinal: z.number().optional().describe("Step/order number (0-based)"),
961
- 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)
962
1307
  },
963
1308
  async (params) => {
964
1309
  try {
@@ -971,16 +1316,16 @@ function buildPmTools(connection, storyPoints) {
971
1316
  }
972
1317
  }
973
1318
  ),
974
- tool(
1319
+ tool2(
975
1320
  "update_subtask",
976
1321
  "Update an existing subtask's fields",
977
1322
  {
978
- subtaskId: z.string().describe("The subtask ID to update"),
979
- title: z.string().optional(),
980
- description: z.string().optional(),
981
- plan: z.string().optional(),
982
- ordinal: z.number().optional(),
983
- 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)
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
- tool(
1339
+ tool2(
995
1340
  "delete_subtask",
996
1341
  "Delete a subtask",
997
- { subtaskId: z.string().describe("The subtask ID to delete") },
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
- tool(
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
- tool(
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: z.string().describe("The PR title"),
1030
- 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")
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 API_ERROR_PATTERN = /API Error: [45]\d\d/;
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
- async function processAssistantEvent(event, host, turnToolCalls) {
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
- 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)) {
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 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
+ );
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
- 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 = {
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: true,
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
- if (isPm) {
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
- ${followUpContent}` : followUpContent;
1324
- 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
+ }
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 (isPm) {
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 (isPm) {
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 && API_ERROR_PATTERN.test(error.message);
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
- planFileSnapshot = /* @__PURE__ */ new Map();
1426
- lockedPlanFile = null;
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
- this.initRtk();
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
- return next?.message?.content ?? null;
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
- return msg.message.content;
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-connection.ts
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
- const eventType = event.type;
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 msg = event.message;
2066
- const content = msg.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 (eventType === "result") {
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
- ProjectConnection,
2312
- ProjectRunner
2604
+ ProjectRunner,
2605
+ FileCache
2313
2606
  };
2314
- //# sourceMappingURL=chunk-3BXPSYXX.js.map
2607
+ //# sourceMappingURL=chunk-5UYKPYFQ.js.map