@rallycry/conveyor-agent 4.3.0 → 4.4.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,5 +1,100 @@
1
1
  // src/connection/task-connection.ts
2
2
  import { io } from "socket.io-client";
3
+
4
+ // src/connection/task-connection-rpc.ts
5
+ function requireSocket(socket) {
6
+ if (!socket) throw new Error("Not connected");
7
+ return socket;
8
+ }
9
+ function emitRpc(socket, event, data) {
10
+ return new Promise((resolve2, reject) => {
11
+ socket.emit(event, data, (response) => {
12
+ if (response.success && response.data) resolve2(response.data);
13
+ else reject(new Error(response.error ?? `Failed: ${event}`));
14
+ });
15
+ });
16
+ }
17
+ function emitRpcVoid(socket, event, data) {
18
+ return new Promise((resolve2, reject) => {
19
+ socket.emit(event, data, (response) => {
20
+ if (response.success) resolve2();
21
+ else reject(new Error(response.error ?? `Failed: ${event}`));
22
+ });
23
+ });
24
+ }
25
+ function fetchChatMessages(socket, limit, taskId) {
26
+ return emitRpc(requireSocket(socket), "agentRunner:getChatMessages", { limit, taskId });
27
+ }
28
+ function fetchTaskFiles(socket) {
29
+ return emitRpc(requireSocket(socket), "agentRunner:getTaskFiles", {});
30
+ }
31
+ function fetchTaskFile(socket, fileId) {
32
+ return emitRpc(requireSocket(socket), "agentRunner:getTaskFile", { fileId });
33
+ }
34
+ function fetchTaskContext(socket) {
35
+ return emitRpc(requireSocket(socket), "agentRunner:getTaskContext", {});
36
+ }
37
+ function createPR(socket, params) {
38
+ return emitRpc(requireSocket(socket), "agentRunner:createPR", params);
39
+ }
40
+ function createSubtask(socket, data) {
41
+ return emitRpc(requireSocket(socket), "agentRunner:createSubtask", data);
42
+ }
43
+ function listSubtasks(socket) {
44
+ return emitRpc(requireSocket(socket), "agentRunner:listSubtasks", {});
45
+ }
46
+ function fetchCliHistory(socket, taskId, limit, source) {
47
+ const s = requireSocket(socket);
48
+ const timeout = 1e4;
49
+ return Promise.race([
50
+ emitRpc(s, "agentRunner:getCliHistory", {
51
+ taskId,
52
+ limit,
53
+ source
54
+ }),
55
+ new Promise((_, reject) => {
56
+ setTimeout(() => reject(new Error("CLI history request timed out")), timeout);
57
+ })
58
+ ]);
59
+ }
60
+ function startChildCloudBuild(socket, childTaskId) {
61
+ return emitRpc(requireSocket(socket), "agentRunner:startChildCloudBuild", { childTaskId });
62
+ }
63
+ function approveAndMergePR(socket, childTaskId) {
64
+ return emitRpc(requireSocket(socket), "agentRunner:approveAndMergePR", { childTaskId });
65
+ }
66
+ function postChildChatMessage(socket, childTaskId, content) {
67
+ return emitRpcVoid(requireSocket(socket), "agentRunner:postChildChatMessage", {
68
+ childTaskId,
69
+ content
70
+ });
71
+ }
72
+ function updateChildStatus(socket, childTaskId, status) {
73
+ return emitRpcVoid(requireSocket(socket), "agentRunner:updateChildStatus", {
74
+ childTaskId,
75
+ status
76
+ });
77
+ }
78
+ function stopChildBuild(socket, childTaskId) {
79
+ return emitRpcVoid(requireSocket(socket), "agentRunner:stopChildBuild", { childTaskId });
80
+ }
81
+ function fetchTask(socket, slugOrId) {
82
+ return emitRpc(requireSocket(socket), "agentRunner:getTask", { slugOrId });
83
+ }
84
+ function listIcons(socket) {
85
+ return emitRpc(requireSocket(socket), "agentRunner:listIcons", {});
86
+ }
87
+ function generateTaskIcon(socket, prompt, aspectRatio) {
88
+ return emitRpc(requireSocket(socket), "agentRunner:generateTaskIcon", { prompt, aspectRatio });
89
+ }
90
+ function getTaskProperties(socket) {
91
+ return emitRpc(requireSocket(socket), "agentRunner:getTaskProperties", {});
92
+ }
93
+ function triggerIdentification(socket) {
94
+ return emitRpc(requireSocket(socket), "agentRunner:triggerIdentification", {});
95
+ }
96
+
97
+ // src/connection/task-connection.ts
3
98
  var ConveyorConnection = class _ConveyorConnection {
4
99
  socket = null;
5
100
  config;
@@ -30,23 +125,15 @@ var ConveyorConnection = class _ConveyorConnection {
30
125
  reconnectionDelay: 2e3,
31
126
  reconnectionDelayMax: 3e4,
32
127
  randomizationFactor: 0.3,
33
- extraHeaders: {
34
- "ngrok-skip-browser-warning": "true"
35
- }
128
+ extraHeaders: { "ngrok-skip-browser-warning": "true" }
36
129
  });
37
130
  this.socket.on("agentRunner:incomingMessage", (msg) => {
38
- if (this.chatMessageCallback) {
39
- this.chatMessageCallback(msg);
40
- } else {
41
- this.earlyMessages.push(msg);
42
- }
131
+ if (this.chatMessageCallback) this.chatMessageCallback(msg);
132
+ else this.earlyMessages.push(msg);
43
133
  });
44
134
  this.socket.on("agentRunner:stop", () => {
45
- if (this.stopCallback) {
46
- this.stopCallback();
47
- } else {
48
- this.earlyStop = true;
49
- }
135
+ if (this.stopCallback) this.stopCallback();
136
+ else this.earlyStop = true;
50
137
  });
51
138
  this.socket.on("agentRunner:questionAnswer", (data) => {
52
139
  const resolver = this.pendingQuestionResolvers.get(data.requestId);
@@ -56,16 +143,11 @@ var ConveyorConnection = class _ConveyorConnection {
56
143
  }
57
144
  });
58
145
  this.socket.on("agentRunner:setMode", (data) => {
59
- if (this.modeChangeCallback) {
60
- this.modeChangeCallback(data);
61
- } else {
62
- this.earlyModeChanges.push(data);
63
- }
146
+ if (this.modeChangeCallback) this.modeChangeCallback(data);
147
+ else this.earlyModeChanges.push(data);
64
148
  });
65
149
  this.socket.on("agentRunner:runStartCommand", () => {
66
- if (this.runStartCommandCallback) {
67
- this.runStartCommandCallback();
68
- }
150
+ if (this.runStartCommandCallback) this.runStartCommandCallback();
69
151
  });
70
152
  this.socket.on("connect", () => {
71
153
  if (!settled) {
@@ -83,72 +165,16 @@ var ConveyorConnection = class _ConveyorConnection {
83
165
  });
84
166
  }
85
167
  fetchChatMessages(limit, taskId) {
86
- const socket = this.socket;
87
- if (!socket) throw new Error("Not connected");
88
- return new Promise((resolve2, reject) => {
89
- socket.emit(
90
- "agentRunner:getChatMessages",
91
- { limit, taskId },
92
- (response) => {
93
- if (response.success && response.data) {
94
- resolve2(response.data);
95
- } else {
96
- reject(new Error(response.error ?? "Failed to fetch chat messages"));
97
- }
98
- }
99
- );
100
- });
168
+ return fetchChatMessages(this.socket, limit, taskId);
101
169
  }
102
170
  fetchTaskFiles() {
103
- const socket = this.socket;
104
- if (!socket) throw new Error("Not connected");
105
- return new Promise((resolve2, reject) => {
106
- socket.emit(
107
- "agentRunner:getTaskFiles",
108
- {},
109
- (response) => {
110
- if (response.success && response.data) {
111
- resolve2(response.data);
112
- } else {
113
- reject(new Error(response.error ?? "Failed to fetch task files"));
114
- }
115
- }
116
- );
117
- });
171
+ return fetchTaskFiles(this.socket);
118
172
  }
119
173
  fetchTaskFile(fileId) {
120
- const socket = this.socket;
121
- if (!socket) throw new Error("Not connected");
122
- return new Promise((resolve2, reject) => {
123
- socket.emit(
124
- "agentRunner:getTaskFile",
125
- { fileId },
126
- (response) => {
127
- if (response.success && response.data) {
128
- resolve2(response.data);
129
- } else {
130
- reject(new Error(response.error ?? "Failed to fetch task file"));
131
- }
132
- }
133
- );
134
- });
174
+ return fetchTaskFile(this.socket, fileId);
135
175
  }
136
176
  fetchTaskContext() {
137
- const socket = this.socket;
138
- if (!socket) throw new Error("Not connected");
139
- return new Promise((resolve2, reject) => {
140
- socket.emit(
141
- "agentRunner:getTaskContext",
142
- {},
143
- (response) => {
144
- if (response.success && response.data) {
145
- resolve2(response.data);
146
- } else {
147
- reject(new Error(response.error ?? "Failed to fetch task context"));
148
- }
149
- }
150
- );
151
- });
177
+ return fetchTaskContext(this.socket);
152
178
  }
153
179
  sendEvent(event) {
154
180
  if (!this.socket) throw new Error("Not connected");
@@ -163,9 +189,7 @@ var ConveyorConnection = class _ConveyorConnection {
163
189
  this.flushTimer = null;
164
190
  }
165
191
  if (!this.socket || this.eventBuffer.length === 0) return;
166
- for (const entry of this.eventBuffer) {
167
- this.socket.emit("agentRunner:event", entry);
168
- }
192
+ for (const entry of this.eventBuffer) this.socket.emit("agentRunner:event", entry);
169
193
  this.eventBuffer = [];
170
194
  }
171
195
  updateStatus(status) {
@@ -177,21 +201,7 @@ var ConveyorConnection = class _ConveyorConnection {
177
201
  this.socket.emit("agentRunner:chatMessage", { content });
178
202
  }
179
203
  createPR(params) {
180
- const socket = this.socket;
181
- if (!socket) throw new Error("Not connected");
182
- return new Promise((resolve2, reject) => {
183
- socket.emit(
184
- "agentRunner:createPR",
185
- params,
186
- (response) => {
187
- if (response.success && response.data) {
188
- resolve2(response.data);
189
- } else {
190
- reject(new Error(response.error ?? "Failed to create pull request"));
191
- }
192
- }
193
- );
194
- });
204
+ return createPR(this.socket, params);
195
205
  }
196
206
  askUserQuestion(requestId, questions) {
197
207
  if (!this.socket) throw new Error("Not connected");
@@ -213,9 +223,7 @@ var ConveyorConnection = class _ConveyorConnection {
213
223
  }
214
224
  onChatMessage(callback) {
215
225
  this.chatMessageCallback = callback;
216
- for (const msg of this.earlyMessages) {
217
- callback(msg);
218
- }
226
+ for (const msg of this.earlyMessages) callback(msg);
219
227
  this.earlyMessages = [];
220
228
  }
221
229
  onStopRequested(callback) {
@@ -227,9 +235,7 @@ var ConveyorConnection = class _ConveyorConnection {
227
235
  }
228
236
  onModeChange(callback) {
229
237
  this.modeChangeCallback = callback;
230
- for (const data of this.earlyModeChanges) {
231
- callback(data);
232
- }
238
+ for (const data of this.earlyModeChanges) callback(data);
233
239
  this.earlyModeChanges = [];
234
240
  }
235
241
  onRunStartCommand(callback) {
@@ -258,14 +264,7 @@ var ConveyorConnection = class _ConveyorConnection {
258
264
  this.sendEvent({ type: "agent_typing_stop" });
259
265
  }
260
266
  createSubtask(data) {
261
- const socket = this.socket;
262
- if (!socket) throw new Error("Not connected");
263
- return new Promise((resolve2, reject) => {
264
- socket.emit("agentRunner:createSubtask", data, (response) => {
265
- if (response.success && response.data) resolve2(response.data);
266
- else reject(new Error(response.error ?? "Failed to create subtask"));
267
- });
268
- });
267
+ return createSubtask(this.socket, data);
269
268
  }
270
269
  updateSubtask(subtaskId, fields) {
271
270
  if (!this.socket) throw new Error("Not connected");
@@ -276,183 +275,44 @@ var ConveyorConnection = class _ConveyorConnection {
276
275
  this.socket.emit("agentRunner:deleteSubtask", { subtaskId });
277
276
  }
278
277
  listSubtasks() {
279
- const socket = this.socket;
280
- if (!socket) throw new Error("Not connected");
281
- return new Promise((resolve2, reject) => {
282
- socket.emit("agentRunner:listSubtasks", {}, (response) => {
283
- if (response.success && response.data) resolve2(response.data);
284
- else reject(new Error(response.error ?? "Failed to list subtasks"));
285
- });
286
- });
278
+ return listSubtasks(this.socket);
287
279
  }
288
280
  fetchCliHistory(taskId, limit, source) {
289
- const socket = this.socket;
290
- if (!socket) throw new Error("Not connected");
291
- const timeout = 1e4;
292
- return Promise.race([
293
- new Promise((resolve2, reject) => {
294
- socket.emit(
295
- "agentRunner:getCliHistory",
296
- { taskId: taskId ?? this.config.taskId, limit, source },
297
- (response) => {
298
- if (response.success && response.data) {
299
- resolve2(response.data);
300
- } else {
301
- reject(new Error(response.error ?? "Failed to fetch CLI history"));
302
- }
303
- }
304
- );
305
- }),
306
- new Promise(
307
- (_, reject) => setTimeout(() => reject(new Error("CLI history request timed out")), timeout)
308
- )
309
- ]);
281
+ return fetchCliHistory(this.socket, taskId ?? this.config.taskId, limit, source);
310
282
  }
311
283
  startChildCloudBuild(childTaskId) {
312
- const socket = this.socket;
313
- if (!socket) throw new Error("Not connected");
314
- return new Promise((resolve2, reject) => {
315
- socket.emit(
316
- "agentRunner:startChildCloudBuild",
317
- { childTaskId },
318
- (response) => {
319
- if (response.success && response.data) resolve2(response.data);
320
- else reject(new Error(response.error ?? "Failed to start child cloud build"));
321
- }
322
- );
323
- });
284
+ return startChildCloudBuild(this.socket, childTaskId);
324
285
  }
325
286
  approveAndMergePR(childTaskId) {
326
- const socket = this.socket;
327
- if (!socket) throw new Error("Not connected");
328
- return new Promise((resolve2, reject) => {
329
- socket.emit(
330
- "agentRunner:approveAndMergePR",
331
- { childTaskId },
332
- (response) => {
333
- if (response.success && response.data) {
334
- resolve2(response.data);
335
- } else {
336
- reject(new Error(response.error ?? "Failed to approve and merge PR"));
337
- }
338
- }
339
- );
340
- });
287
+ return approveAndMergePR(this.socket, childTaskId);
341
288
  }
342
289
  postChildChatMessage(childTaskId, content) {
343
- const socket = this.socket;
344
- if (!socket) throw new Error("Not connected");
345
- return new Promise((resolve2, reject) => {
346
- socket.emit(
347
- "agentRunner:postChildChatMessage",
348
- { childTaskId, content },
349
- (response) => {
350
- if (response.success) resolve2();
351
- else reject(new Error(response.error ?? "Failed to post to child chat"));
352
- }
353
- );
354
- });
290
+ return postChildChatMessage(this.socket, childTaskId, content);
355
291
  }
356
292
  updateChildStatus(childTaskId, status) {
357
- const socket = this.socket;
358
- if (!socket) throw new Error("Not connected");
359
- return new Promise((resolve2, reject) => {
360
- socket.emit(
361
- "agentRunner:updateChildStatus",
362
- { childTaskId, status },
363
- (response) => {
364
- if (response.success) resolve2();
365
- else reject(new Error(response.error ?? "Failed to update child status"));
366
- }
367
- );
368
- });
293
+ return updateChildStatus(this.socket, childTaskId, status);
369
294
  }
370
295
  stopChildBuild(childTaskId) {
371
- const socket = this.socket;
372
- if (!socket) throw new Error("Not connected");
373
- return new Promise((resolve2, reject) => {
374
- socket.emit(
375
- "agentRunner:stopChildBuild",
376
- { childTaskId },
377
- (response) => {
378
- if (response.success) resolve2();
379
- else reject(new Error(response.error ?? "Failed to stop child build"));
380
- }
381
- );
382
- });
296
+ return stopChildBuild(this.socket, childTaskId);
383
297
  }
384
298
  fetchTask(slugOrId) {
385
- const socket = this.socket;
386
- if (!socket) throw new Error("Not connected");
387
- return new Promise((resolve2, reject) => {
388
- socket.emit(
389
- "agentRunner:getTask",
390
- { slugOrId },
391
- (response) => {
392
- if (response.success && response.data) {
393
- resolve2(response.data);
394
- } else {
395
- reject(new Error(response.error ?? "Failed to fetch task"));
396
- }
397
- }
398
- );
399
- });
299
+ return fetchTask(this.socket, slugOrId);
400
300
  }
401
301
  updateTaskProperties(data) {
402
302
  if (!this.socket) throw new Error("Not connected");
403
303
  this.socket.emit("agentRunner:updateTaskProperties", data);
404
304
  }
405
305
  listIcons() {
406
- const socket = this.socket;
407
- if (!socket) throw new Error("Not connected");
408
- return new Promise((resolve2, reject) => {
409
- socket.emit("agentRunner:listIcons", {}, (response) => {
410
- if (response.success && response.data) resolve2(response.data);
411
- else reject(new Error(response.error ?? "Failed to list icons"));
412
- });
413
- });
306
+ return listIcons(this.socket);
414
307
  }
415
308
  generateTaskIcon(prompt, aspectRatio) {
416
- const socket = this.socket;
417
- if (!socket) throw new Error("Not connected");
418
- return new Promise((resolve2, reject) => {
419
- socket.emit(
420
- "agentRunner:generateTaskIcon",
421
- { prompt, aspectRatio },
422
- (response) => {
423
- if (response.success && response.data) resolve2(response.data);
424
- else reject(new Error(response.error ?? "Failed to generate icon"));
425
- }
426
- );
427
- });
309
+ return generateTaskIcon(this.socket, prompt, aspectRatio);
428
310
  }
429
311
  getTaskProperties() {
430
- const socket = this.socket;
431
- if (!socket) throw new Error("Not connected");
432
- return new Promise((resolve2, reject) => {
433
- socket.emit(
434
- "agentRunner:getTaskProperties",
435
- {},
436
- (response) => {
437
- if (response.success && response.data) resolve2(response.data);
438
- else reject(new Error(response.error ?? "Failed to get task properties"));
439
- }
440
- );
441
- });
312
+ return getTaskProperties(this.socket);
442
313
  }
443
314
  triggerIdentification() {
444
- const socket = this.socket;
445
- if (!socket) throw new Error("Not connected");
446
- return new Promise((resolve2, reject) => {
447
- socket.emit(
448
- "agentRunner:triggerIdentification",
449
- {},
450
- (response) => {
451
- if (response.success && response.data) resolve2(response.data);
452
- else reject(new Error(response.error ?? "Identification failed"));
453
- }
454
- );
455
- });
315
+ return triggerIdentification(this.socket);
456
316
  }
457
317
  emitModeTransition(payload) {
458
318
  if (!this.socket) return;
@@ -615,14 +475,52 @@ var ProjectConnection = class {
615
475
  }
616
476
  };
617
477
 
478
+ // src/runner/worktree.ts
479
+ import { execSync } from "child_process";
480
+ import { existsSync } from "fs";
481
+ import { join } from "path";
482
+ var WORKTREE_DIR = ".worktrees";
483
+ function ensureWorktree(projectDir, taskId, branch) {
484
+ const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
485
+ if (existsSync(worktreePath)) {
486
+ if (branch) {
487
+ try {
488
+ execSync(`git checkout --detach origin/${branch}`, {
489
+ cwd: worktreePath,
490
+ stdio: "ignore"
491
+ });
492
+ } catch {
493
+ }
494
+ }
495
+ return worktreePath;
496
+ }
497
+ const ref = branch ? `origin/${branch}` : "HEAD";
498
+ execSync(`git worktree add --detach "${worktreePath}" ${ref}`, {
499
+ cwd: projectDir,
500
+ stdio: "ignore"
501
+ });
502
+ return worktreePath;
503
+ }
504
+ function removeWorktree(projectDir, taskId) {
505
+ const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
506
+ if (!existsSync(worktreePath)) return;
507
+ try {
508
+ execSync(`git worktree remove "${worktreePath}" --force`, {
509
+ cwd: projectDir,
510
+ stdio: "ignore"
511
+ });
512
+ } catch {
513
+ }
514
+ }
515
+
618
516
  // src/setup/config.ts
619
517
  import { readFile } from "fs/promises";
620
- import { join } from "path";
518
+ import { join as join2 } from "path";
621
519
  var CONVEYOR_CONFIG_PATH = ".conveyor/config.json";
622
520
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
623
521
  async function loadForwardPorts(workspaceDir) {
624
522
  try {
625
- const raw = await readFile(join(workspaceDir, DEVCONTAINER_PATH), "utf-8");
523
+ const raw = await readFile(join2(workspaceDir, DEVCONTAINER_PATH), "utf-8");
626
524
  const parsed = JSON.parse(raw);
627
525
  return parsed.forwardPorts ?? [];
628
526
  } catch {
@@ -631,13 +529,13 @@ async function loadForwardPorts(workspaceDir) {
631
529
  }
632
530
  async function loadConveyorConfig(workspaceDir) {
633
531
  try {
634
- const raw = await readFile(join(workspaceDir, CONVEYOR_CONFIG_PATH), "utf-8");
532
+ const raw = await readFile(join2(workspaceDir, CONVEYOR_CONFIG_PATH), "utf-8");
635
533
  const parsed = JSON.parse(raw);
636
534
  if (parsed.setupCommand || parsed.startCommand) return parsed;
637
535
  } catch {
638
536
  }
639
537
  try {
640
- const raw = await readFile(join(workspaceDir, DEVCONTAINER_PATH), "utf-8");
538
+ const raw = await readFile(join2(workspaceDir, DEVCONTAINER_PATH), "utf-8");
641
539
  const parsed = JSON.parse(raw);
642
540
  if (parsed.conveyor && (parsed.conveyor.startCommand || parsed.conveyor.setupCommand)) {
643
541
  return parsed.conveyor;
@@ -692,17 +590,17 @@ function runStartCommand(cmd, cwd, onOutput) {
692
590
  }
693
591
 
694
592
  // src/setup/codespace.ts
695
- import { execSync } from "child_process";
593
+ import { execSync as execSync2 } from "child_process";
696
594
  function initRtk() {
697
595
  try {
698
- execSync("rtk --version", { stdio: "ignore" });
699
- execSync("rtk init --global --auto-patch", { stdio: "ignore" });
596
+ execSync2("rtk --version", { stdio: "ignore" });
597
+ execSync2("rtk init --global --auto-patch", { stdio: "ignore" });
700
598
  } catch {
701
599
  }
702
600
  }
703
601
  function unshallowRepo(workspaceDir) {
704
602
  try {
705
- execSync("git fetch --unshallow", {
603
+ execSync2("git fetch --unshallow", {
706
604
  cwd: workspaceDir,
707
605
  stdio: "ignore",
708
606
  timeout: 6e4
@@ -711,44 +609,6 @@ function unshallowRepo(workspaceDir) {
711
609
  }
712
610
  }
713
611
 
714
- // src/runner/worktree.ts
715
- import { execSync as execSync2 } from "child_process";
716
- import { existsSync } from "fs";
717
- import { join as join2 } from "path";
718
- var WORKTREE_DIR = ".worktrees";
719
- function ensureWorktree(projectDir, taskId, branch) {
720
- const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
721
- if (existsSync(worktreePath)) {
722
- if (branch) {
723
- try {
724
- execSync2(`git checkout --detach origin/${branch}`, {
725
- cwd: worktreePath,
726
- stdio: "ignore"
727
- });
728
- } catch {
729
- }
730
- }
731
- return worktreePath;
732
- }
733
- const ref = branch ? `origin/${branch}` : "HEAD";
734
- execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
735
- cwd: projectDir,
736
- stdio: "ignore"
737
- });
738
- return worktreePath;
739
- }
740
- function removeWorktree(projectDir, taskId) {
741
- const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
742
- if (!existsSync(worktreePath)) return;
743
- try {
744
- execSync2(`git worktree remove "${worktreePath}" --force`, {
745
- cwd: projectDir,
746
- stdio: "ignore"
747
- });
748
- } catch {
749
- }
750
- }
751
-
752
612
  // src/runner/agent-runner.ts
753
613
  import { randomUUID as randomUUID2 } from "crypto";
754
614
  import { execSync as execSync3 } from "child_process";
@@ -785,45 +645,64 @@ async function processAssistantEvent(event, host, turnToolCalls) {
785
645
  }
786
646
  }
787
647
  var API_ERROR_PATTERN = /API Error: [45]\d\d/;
788
- function handleResultEvent(event, host, context, startTime) {
789
- let totalCostUsd = 0;
790
- let retriable = false;
791
- if (event.subtype === "success") {
792
- const successEvent = event;
793
- const queryCostUsd = successEvent.total_cost_usd;
794
- const durationMs = Date.now() - startTime;
795
- const summary = successEvent.result || "Task completed.";
796
- const isImageError = /Could not process image/i.test(summary);
797
- if (isImageError || API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
798
- retriable = true;
799
- }
800
- const cumulativeTotal = host.costTracker.addQueryCost(queryCostUsd);
801
- totalCostUsd = cumulativeTotal;
802
- const { modelUsage } = successEvent;
803
- if (modelUsage && typeof modelUsage === "object") {
804
- host.costTracker.addModelUsage(modelUsage);
805
- }
806
- host.connection.sendEvent({ type: "completed", summary, costUsd: cumulativeTotal, durationMs });
807
- if (cumulativeTotal > 0 && context.agentId && context._runnerSessionId) {
808
- const breakdown = host.costTracker.modelBreakdown;
809
- host.connection.trackSpending({
810
- agentId: context.agentId,
811
- sessionId: context._runnerSessionId,
812
- totalCostUsd: cumulativeTotal,
813
- onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
814
- modelUsage: breakdown.length > 0 ? breakdown : void 0
648
+ var IMAGE_ERROR_PATTERN = /Could not process image/i;
649
+ function isRetriableMessage(msg, durationMs) {
650
+ if (IMAGE_ERROR_PATTERN.test(msg)) return true;
651
+ if (API_ERROR_PATTERN.test(msg) && (durationMs === void 0 || durationMs < 3e4)) return true;
652
+ return false;
653
+ }
654
+ function handleSuccessResult(event, host, context, startTime) {
655
+ const durationMs = Date.now() - startTime;
656
+ const summary = event.result || "Task completed.";
657
+ const retriable = isRetriableMessage(summary, durationMs);
658
+ const cumulativeTotal = host.costTracker.addQueryCost(event.total_cost_usd);
659
+ const { modelUsage } = event;
660
+ if (modelUsage && typeof modelUsage === "object") {
661
+ host.costTracker.addModelUsage(modelUsage);
662
+ }
663
+ host.connection.sendEvent({ type: "completed", summary, costUsd: cumulativeTotal, durationMs });
664
+ if (modelUsage && typeof modelUsage === "object") {
665
+ let queryInputTokens = 0;
666
+ let contextWindow = 0;
667
+ for (const data of Object.values(modelUsage)) {
668
+ queryInputTokens += data.inputTokens ?? 0;
669
+ const cw = data.contextWindow ?? 0;
670
+ if (cw > contextWindow) contextWindow = cw;
671
+ }
672
+ if (contextWindow > 0) {
673
+ host.connection.sendEvent({
674
+ type: "context_update",
675
+ contextTokens: queryInputTokens,
676
+ contextWindow
815
677
  });
816
678
  }
817
- } else {
818
- const errorEvent = event;
819
- const errorMsg = errorEvent.errors.length > 0 ? errorEvent.errors.join(", ") : `Agent stopped: ${errorEvent.subtype}`;
820
- if (API_ERROR_PATTERN.test(errorMsg) || /Could not process image/i.test(errorMsg)) {
821
- retriable = true;
822
- }
823
- host.connection.sendEvent({ type: "error", message: errorMsg });
824
679
  }
680
+ if (cumulativeTotal > 0 && context.agentId && context._runnerSessionId) {
681
+ const breakdown = host.costTracker.modelBreakdown;
682
+ host.connection.trackSpending({
683
+ agentId: context.agentId,
684
+ sessionId: context._runnerSessionId,
685
+ totalCostUsd: cumulativeTotal,
686
+ onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
687
+ modelUsage: breakdown.length > 0 ? breakdown : void 0
688
+ });
689
+ }
690
+ return { totalCostUsd: cumulativeTotal, retriable };
691
+ }
692
+ function handleErrorResult(event, host) {
693
+ const errorMsg = event.errors.length > 0 ? event.errors.join(", ") : `Agent stopped: ${event.subtype}`;
694
+ const retriable = isRetriableMessage(errorMsg);
695
+ host.connection.sendEvent({ type: "error", message: errorMsg });
696
+ return { retriable };
697
+ }
698
+ function handleResultEvent(event, host, context, startTime) {
825
699
  const resultSummary = event.subtype === "success" ? event.result : event.errors.join(", ");
826
- return { totalCostUsd, retriable, resultSummary };
700
+ if (event.subtype === "success") {
701
+ const result2 = handleSuccessResult(event, host, context, startTime);
702
+ return { ...result2, resultSummary };
703
+ }
704
+ const result = handleErrorResult(event, host);
705
+ return { totalCostUsd: 0, ...result, resultSummary };
827
706
  }
828
707
  async function emitResultEvent(event, host, context, startTime) {
829
708
  const result = handleResultEvent(event, host, context, startTime);
@@ -926,256 +805,137 @@ async function processEvents(events, context, host) {
926
805
  }
927
806
 
928
807
  // src/execution/query-executor.ts
929
- import { randomUUID } from "crypto";
930
808
  import {
931
809
  query
932
810
  } from "@anthropic-ai/claude-agent-sdk";
933
811
 
934
- // src/execution/prompt-builder.ts
935
- var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
936
- function formatFileSize(bytes) {
937
- if (bytes === void 0) return "";
938
- if (bytes < 1024) return `${bytes}B`;
939
- if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`;
940
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
941
- }
812
+ // src/execution/pack-runner-prompt.ts
942
813
  function findLastAgentMessageIndex(history) {
943
814
  for (let i = history.length - 1; i >= 0; i--) {
944
815
  if (history[i].role === "assistant") return i;
945
816
  }
946
817
  return -1;
947
818
  }
948
- function detectRelaunchScenario(context) {
949
- const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
950
- if (lastAgentIdx === -1) return "fresh";
951
- const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId || ACTIVE_STATUSES.has(context.status ?? "");
952
- if (!hasPriorWork) return "fresh";
953
- const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
954
- const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
955
- return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
819
+ function formatProjectAgents(projectAgents) {
820
+ const parts = [``, `## Project Agents`];
821
+ for (const pa of projectAgents) {
822
+ const role = pa.role ? `role: ${pa.role}` : "role: unassigned";
823
+ const sp = pa.storyPoints === null || pa.storyPoints === void 0 ? "" : `, story points: ${pa.storyPoints}`;
824
+ parts.push(`- ${pa.agent.name} (${role}${sp})`);
825
+ }
826
+ return parts;
956
827
  }
957
- function buildRelaunchWithSession(mode, context) {
958
- const scenario = detectRelaunchScenario(context);
959
- if (!context.claudeSessionId || scenario === "fresh") return null;
960
- const parts = [];
961
- const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
962
- if (mode === "pm") {
963
- const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
964
- if (newMessages.length > 0) {
965
- parts.push(
966
- `You have been relaunched. Here are new messages since your last session:`,
967
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
968
- );
969
- } else {
970
- parts.push(`You have been relaunched. No new messages since your last session.`);
971
- }
972
- parts.push(
973
- `
974
- You are the project manager for this task.`,
975
- `Review the context above and wait for the team to provide instructions before taking action.`
976
- );
977
- } else if (scenario === "feedback_relaunch") {
978
- const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
979
- parts.push(
980
- `You have been relaunched with new feedback.`,
981
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
982
- `
983
- New messages since your last run:`,
984
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
985
- `
986
- Address the requested changes. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 review the feedback and implement the changes directly.`,
987
- `Commit and push your updates.`
988
- );
989
- if (context.githubPRUrl) {
990
- parts.push(
991
- `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch. Do NOT create a new PR.`
992
- );
993
- } else {
994
- parts.push(
995
- `When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI.`
996
- );
997
- }
998
- } else {
999
- parts.push(
1000
- `You were relaunched but no new instructions have been given since your last run.`,
1001
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1002
- `Run \`git log --oneline -10\` to review what you already committed.`,
1003
- `Review the current state of the codebase and verify everything is working correctly.`,
1004
- `Reply with a brief status update (visible in chat), then wait for further instructions.`
1005
- );
1006
- if (context.githubPRUrl) {
1007
- parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
1008
- }
828
+ function formatStoryPoints(storyPoints) {
829
+ const parts = [``, `## Story Point Tiers`];
830
+ for (const sp of storyPoints) {
831
+ const desc = sp.description ? ` \u2014 ${sp.description}` : "";
832
+ parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
1009
833
  }
1010
- return parts.join("\n");
834
+ return parts;
1011
835
  }
1012
- function buildTaskBody(context) {
1013
- const parts = [];
1014
- parts.push(`# Task: ${context.title}`);
1015
- if (context.description) {
1016
- parts.push(`
1017
- ## Description
1018
- ${context.description}`);
836
+ function buildPackRunnerSystemPrompt(context, config, setupLog) {
837
+ const parts = [
838
+ `You are an autonomous Pack Runner managing child tasks for the "${context.title}" project.`,
839
+ `You are running locally with full access to the repository and task management tools.`,
840
+ `Your job is to sequentially execute child tasks by firing cloud builds, reviewing their PRs, and merging them.`,
841
+ ``,
842
+ `## Child Task Status Lifecycle`,
843
+ `- "Planning" \u2014 Not ready for execution. Skip it (or escalate if blocking).`,
844
+ `- "Open" \u2014 Ready to execute. Use start_child_cloud_build to fire it.`,
845
+ `- "InProgress" \u2014 Currently being worked on by a Task Runner. Wait \u2014 it will move to ReviewPR when done.`,
846
+ `- "ReviewPR" \u2014 Task Runner finished and opened a PR. Review and merge it.`,
847
+ `- "ReviewDev" \u2014 PR was merged to dev. This child is complete. Move on.`,
848
+ `- "Complete" \u2014 Fully done. Move on.`,
849
+ ``,
850
+ `## Autonomous Loop`,
851
+ `Follow this loop each time you are launched or relaunched:`,
852
+ ``,
853
+ `1. Call list_subtasks to see the current state of all child tasks.`,
854
+ ` The response includes PR info (githubPRNumber, githubPRUrl) and agent assignment (agentId).`,
855
+ ``,
856
+ `2. Evaluate each child by status (in ordinal order):`,
857
+ ` - "ReviewPR": Review and merge its PR with approve_and_merge_pr.`,
858
+ ` - If merge fails due to pending CI: post a status update to chat, state you are going idle, and the system will relaunch you to try again.`,
859
+ ` - If merge fails due to failed CI: use get_task_cli(childTaskId) to check what went wrong. Escalate to the team in chat.`,
860
+ ` - "InProgress": A Task Runner is actively working on this child. Do nothing \u2014 wait for it to finish.`,
861
+ ` - "Open": This is the next child to execute. Fire it with start_child_cloud_build.`,
862
+ ` - If it fails because the child is missing story points or an agent: notify the team in chat and go idle.`,
863
+ ` - "ReviewDev" / "Complete": Already done. Skip.`,
864
+ ` - "Planning": Not ready. If this is blocking progress, notify the team.`,
865
+ ``,
866
+ `3. After merging a PR: run \`git pull origin ${context.baseBranch}\` to get the merged changes before firing the next child.`,
867
+ ``,
868
+ `4. After firing a child build: report which task you fired to chat, then explicitly state you are going idle. The system will relaunch you when the child completes or changes status.`,
869
+ ``,
870
+ `5. When ALL children are in "ReviewDev" or "Complete" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with update_task_status("Complete").`,
871
+ ``,
872
+ `## Important Rules`,
873
+ `- Process children ONE at a time, in ordinal order.`,
874
+ `- After firing a child build OR when waiting on CI, explicitly state you are going idle. The system will disconnect you and relaunch when there's a status change.`,
875
+ `- Do NOT attempt to write code yourself. Your role is coordination only.`,
876
+ `- If a child is stuck in "InProgress" for an unusually long time, use get_task_cli(childTaskId) to check its logs and escalate to the team if it appears stuck.`,
877
+ `- You can use get_task(childTaskId) to get a child's full details including PR URL and branch.`,
878
+ `- list_subtasks returns PR info (githubPRNumber, githubPRUrl) and agent assignment (agentId) for each child \u2014 use this to verify readiness before firing builds.`,
879
+ `- You can use read_task_chat to check for team messages.`
880
+ ];
881
+ if (context.storyPoints && context.storyPoints.length > 0) {
882
+ parts.push(...formatStoryPoints(context.storyPoints));
1019
883
  }
1020
- if (context.plan) {
1021
- parts.push(`
1022
- ## Plan
1023
- ${context.plan}`);
884
+ if (context.projectAgents && context.projectAgents.length > 0) {
885
+ parts.push(...formatProjectAgents(context.projectAgents));
1024
886
  }
1025
- if (context.files && context.files.length > 0) {
1026
- parts.push(`
1027
- ## Attached Files`);
1028
- for (const file of context.files) {
1029
- if (file.content && file.contentEncoding === "utf-8") {
1030
- parts.push(`
1031
- ### ${file.fileName} (${file.mimeType})`);
1032
- parts.push("```");
1033
- parts.push(file.content);
1034
- parts.push("```");
1035
- } else if (file.content && file.contentEncoding === "base64") {
1036
- const size = formatFileSize(file.fileSize);
1037
- parts.push(
1038
- `- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_task_file("${file.fileId}") to view]`
1039
- );
1040
- } else if (!file.content) {
1041
- parts.push(`- **${file.fileName}** (${file.mimeType}): ${file.downloadUrl}`);
1042
- }
1043
- }
887
+ if (setupLog.length > 0) {
888
+ parts.push(``, `## Environment setup log`, "```", ...setupLog, "```");
1044
889
  }
1045
- if (context.repoRefs && context.repoRefs.length > 0) {
1046
- parts.push(`
1047
- ## Repository References`);
1048
- for (const ref of context.repoRefs) {
1049
- const icon = ref.refType === "folder" ? "folder" : "file";
1050
- parts.push(`- [${icon}] \`${ref.path}\``);
1051
- }
890
+ if (context.agentInstructions) {
891
+ parts.push(``, `## Agent Instructions`, context.agentInstructions);
1052
892
  }
1053
- if (context.chatHistory.length > 0) {
1054
- const relevant = context.chatHistory.slice(-20);
1055
- parts.push(`
1056
- ## Recent Chat Context`);
1057
- for (const msg of relevant) {
1058
- const sender = msg.userName ?? msg.role;
1059
- parts.push(`[${sender}]: ${msg.content}`);
1060
- if (msg.files?.length) {
1061
- for (const file of msg.files) {
1062
- const sizeStr = file.fileSize ? `, ${formatFileSize(file.fileSize)}` : "";
1063
- if (file.content && file.contentEncoding === "utf-8") {
1064
- parts.push(`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`);
1065
- parts.push("```");
1066
- parts.push(file.content);
1067
- parts.push("```");
1068
- } else if (!file.content) {
1069
- parts.push(
1070
- `[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]: ${file.downloadUrl}`
1071
- );
1072
- } else if (file.content && file.contentEncoding === "base64") {
1073
- parts.push(
1074
- `[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_task_file("${file.fileId}") to view]`
1075
- );
1076
- } else {
1077
- parts.push(`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`);
1078
- }
1079
- }
1080
- }
1081
- }
893
+ if (config.instructions) {
894
+ parts.push(``, `## Additional Instructions`, config.instructions);
1082
895
  }
1083
- return parts;
896
+ parts.push(
897
+ ``,
898
+ `Your responses are sent directly to the task chat \u2014 the team sees everything you say.`,
899
+ `Do NOT call the post_to_chat tool for your own task; your replies already appear in chat automatically.`,
900
+ `Only use post_to_chat if you need to message a different task's chat (e.g. a child task).`,
901
+ `Use read_task_chat only if you need to re-read earlier messages beyond the chat context above.`
902
+ );
903
+ return parts.join("\n");
1084
904
  }
1085
- function buildInstructions(mode, context, scenario, agentMode) {
905
+ function buildPackRunnerInstructions(context, scenario) {
1086
906
  const parts = [`
1087
907
  ## Instructions`];
1088
- const isPm = mode === "pm";
1089
- const isAutoMode = agentMode === "auto";
1090
908
  if (scenario === "fresh") {
1091
- if (isAutoMode && isPm) {
1092
- parts.push(
1093
- `You are operating autonomously. Begin planning immediately.`,
1094
- `1. Explore the codebase to understand the architecture and relevant files`,
1095
- `2. Draft a clear implementation plan and save it with update_task`,
1096
- `3. Set story points (set_story_points), tags (set_task_tags), and title (set_task_title)`,
1097
- `4. When the plan and all required properties are set, call ExitPlanMode to transition to building`,
1098
- `Do NOT wait for team input \u2014 proceed autonomously.`
1099
- );
1100
- } else if (isPm && context.isParentTask) {
1101
- parts.push(
1102
- `You are the project manager for this task and its subtasks.`,
1103
- `Use list_subtasks to review the current state of child tasks.`,
1104
- `The task details are provided above. Wait for the team to provide instructions before taking action.`,
1105
- `When you finish planning, save the plan with update_task and end your turn. Your reply will be visible to the team in chat.`
1106
- );
1107
- } else if (isPm) {
1108
- parts.push(
1109
- `You are the project manager for this task.`,
1110
- `The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`,
1111
- `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.`
1112
- );
1113
- } else {
1114
- parts.push(
1115
- `Begin executing the task plan above immediately.`,
1116
- `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.`,
1117
- `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
1118
- `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.`,
1119
- `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.`
1120
- );
1121
- }
909
+ parts.push(
910
+ `You are the Pack Runner for this task and its subtasks.`,
911
+ `Begin your autonomous loop immediately: call list_subtasks to assess the current state.`,
912
+ `If any child is in "ReviewPR" status, review and merge its PR first.`,
913
+ `Then fire the next "Open" child task.`
914
+ );
1122
915
  } else if (scenario === "idle_relaunch") {
1123
- if (isPm) {
1124
- parts.push(
1125
- `You were relaunched but no new instructions have been given since your last run.`,
1126
- `You are the project manager for this task.`,
1127
- `Wait for the team to provide instructions before taking action.`
1128
- );
1129
- } else {
1130
- parts.push(
1131
- `You were relaunched but no new instructions have been given since your last run.`,
1132
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1133
- `Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
1134
- `Reply with a brief status update summarizing where things stand (visible in chat).`,
1135
- `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
1136
- );
1137
- if (context.githubPRUrl) {
1138
- parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
1139
- }
1140
- }
916
+ parts.push(
917
+ `You have been relaunched \u2014 a child task likely changed status (completed work, opened a PR, or was merged).`,
918
+ `Call list_subtasks to check the current state of all children.`,
919
+ `Look for children in "ReviewPR" status first \u2014 review and merge their PRs.`,
920
+ `If a child you previously fired is now in "ReviewDev", it was merged. Pull latest with \`git pull origin ${context.baseBranch}\` and continue to the next "Open" child.`,
921
+ `If no children need action (all InProgress or waiting), state you are going idle.`
922
+ );
1141
923
  } else {
1142
924
  const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
1143
925
  const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
1144
- if (isPm) {
1145
- parts.push(
1146
- `You were relaunched with new feedback since your last run.`,
1147
- `You are the project manager for this task.`,
1148
- `
1149
- New messages since your last run:`,
1150
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1151
- `
1152
- Review these messages and wait for the team to provide instructions before taking action.`
1153
- );
1154
- } else {
1155
- parts.push(
1156
- `You have been relaunched to address feedback on your previous work.`,
1157
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1158
- `Start by running \`git log --oneline -10\` and \`git diff HEAD~3 HEAD --stat\` to review what you already committed.`,
1159
- `
926
+ parts.push(
927
+ `You have been relaunched with new messages.`,
928
+ `
1160
929
  New messages since your last run:`,
1161
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1162
- `
1163
- Address the requested changes directly. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 go straight to implementing the feedback.`,
1164
- `Commit and push your updates.`
1165
- );
1166
- if (context.githubPRUrl) {
1167
- parts.push(
1168
- `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch to update it. Do NOT create a new PR.`
1169
- );
1170
- } else {
1171
- parts.push(
1172
- `When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
1173
- );
1174
- }
1175
- }
930
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
931
+ `
932
+ After addressing the feedback, resume your autonomous loop: call list_subtasks and proceed accordingly.`
933
+ );
1176
934
  }
1177
935
  return parts;
1178
936
  }
937
+
938
+ // src/execution/mode-prompt.ts
1179
939
  function buildPropertyInstructions(context) {
1180
940
  const parts = [];
1181
941
  parts.push(
@@ -1287,135 +1047,15 @@ function buildModePrompt(agentMode, context) {
1287
1047
  return null;
1288
1048
  }
1289
1049
  }
1290
- function buildPackRunnerSystemPrompt(context, config, setupLog) {
1050
+
1051
+ // src/execution/system-prompt.ts
1052
+ function formatProjectAgentLine(pa) {
1053
+ const role = pa.role ? `role: ${pa.role}` : "role: unassigned";
1054
+ const sp = pa.storyPoints === null || pa.storyPoints === void 0 ? "" : `, story points: ${pa.storyPoints}`;
1055
+ return `- ${pa.agent.name} (${role}${sp})`;
1056
+ }
1057
+ function buildPmPreamble(context) {
1291
1058
  const parts = [
1292
- `You are an autonomous Pack Runner managing child tasks for the "${context.title}" project.`,
1293
- `You are running locally with full access to the repository and task management tools.`,
1294
- `Your job is to sequentially execute child tasks by firing cloud builds, reviewing their PRs, and merging them.`,
1295
- ``,
1296
- `## Child Task Status Lifecycle`,
1297
- `- "Planning" \u2014 Not ready for execution. Skip it (or escalate if blocking).`,
1298
- `- "Open" \u2014 Ready to execute. Use start_child_cloud_build to fire it.`,
1299
- `- "InProgress" \u2014 Currently being worked on by a Task Runner. Wait \u2014 it will move to ReviewPR when done.`,
1300
- `- "ReviewPR" \u2014 Task Runner finished and opened a PR. Review and merge it.`,
1301
- `- "ReviewDev" \u2014 PR was merged to dev. This child is complete. Move on.`,
1302
- `- "Complete" \u2014 Fully done. Move on.`,
1303
- ``,
1304
- `## Autonomous Loop`,
1305
- `Follow this loop each time you are launched or relaunched:`,
1306
- ``,
1307
- `1. Call list_subtasks to see the current state of all child tasks.`,
1308
- ` The response includes PR info (githubPRNumber, githubPRUrl) and agent assignment (agentId).`,
1309
- ``,
1310
- `2. Evaluate each child by status (in ordinal order):`,
1311
- ` - "ReviewPR": Review and merge its PR with approve_and_merge_pr.`,
1312
- ` - If merge fails due to pending CI: post a status update to chat, state you are going idle, and the system will relaunch you to try again.`,
1313
- ` - If merge fails due to failed CI: use get_task_cli(childTaskId) to check what went wrong. Escalate to the team in chat.`,
1314
- ` - "InProgress": A Task Runner is actively working on this child. Do nothing \u2014 wait for it to finish.`,
1315
- ` - "Open": This is the next child to execute. Fire it with start_child_cloud_build.`,
1316
- ` - If it fails because the child is missing story points or an agent: notify the team in chat and go idle.`,
1317
- ` - "ReviewDev" / "Complete": Already done. Skip.`,
1318
- ` - "Planning": Not ready. If this is blocking progress, notify the team.`,
1319
- ``,
1320
- `3. After merging a PR: run \`git pull origin ${context.baseBranch}\` to get the merged changes before firing the next child.`,
1321
- ``,
1322
- `4. After firing a child build: report which task you fired to chat, then explicitly state you are going idle. The system will relaunch you when the child completes or changes status.`,
1323
- ``,
1324
- `5. When ALL children are in "ReviewDev" or "Complete" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with update_task_status("Complete").`,
1325
- ``,
1326
- `## Important Rules`,
1327
- `- Process children ONE at a time, in ordinal order.`,
1328
- `- After firing a child build OR when waiting on CI, explicitly state you are going idle. The system will disconnect you and relaunch when there's a status change.`,
1329
- `- Do NOT attempt to write code yourself. Your role is coordination only.`,
1330
- `- If a child is stuck in "InProgress" for an unusually long time, use get_task_cli(childTaskId) to check its logs and escalate to the team if it appears stuck.`,
1331
- `- You can use get_task(childTaskId) to get a child's full details including PR URL and branch.`,
1332
- `- list_subtasks returns PR info (githubPRNumber, githubPRUrl) and agent assignment (agentId) for each child \u2014 use this to verify readiness before firing builds.`,
1333
- `- You can use read_task_chat to check for team messages.`
1334
- ];
1335
- if (context.storyPoints && context.storyPoints.length > 0) {
1336
- parts.push(``, `## Story Point Tiers`);
1337
- for (const sp of context.storyPoints) {
1338
- const desc = sp.description ? ` \u2014 ${sp.description}` : "";
1339
- parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
1340
- }
1341
- }
1342
- if (context.projectAgents && context.projectAgents.length > 0) {
1343
- parts.push(``, `## Project Agents`);
1344
- for (const pa of context.projectAgents) {
1345
- const role = pa.role ? `role: ${pa.role}` : "role: unassigned";
1346
- const sp = pa.storyPoints != null ? `, story points: ${pa.storyPoints}` : "";
1347
- parts.push(`- ${pa.agent.name} (${role}${sp})`);
1348
- }
1349
- }
1350
- if (setupLog.length > 0) {
1351
- parts.push(``, `## Environment setup log`, "```", ...setupLog, "```");
1352
- }
1353
- if (context.agentInstructions) {
1354
- parts.push(``, `## Agent Instructions`, context.agentInstructions);
1355
- }
1356
- if (config.instructions) {
1357
- parts.push(``, `## Additional Instructions`, config.instructions);
1358
- }
1359
- parts.push(
1360
- ``,
1361
- `Your responses are sent directly to the task chat \u2014 the team sees everything you say.`,
1362
- `Do NOT call the post_to_chat tool for your own task; your replies already appear in chat automatically.`,
1363
- `Only use post_to_chat if you need to message a different task's chat (e.g. a child task).`,
1364
- `Use read_task_chat only if you need to re-read earlier messages beyond the chat context above.`
1365
- );
1366
- return parts.join("\n");
1367
- }
1368
- function buildPackRunnerInstructions(context, scenario) {
1369
- const parts = [`
1370
- ## Instructions`];
1371
- if (scenario === "fresh") {
1372
- parts.push(
1373
- `You are the Pack Runner for this task and its subtasks.`,
1374
- `Begin your autonomous loop immediately: call list_subtasks to assess the current state.`,
1375
- `If any child is in "ReviewPR" status, review and merge its PR first.`,
1376
- `Then fire the next "Open" child task.`
1377
- );
1378
- } else if (scenario === "idle_relaunch") {
1379
- parts.push(
1380
- `You have been relaunched \u2014 a child task likely changed status (completed work, opened a PR, or was merged).`,
1381
- `Call list_subtasks to check the current state of all children.`,
1382
- `Look for children in "ReviewPR" status first \u2014 review and merge their PRs.`,
1383
- `If a child you previously fired is now in "ReviewDev", it was merged. Pull latest with \`git pull origin ${context.baseBranch}\` and continue to the next "Open" child.`,
1384
- `If no children need action (all InProgress or waiting), state you are going idle.`
1385
- );
1386
- } else {
1387
- const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
1388
- const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
1389
- parts.push(
1390
- `You have been relaunched with new messages.`,
1391
- `
1392
- New messages since your last run:`,
1393
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1394
- `
1395
- After addressing the feedback, resume your autonomous loop: call list_subtasks and proceed accordingly.`
1396
- );
1397
- }
1398
- return parts;
1399
- }
1400
- function buildInitialPrompt(mode, context, isAuto, agentMode) {
1401
- const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
1402
- if (!isPackRunner) {
1403
- const sessionRelaunch = buildRelaunchWithSession(mode, context);
1404
- if (sessionRelaunch) return sessionRelaunch;
1405
- }
1406
- const scenario = detectRelaunchScenario(context);
1407
- const body = buildTaskBody(context);
1408
- const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
1409
- return [...body, ...instructions].join("\n");
1410
- }
1411
- function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
1412
- const isPm = mode === "pm";
1413
- const isPmActive = isPm && agentMode === "building";
1414
- const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
1415
- if (isPackRunner) {
1416
- return buildPackRunnerSystemPrompt(context, config, setupLog);
1417
- }
1418
- const pmParts = [
1419
1059
  `You are an AI project manager helping to plan tasks for the "${context.title}" project.`,
1420
1060
  `You are running locally with full access to the repository.`,
1421
1061
  `You can read files, search code, and run shell commands (e.g. git log, git diff) to understand the codebase. You cannot write or edit files.`,
@@ -1431,8 +1071,8 @@ Workflow:`,
1431
1071
  `- 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.`,
1432
1072
  `- A separate task agent will handle execution after the team reviews and approves your plan.`
1433
1073
  ];
1434
- if (isPm && context.isParentTask) {
1435
- pmParts.push(
1074
+ if (context.isParentTask) {
1075
+ parts.push(
1436
1076
  `
1437
1077
  You are the Project Manager for this set of tasks.`,
1438
1078
  `This task has child tasks (subtasks) that are tracked on the board.`,
@@ -1440,26 +1080,27 @@ You are the Project Manager for this set of tasks.`,
1440
1080
  `Use the subtask tools (create_subtask, update_subtask, list_subtasks) to manage work breakdown.`
1441
1081
  );
1442
1082
  }
1443
- if (isPm && context.storyPoints && context.storyPoints.length > 0) {
1444
- pmParts.push(`
1083
+ if (context.storyPoints && context.storyPoints.length > 0) {
1084
+ parts.push(`
1445
1085
  Story Point Tiers:`);
1446
1086
  for (const sp of context.storyPoints) {
1447
1087
  const desc = sp.description ? ` \u2014 ${sp.description}` : "";
1448
- pmParts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
1088
+ parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
1449
1089
  }
1450
1090
  }
1451
- if (isPm && context.projectAgents && context.projectAgents.length > 0) {
1452
- pmParts.push(`
1091
+ if (context.projectAgents && context.projectAgents.length > 0) {
1092
+ parts.push(`
1453
1093
  Project Agents:`);
1454
1094
  for (const pa of context.projectAgents) {
1455
- const role = pa.role ? `role: ${pa.role}` : "role: unassigned";
1456
- const sp = pa.storyPoints != null ? `, story points: ${pa.storyPoints}` : "";
1457
- pmParts.push(`- ${pa.agent.name} (${role}${sp})`);
1095
+ parts.push(formatProjectAgentLine(pa));
1458
1096
  }
1459
1097
  }
1460
- const activeParts = [
1098
+ return parts;
1099
+ }
1100
+ function buildActivePreamble(context, workspaceDir) {
1101
+ return [
1461
1102
  `You are an AI project manager in ACTIVE mode for the "${context.title}" project.`,
1462
- `You have direct coding access to the repository at ${config.workspaceDir}.`,
1103
+ `You have direct coding access to the repository at ${workspaceDir}.`,
1463
1104
  `You can edit files, run tests, and make commits.`,
1464
1105
  `You still have access to all PM tools (subtasks, update_task, chat).`,
1465
1106
  `
@@ -1470,7 +1111,7 @@ Environment (ready, no setup required):`,
1470
1111
  context.githubBranch ? `- You are working on branch: \`${context.githubBranch}\`` : "",
1471
1112
  `
1472
1113
  Safety rules:`,
1473
- `- Stay within the project directory (${config.workspaceDir}).`,
1114
+ `- Stay within the project directory (${workspaceDir}).`,
1474
1115
  `- Do NOT run \`git push --force\` or \`git reset --hard\`. Use \`--force-with-lease\` if needed.`,
1475
1116
  `- Do NOT delete \`.env\` files or modify \`node_modules\`.`,
1476
1117
  `- Do NOT run destructive commands like \`rm -rf /\`.`,
@@ -1480,7 +1121,9 @@ Workflow:`,
1480
1121
  `- When done with changes, summarize what you did in your reply.`,
1481
1122
  `- If you toggled into active mode temporarily, mention when you're done so the team can switch you back to planning mode.`
1482
1123
  ].filter(Boolean);
1483
- const parts = isPmActive ? activeParts : isPm ? pmParts : [
1124
+ }
1125
+ function buildTaskAgentPreamble(context) {
1126
+ return [
1484
1127
  `You are an AI agent working on a task for the "${context.title}" project.`,
1485
1128
  `You are running inside a GitHub Codespace with full access to the repository.`,
1486
1129
  `
@@ -1505,6 +1148,15 @@ Git safety \u2014 STRICT rules:`,
1505
1148
  `- This branch was created from \`${context.baseBranch}\`. PRs will automatically target that branch.`,
1506
1149
  `- 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.`
1507
1150
  ];
1151
+ }
1152
+ function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
1153
+ const isPm = mode === "pm";
1154
+ const isPmActive = isPm && agentMode === "building";
1155
+ const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
1156
+ if (isPackRunner) {
1157
+ return buildPackRunnerSystemPrompt(context, config, setupLog);
1158
+ }
1159
+ const parts = isPmActive ? buildActivePreamble(context, config.workspaceDir) : isPm ? buildPmPreamble(context) : buildTaskAgentPreamble(context);
1508
1160
  if (setupLog.length > 0) {
1509
1161
  parts.push(
1510
1162
  `
@@ -1536,11 +1188,287 @@ Your responses are sent directly to the task chat \u2014 the team sees everythin
1536
1188
  `Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
1537
1189
  );
1538
1190
  }
1539
- const modePrompt = buildModePrompt(agentMode, context);
1540
- if (modePrompt) {
1541
- parts.push(modePrompt);
1191
+ const modePrompt = buildModePrompt(agentMode, context);
1192
+ if (modePrompt) {
1193
+ parts.push(modePrompt);
1194
+ }
1195
+ return parts.join("\n");
1196
+ }
1197
+
1198
+ // src/execution/prompt-builder.ts
1199
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
1200
+ function formatFileSize(bytes) {
1201
+ if (bytes === void 0) return "";
1202
+ if (bytes < 1024) return `${bytes}B`;
1203
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`;
1204
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1205
+ }
1206
+ function findLastAgentMessageIndex2(history) {
1207
+ for (let i = history.length - 1; i >= 0; i--) {
1208
+ if (history[i].role === "assistant") return i;
1209
+ }
1210
+ return -1;
1211
+ }
1212
+ function detectRelaunchScenario(context) {
1213
+ const lastAgentIdx = findLastAgentMessageIndex2(context.chatHistory);
1214
+ if (lastAgentIdx === -1) return "fresh";
1215
+ const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId || ACTIVE_STATUSES.has(context.status ?? "");
1216
+ if (!hasPriorWork) return "fresh";
1217
+ const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
1218
+ const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
1219
+ return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
1220
+ }
1221
+ function buildRelaunchWithSession(mode, context) {
1222
+ const scenario = detectRelaunchScenario(context);
1223
+ if (!context.claudeSessionId || scenario === "fresh") return null;
1224
+ const parts = [];
1225
+ const lastAgentIdx = findLastAgentMessageIndex2(context.chatHistory);
1226
+ if (mode === "pm") {
1227
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
1228
+ if (newMessages.length > 0) {
1229
+ parts.push(
1230
+ `You have been relaunched. Here are new messages since your last session:`,
1231
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
1232
+ );
1233
+ } else {
1234
+ parts.push(`You have been relaunched. No new messages since your last session.`);
1235
+ }
1236
+ parts.push(
1237
+ `
1238
+ You are the project manager for this task.`,
1239
+ `Review the context above and wait for the team to provide instructions before taking action.`
1240
+ );
1241
+ } else if (scenario === "feedback_relaunch") {
1242
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
1243
+ parts.push(
1244
+ `You have been relaunched with new feedback.`,
1245
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1246
+ `
1247
+ New messages since your last run:`,
1248
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1249
+ `
1250
+ Address the requested changes. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 review the feedback and implement the changes directly.`,
1251
+ `Commit and push your updates.`
1252
+ );
1253
+ if (context.githubPRUrl) {
1254
+ parts.push(
1255
+ `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch. Do NOT create a new PR.`
1256
+ );
1257
+ } else {
1258
+ parts.push(
1259
+ `When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI.`
1260
+ );
1261
+ }
1262
+ } else {
1263
+ parts.push(
1264
+ `You were relaunched but no new instructions have been given since your last run.`,
1265
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1266
+ `Run \`git log --oneline -10\` to review what you already committed.`,
1267
+ `Review the current state of the codebase and verify everything is working correctly.`,
1268
+ `Reply with a brief status update (visible in chat), then wait for further instructions.`
1269
+ );
1270
+ if (context.githubPRUrl) {
1271
+ parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
1272
+ }
1273
+ }
1274
+ return parts.join("\n");
1275
+ }
1276
+ function formatChatFile(file) {
1277
+ const sizeStr = file.fileSize ? `, ${formatFileSize(file.fileSize)}` : "";
1278
+ if (file.content && file.contentEncoding === "utf-8") {
1279
+ return [
1280
+ `[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`,
1281
+ "```",
1282
+ file.content,
1283
+ "```"
1284
+ ];
1285
+ }
1286
+ if (!file.content) {
1287
+ return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]: ${file.downloadUrl}`];
1288
+ }
1289
+ if (file.content && file.contentEncoding === "base64") {
1290
+ return [
1291
+ `[Attached image: ${file.fileName} (${file.mimeType}${sizeStr}) \u2014 use get_task_file("${file.fileId}") to view]`
1292
+ ];
1293
+ }
1294
+ return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`];
1295
+ }
1296
+ function formatTaskFile(file) {
1297
+ if (file.content && file.contentEncoding === "utf-8") {
1298
+ return [`
1299
+ ### ${file.fileName} (${file.mimeType})`, "```", file.content, "```"];
1300
+ }
1301
+ if (file.content && file.contentEncoding === "base64") {
1302
+ const size = formatFileSize(file.fileSize);
1303
+ return [
1304
+ `- [Attached image: ${file.fileName} (${file.mimeType}${size ? `, ${size}` : ""}) \u2014 use get_task_file("${file.fileId}") to view]`
1305
+ ];
1306
+ }
1307
+ if (!file.content) {
1308
+ return [`- **${file.fileName}** (${file.mimeType}): ${file.downloadUrl}`];
1309
+ }
1310
+ return [];
1311
+ }
1312
+ function formatChatHistory(chatHistory) {
1313
+ const relevant = chatHistory.slice(-20);
1314
+ const parts = [`
1315
+ ## Recent Chat Context`];
1316
+ for (const msg of relevant) {
1317
+ const sender = msg.userName ?? msg.role;
1318
+ parts.push(`[${sender}]: ${msg.content}`);
1319
+ if (msg.files?.length) {
1320
+ for (const file of msg.files) {
1321
+ parts.push(...formatChatFile(file));
1322
+ }
1323
+ }
1324
+ }
1325
+ return parts;
1326
+ }
1327
+ function buildTaskBody(context) {
1328
+ const parts = [];
1329
+ parts.push(`# Task: ${context.title}`);
1330
+ if (context.description) {
1331
+ parts.push(`
1332
+ ## Description
1333
+ ${context.description}`);
1334
+ }
1335
+ if (context.plan) {
1336
+ parts.push(`
1337
+ ## Plan
1338
+ ${context.plan}`);
1339
+ }
1340
+ if (context.files && context.files.length > 0) {
1341
+ parts.push(`
1342
+ ## Attached Files`);
1343
+ for (const file of context.files) {
1344
+ parts.push(...formatTaskFile(file));
1345
+ }
1346
+ }
1347
+ if (context.repoRefs && context.repoRefs.length > 0) {
1348
+ parts.push(`
1349
+ ## Repository References`);
1350
+ for (const ref of context.repoRefs) {
1351
+ const icon = ref.refType === "folder" ? "folder" : "file";
1352
+ parts.push(`- [${icon}] \`${ref.path}\``);
1353
+ }
1354
+ }
1355
+ if (context.chatHistory.length > 0) {
1356
+ parts.push(...formatChatHistory(context.chatHistory));
1357
+ }
1358
+ return parts;
1359
+ }
1360
+ function buildFreshInstructions(isPm, isAutoMode, context) {
1361
+ if (isAutoMode && isPm) {
1362
+ return [
1363
+ `You are operating autonomously. Begin planning immediately.`,
1364
+ `1. Explore the codebase to understand the architecture and relevant files`,
1365
+ `2. Draft a clear implementation plan and save it with update_task`,
1366
+ `3. Set story points (set_story_points), tags (set_task_tags), and title (set_task_title)`,
1367
+ `4. When the plan and all required properties are set, call ExitPlanMode to transition to building`,
1368
+ `Do NOT wait for team input \u2014 proceed autonomously.`
1369
+ ];
1370
+ }
1371
+ if (isPm && context.isParentTask) {
1372
+ return [
1373
+ `You are the project manager for this task and its subtasks.`,
1374
+ `Use list_subtasks to review the current state of child tasks.`,
1375
+ `The task details are provided above. Wait for the team to provide instructions before taking action.`,
1376
+ `When you finish planning, save the plan with update_task and end your turn. Your reply will be visible to the team in chat.`
1377
+ ];
1378
+ }
1379
+ if (isPm) {
1380
+ return [
1381
+ `You are the project manager for this task.`,
1382
+ `The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`,
1383
+ `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.`
1384
+ ];
1385
+ }
1386
+ return [
1387
+ `Begin executing the task plan above immediately.`,
1388
+ `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.`,
1389
+ `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
1390
+ `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.`,
1391
+ `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.`
1392
+ ];
1393
+ }
1394
+ function buildFeedbackInstructions(context, isPm) {
1395
+ const lastAgentIdx = findLastAgentMessageIndex2(context.chatHistory);
1396
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
1397
+ if (isPm) {
1398
+ return [
1399
+ `You were relaunched with new feedback since your last run.`,
1400
+ `You are the project manager for this task.`,
1401
+ `
1402
+ New messages since your last run:`,
1403
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1404
+ `
1405
+ Review these messages and wait for the team to provide instructions before taking action.`
1406
+ ];
1407
+ }
1408
+ const parts = [
1409
+ `You have been relaunched to address feedback on your previous work.`,
1410
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1411
+ `Start by running \`git log --oneline -10\` and \`git diff HEAD~3 HEAD --stat\` to review what you already committed.`,
1412
+ `
1413
+ New messages since your last run:`,
1414
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1415
+ `
1416
+ Address the requested changes directly. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 go straight to implementing the feedback.`,
1417
+ `Commit and push your updates.`
1418
+ ];
1419
+ if (context.githubPRUrl) {
1420
+ parts.push(
1421
+ `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch to update it. Do NOT create a new PR.`
1422
+ );
1423
+ } else {
1424
+ parts.push(
1425
+ `When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
1426
+ );
1427
+ }
1428
+ return parts;
1429
+ }
1430
+ function buildInstructions(mode, context, scenario, agentMode) {
1431
+ const parts = [`
1432
+ ## Instructions`];
1433
+ const isPm = mode === "pm";
1434
+ if (scenario === "fresh") {
1435
+ parts.push(...buildFreshInstructions(isPm, agentMode === "auto", context));
1436
+ return parts;
1437
+ }
1438
+ if (scenario === "idle_relaunch") {
1439
+ if (isPm) {
1440
+ parts.push(
1441
+ `You were relaunched but no new instructions have been given since your last run.`,
1442
+ `You are the project manager for this task.`,
1443
+ `Wait for the team to provide instructions before taking action.`
1444
+ );
1445
+ } else {
1446
+ parts.push(
1447
+ `You were relaunched but no new instructions have been given since your last run.`,
1448
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
1449
+ `Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
1450
+ `Reply with a brief status update summarizing where things stand (visible in chat).`,
1451
+ `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
1452
+ );
1453
+ if (context.githubPRUrl) {
1454
+ parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
1455
+ }
1456
+ }
1457
+ return parts;
1458
+ }
1459
+ parts.push(...buildFeedbackInstructions(context, isPm));
1460
+ return parts;
1461
+ }
1462
+ function buildInitialPrompt(mode, context, isAuto, agentMode) {
1463
+ const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
1464
+ if (!isPackRunner) {
1465
+ const sessionRelaunch = buildRelaunchWithSession(mode, context);
1466
+ if (sessionRelaunch) return sessionRelaunch;
1542
1467
  }
1543
- return parts.join("\n");
1468
+ const scenario = detectRelaunchScenario(context);
1469
+ const body = buildTaskBody(context);
1470
+ const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
1471
+ return [...body, ...instructions].join("\n");
1544
1472
  }
1545
1473
 
1546
1474
  // src/tools/index.ts
@@ -1552,212 +1480,218 @@ import { z } from "zod";
1552
1480
  function isImageMimeType(mimeType) {
1553
1481
  return mimeType.startsWith("image/");
1554
1482
  }
1483
+ var cliEventFormatters = {
1484
+ thinking: (e) => e.message ?? "",
1485
+ tool_use: (e) => `${e.tool}: ${e.input?.slice(0, 1e3) ?? ""}`,
1486
+ tool_result: (e) => `${e.tool} \u2192 ${e.output?.slice(0, 500) ?? ""}${e.isError ? " [ERROR]" : ""}`,
1487
+ message: (e) => e.content ?? "",
1488
+ error: (e) => `ERROR: ${e.message ?? ""}`,
1489
+ completed: (e) => `Completed: ${e.summary ?? ""} (cost: $${e.costUsd ?? "?"}, duration: ${e.durationMs ?? "?"}ms)`,
1490
+ setup_output: (e) => `[${e.stream ?? "stdout"}] ${e.data ?? ""}`,
1491
+ start_command_output: (e) => `[${e.stream ?? "stdout"}] ${e.data ?? ""}`,
1492
+ turn_end: (e) => `Turn complete (${e.toolCalls?.length ?? 0} tool calls)`
1493
+ };
1555
1494
  function formatCliEvent(e) {
1556
- switch (e.type) {
1557
- case "thinking":
1558
- return e.message ?? "";
1559
- case "tool_use": {
1560
- const tu = e;
1561
- return `${tu.tool}: ${tu.input?.slice(0, 1e3) ?? ""}`;
1562
- }
1563
- case "tool_result": {
1564
- const tr = e;
1565
- return `${tr.tool} \u2192 ${tr.output?.slice(0, 500) ?? ""}${tr.isError ? " [ERROR]" : ""}`;
1566
- }
1567
- case "message":
1568
- return e.content ?? "";
1569
- case "error":
1570
- return `ERROR: ${e.message ?? ""}`;
1571
- case "completed": {
1572
- const c = e;
1573
- return `Completed: ${c.summary ?? ""} (cost: $${c.costUsd ?? "?"}, duration: ${c.durationMs ?? "?"}ms)`;
1574
- }
1575
- case "setup_output":
1576
- case "start_command_output": {
1577
- const so = e;
1578
- return `[${so.stream ?? "stdout"}] ${so.data ?? ""}`;
1579
- }
1580
- case "turn_end": {
1581
- const te = e;
1582
- return `Turn complete (${te.toolCalls?.length ?? 0} tool calls)`;
1583
- }
1584
- default:
1585
- return JSON.stringify(e);
1586
- }
1495
+ const formatter = cliEventFormatters[e.type];
1496
+ return formatter ? formatter(e) : JSON.stringify(e);
1587
1497
  }
1588
- function buildCommonTools(connection, config) {
1589
- return [
1590
- tool(
1591
- "read_task_chat",
1592
- "Read recent messages from a task chat. Omit task_id to read the current task's chat, or provide a child task ID to read a child's chat.",
1593
- {
1594
- limit: z.number().optional().describe("Number of recent messages to fetch (default 20)"),
1595
- task_id: z.string().optional().describe("Child task ID to read chat from. Omit to read the current task's chat.")
1596
- },
1597
- async ({ limit, task_id }) => {
1598
- try {
1599
- const messages = await connection.fetchChatMessages(limit, task_id);
1600
- return textResult(JSON.stringify(messages, null, 2));
1601
- } catch {
1602
- return textResult(
1603
- JSON.stringify({
1604
- note: "Could not fetch live chat. Chat history was provided in the initial context."
1605
- })
1606
- );
1607
- }
1608
- },
1609
- { annotations: { readOnlyHint: true } }
1610
- ),
1611
- tool(
1612
- "post_to_chat",
1613
- "Post a message to a task chat. Your normal replies already appear in chat \u2014 only use this for explicit out-of-band updates or posting to a child task's chat.",
1614
- {
1615
- message: z.string().describe("The message to post to the team"),
1616
- task_id: z.string().optional().describe("Child task ID to post to. Omit to post to the current task's chat.")
1617
- },
1618
- async ({ message, task_id }) => {
1619
- try {
1620
- if (task_id) {
1621
- await connection.postChildChatMessage(task_id, message);
1622
- return textResult(`Message posted to child task ${task_id} chat.`);
1623
- }
1624
- connection.postChatMessage(message);
1625
- return textResult("Message posted to task chat.");
1626
- } catch (error) {
1627
- return textResult(
1628
- `Failed to post message: ${error instanceof Error ? error.message : "Unknown error"}`
1629
- );
1630
- }
1498
+ function buildReadTaskChatTool(connection) {
1499
+ return tool(
1500
+ "read_task_chat",
1501
+ "Read recent messages from a task chat. Omit task_id to read the current task's chat, or provide a child task ID to read a child's chat.",
1502
+ {
1503
+ limit: z.number().optional().describe("Number of recent messages to fetch (default 20)"),
1504
+ task_id: z.string().optional().describe("Child task ID to read chat from. Omit to read the current task's chat.")
1505
+ },
1506
+ async ({ limit, task_id }) => {
1507
+ try {
1508
+ const messages = await connection.fetchChatMessages(limit, task_id);
1509
+ return textResult(JSON.stringify(messages, null, 2));
1510
+ } catch {
1511
+ return textResult(
1512
+ JSON.stringify({
1513
+ note: "Could not fetch live chat. Chat history was provided in the initial context."
1514
+ })
1515
+ );
1631
1516
  }
1632
- ),
1633
- tool(
1634
- "update_task_status",
1635
- "Update a task's status on the Kanban board. Omit task_id to update the current task, or provide a child task ID to update a child's status.",
1636
- {
1637
- status: z.enum(["InProgress", "ReviewPR", "ReviewDev", "Complete"]).describe("The new status for the task"),
1638
- task_id: z.string().optional().describe("Child task ID to update. Omit to update the current task.")
1639
- },
1640
- async ({ status, task_id }) => {
1641
- try {
1642
- if (task_id) {
1643
- await connection.updateChildStatus(task_id, status);
1644
- return textResult(`Child task ${task_id} status updated to ${status}.`);
1645
- }
1646
- connection.updateStatus(status);
1647
- return textResult(`Task status updated to ${status}.`);
1648
- } catch (error) {
1649
- return textResult(
1650
- `Failed to update status: ${error instanceof Error ? error.message : "Unknown error"}`
1651
- );
1517
+ },
1518
+ { annotations: { readOnlyHint: true } }
1519
+ );
1520
+ }
1521
+ function buildPostToChatTool(connection) {
1522
+ return tool(
1523
+ "post_to_chat",
1524
+ "Post a message to a task chat. Your normal replies already appear in chat \u2014 only use this for explicit out-of-band updates or posting to a child task's chat.",
1525
+ {
1526
+ message: z.string().describe("The message to post to the team"),
1527
+ task_id: z.string().optional().describe("Child task ID to post to. Omit to post to the current task's chat.")
1528
+ },
1529
+ async ({ message, task_id }) => {
1530
+ try {
1531
+ if (task_id) {
1532
+ await connection.postChildChatMessage(task_id, message);
1533
+ return textResult(`Message posted to child task ${task_id} chat.`);
1652
1534
  }
1535
+ connection.postChatMessage(message);
1536
+ return textResult("Message posted to task chat.");
1537
+ } catch (error) {
1538
+ return textResult(
1539
+ `Failed to post message: ${error instanceof Error ? error.message : "Unknown error"}`
1540
+ );
1653
1541
  }
1654
- ),
1655
- tool(
1656
- "get_task_plan",
1657
- "Re-read the latest task plan in case it was updated",
1658
- {},
1659
- async () => {
1660
- try {
1661
- const ctx = await connection.fetchTaskContext();
1662
- return textResult(ctx.plan ?? "No plan available.");
1663
- } catch {
1664
- return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
1665
- }
1666
- },
1667
- { annotations: { readOnlyHint: true } }
1668
- ),
1669
- tool(
1670
- "get_task",
1671
- "Look up a task by slug or ID to get its title, description, plan, and status",
1672
- {
1673
- slug_or_id: z.string().describe("The task slug (e.g. 'my-task') or CUID")
1674
- },
1675
- async ({ slug_or_id }) => {
1676
- try {
1677
- const task = await connection.fetchTask(slug_or_id);
1678
- return textResult(JSON.stringify(task, null, 2));
1679
- } catch (error) {
1680
- return textResult(
1681
- `Failed to get task: ${error instanceof Error ? error.message : "Unknown error"}`
1682
- );
1683
- }
1684
- },
1685
- { annotations: { readOnlyHint: true } }
1686
- ),
1687
- tool(
1688
- "get_task_cli",
1689
- "Read CLI execution logs from a task. Returns agent reasoning, tool calls, setup output, and other execution events. Use 'source' to filter: 'agent' for agent reasoning/tool calls only, 'application' for setup/dev-server output only.",
1690
- {
1691
- task_id: z.string().optional().describe("Task ID or slug. Omit to read logs from the current task."),
1692
- source: z.enum(["agent", "application"]).optional().describe("Filter by log source. Omit for all logs."),
1693
- limit: z.number().optional().describe("Max number of log entries to return (default 50, max 500).")
1694
- },
1695
- async ({ task_id, source, limit }) => {
1696
- try {
1697
- const effectiveLimit = Math.min(limit ?? 50, 500);
1698
- const result = await connection.fetchCliHistory(task_id, effectiveLimit, source);
1699
- const formatted = result.map((entry) => {
1700
- const time = entry.time;
1701
- const e = entry.event;
1702
- return `[${time}] [${e.type}] ${formatCliEvent(e)}`;
1703
- }).join("\n");
1704
- return textResult(formatted || "No CLI logs found.");
1705
- } catch (error) {
1706
- return textResult(
1707
- `Failed to fetch CLI logs: ${error instanceof Error ? error.message : "Unknown error"}`
1708
- );
1709
- }
1710
- },
1711
- { annotations: { readOnlyHint: true } }
1712
- ),
1713
- tool(
1714
- "list_task_files",
1715
- "List all files attached to this task with metadata (name, type, size) and download URLs",
1716
- {},
1717
- async () => {
1718
- try {
1719
- const files = await connection.fetchTaskFiles();
1720
- const metadata = files.map(({ content: _c, ...rest }) => rest);
1721
- const content = [
1722
- { type: "text", text: JSON.stringify(metadata, null, 2) }
1723
- ];
1724
- for (const file of files) {
1725
- if (file.content && file.contentEncoding === "base64" && isImageMimeType(file.mimeType)) {
1726
- content.push(imageBlock(file.content, file.mimeType));
1727
- }
1728
- }
1729
- return { content };
1730
- } catch {
1731
- return textResult("Failed to list task files.");
1542
+ }
1543
+ );
1544
+ }
1545
+ function buildUpdateTaskStatusTool(connection) {
1546
+ return tool(
1547
+ "update_task_status",
1548
+ "Update a task's status on the Kanban board. Omit task_id to update the current task, or provide a child task ID to update a child's status.",
1549
+ {
1550
+ status: z.enum(["InProgress", "ReviewPR", "ReviewDev", "Complete"]).describe("The new status for the task"),
1551
+ task_id: z.string().optional().describe("Child task ID to update. Omit to update the current task.")
1552
+ },
1553
+ async ({ status, task_id }) => {
1554
+ try {
1555
+ if (task_id) {
1556
+ await connection.updateChildStatus(task_id, status);
1557
+ return textResult(`Child task ${task_id} status updated to ${status}.`);
1732
1558
  }
1733
- },
1734
- { annotations: { readOnlyHint: true } }
1735
- ),
1736
- tool(
1737
- "get_task_file",
1738
- "Get a specific task file's content and download URL by file ID",
1739
- { fileId: z.string().describe("The file ID to retrieve") },
1740
- async ({ fileId }) => {
1741
- try {
1742
- const file = await connection.fetchTaskFile(fileId);
1743
- const { content: rawContent, ...metadata } = file;
1744
- const content = [
1745
- { type: "text", text: JSON.stringify(metadata, null, 2) }
1746
- ];
1747
- if (rawContent && file.contentEncoding === "base64" && isImageMimeType(file.mimeType)) {
1748
- content.push(imageBlock(rawContent, file.mimeType));
1749
- } else if (rawContent) {
1750
- content[0] = { type: "text", text: JSON.stringify(file, null, 2) };
1559
+ connection.updateStatus(status);
1560
+ return textResult(`Task status updated to ${status}.`);
1561
+ } catch (error) {
1562
+ return textResult(
1563
+ `Failed to update status: ${error instanceof Error ? error.message : "Unknown error"}`
1564
+ );
1565
+ }
1566
+ }
1567
+ );
1568
+ }
1569
+ function buildGetTaskPlanTool(connection, config) {
1570
+ return tool(
1571
+ "get_task_plan",
1572
+ "Re-read the latest task plan in case it was updated",
1573
+ {},
1574
+ async () => {
1575
+ try {
1576
+ const ctx = await connection.fetchTaskContext();
1577
+ return textResult(ctx.plan ?? "No plan available.");
1578
+ } catch {
1579
+ return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
1580
+ }
1581
+ },
1582
+ { annotations: { readOnlyHint: true } }
1583
+ );
1584
+ }
1585
+ function buildGetTaskTool(connection) {
1586
+ return tool(
1587
+ "get_task",
1588
+ "Look up a task by slug or ID to get its title, description, plan, and status",
1589
+ {
1590
+ slug_or_id: z.string().describe("The task slug (e.g. 'my-task') or CUID")
1591
+ },
1592
+ async ({ slug_or_id }) => {
1593
+ try {
1594
+ const task = await connection.fetchTask(slug_or_id);
1595
+ return textResult(JSON.stringify(task, null, 2));
1596
+ } catch (error) {
1597
+ return textResult(
1598
+ `Failed to get task: ${error instanceof Error ? error.message : "Unknown error"}`
1599
+ );
1600
+ }
1601
+ },
1602
+ { annotations: { readOnlyHint: true } }
1603
+ );
1604
+ }
1605
+ function buildGetTaskCliTool(connection) {
1606
+ return tool(
1607
+ "get_task_cli",
1608
+ "Read CLI execution logs from a task. Returns agent reasoning, tool calls, setup output, and other execution events. Use 'source' to filter: 'agent' for agent reasoning/tool calls only, 'application' for setup/dev-server output only.",
1609
+ {
1610
+ task_id: z.string().optional().describe("Task ID or slug. Omit to read logs from the current task."),
1611
+ source: z.enum(["agent", "application"]).optional().describe("Filter by log source. Omit for all logs."),
1612
+ limit: z.number().optional().describe("Max number of log entries to return (default 50, max 500).")
1613
+ },
1614
+ async ({ task_id, source, limit }) => {
1615
+ try {
1616
+ const effectiveLimit = Math.min(limit ?? 50, 500);
1617
+ const result = await connection.fetchCliHistory(task_id, effectiveLimit, source);
1618
+ const formatted = result.map((entry) => {
1619
+ const time = entry.time;
1620
+ const e = entry.event;
1621
+ return `[${time}] [${e.type}] ${formatCliEvent(e)}`;
1622
+ }).join("\n");
1623
+ return textResult(formatted || "No CLI logs found.");
1624
+ } catch (error) {
1625
+ return textResult(
1626
+ `Failed to fetch CLI logs: ${error instanceof Error ? error.message : "Unknown error"}`
1627
+ );
1628
+ }
1629
+ },
1630
+ { annotations: { readOnlyHint: true } }
1631
+ );
1632
+ }
1633
+ function buildListTaskFilesTool(connection) {
1634
+ return tool(
1635
+ "list_task_files",
1636
+ "List all files attached to this task with metadata (name, type, size) and download URLs",
1637
+ {},
1638
+ async () => {
1639
+ try {
1640
+ const files = await connection.fetchTaskFiles();
1641
+ const metadata = files.map(({ content: _c, ...rest }) => rest);
1642
+ const content = [
1643
+ { type: "text", text: JSON.stringify(metadata, null, 2) }
1644
+ ];
1645
+ for (const file of files) {
1646
+ if (file.content && file.contentEncoding === "base64" && isImageMimeType(file.mimeType)) {
1647
+ content.push(imageBlock(file.content, file.mimeType));
1751
1648
  }
1752
- return { content };
1753
- } catch (error) {
1754
- return textResult(
1755
- `Failed to get task file: ${error instanceof Error ? error.message : "Unknown error"}`
1756
- );
1757
1649
  }
1758
- },
1759
- { annotations: { readOnlyHint: true } }
1760
- )
1650
+ return { content };
1651
+ } catch {
1652
+ return textResult("Failed to list task files.");
1653
+ }
1654
+ },
1655
+ { annotations: { readOnlyHint: true } }
1656
+ );
1657
+ }
1658
+ function buildGetTaskFileTool(connection) {
1659
+ return tool(
1660
+ "get_task_file",
1661
+ "Get a specific task file's content and download URL by file ID",
1662
+ { fileId: z.string().describe("The file ID to retrieve") },
1663
+ async ({ fileId }) => {
1664
+ try {
1665
+ const file = await connection.fetchTaskFile(fileId);
1666
+ const { content: rawContent, ...metadata } = file;
1667
+ const content = [
1668
+ { type: "text", text: JSON.stringify(metadata, null, 2) }
1669
+ ];
1670
+ if (rawContent && file.contentEncoding === "base64" && isImageMimeType(file.mimeType)) {
1671
+ content.push(imageBlock(rawContent, file.mimeType));
1672
+ } else if (rawContent) {
1673
+ content[0] = { type: "text", text: JSON.stringify(file, null, 2) };
1674
+ }
1675
+ return { content };
1676
+ } catch (error) {
1677
+ return textResult(
1678
+ `Failed to get task file: ${error instanceof Error ? error.message : "Unknown error"}`
1679
+ );
1680
+ }
1681
+ },
1682
+ { annotations: { readOnlyHint: true } }
1683
+ );
1684
+ }
1685
+ function buildCommonTools(connection, config) {
1686
+ return [
1687
+ buildReadTaskChatTool(connection),
1688
+ buildPostToChatTool(connection),
1689
+ buildUpdateTaskStatusTool(connection),
1690
+ buildGetTaskPlanTool(connection, config),
1691
+ buildGetTaskTool(connection),
1692
+ buildGetTaskCliTool(connection),
1693
+ buildListTaskFilesTool(connection),
1694
+ buildGetTaskFileTool(connection)
1761
1695
  ];
1762
1696
  }
1763
1697
 
@@ -1771,26 +1705,8 @@ function buildStoryPointDescription(storyPoints) {
1771
1705
  }
1772
1706
  return "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)";
1773
1707
  }
1774
- function buildPmTools(connection, storyPoints, options) {
1775
- const spDescription = buildStoryPointDescription(storyPoints);
1776
- const includePackTools = options?.includePackTools ?? false;
1777
- const tools = [
1778
- tool2(
1779
- "update_task",
1780
- "Save the finalized task plan and/or description",
1781
- {
1782
- plan: z2.string().optional().describe("The task plan in markdown"),
1783
- description: z2.string().optional().describe("Updated task description")
1784
- },
1785
- async ({ plan, description }) => {
1786
- try {
1787
- await Promise.resolve(connection.updateTaskFields({ plan, description }));
1788
- return textResult("Task updated successfully.");
1789
- } catch {
1790
- return textResult("Failed to update task.");
1791
- }
1792
- }
1793
- ),
1708
+ function buildSubtaskTools(connection, spDescription) {
1709
+ return [
1794
1710
  tool2(
1795
1711
  "create_subtask",
1796
1712
  "Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
@@ -1860,9 +1776,33 @@ function buildPmTools(connection, storyPoints, options) {
1860
1776
  { annotations: { readOnlyHint: true } }
1861
1777
  )
1862
1778
  ];
1863
- if (!includePackTools) return tools;
1779
+ }
1780
+ function buildPmTools(connection, storyPoints, options) {
1781
+ const spDescription = buildStoryPointDescription(storyPoints);
1782
+ const tools = [
1783
+ tool2(
1784
+ "update_task",
1785
+ "Save the finalized task plan and/or description",
1786
+ {
1787
+ plan: z2.string().optional().describe("The task plan in markdown"),
1788
+ description: z2.string().optional().describe("Updated task description")
1789
+ },
1790
+ async ({ plan, description }) => {
1791
+ try {
1792
+ await Promise.resolve(connection.updateTaskFields({ plan, description }));
1793
+ return textResult("Task updated successfully.");
1794
+ } catch {
1795
+ return textResult("Failed to update task.");
1796
+ }
1797
+ }
1798
+ ),
1799
+ ...buildSubtaskTools(connection, spDescription)
1800
+ ];
1801
+ if (!options?.includePackTools) return tools;
1802
+ return [...tools, ...buildPackTools(connection)];
1803
+ }
1804
+ function buildPackTools(connection) {
1864
1805
  return [
1865
- ...tools,
1866
1806
  tool2(
1867
1807
  "start_child_cloud_build",
1868
1808
  "Start a cloud build for a child task. The child must be in Open status with story points and an agent assigned.",
@@ -1948,10 +1888,67 @@ function buildTaskTools(connection) {
1948
1888
  )
1949
1889
  ];
1950
1890
  }
1951
-
1952
- // src/tools/discovery-tools.ts
1953
- import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
1954
- import { z as z4 } from "zod";
1891
+
1892
+ // src/tools/discovery-tools.ts
1893
+ import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
1894
+ import { z as z4 } from "zod";
1895
+ function buildIconTools(connection) {
1896
+ return [
1897
+ tool4(
1898
+ "list_icons",
1899
+ "List available icons (default library + user-created). Returns icon IDs, names, and whether they're defaults. Call this FIRST before set_task_icon to check for existing matches.",
1900
+ {},
1901
+ async () => {
1902
+ try {
1903
+ const icons = await connection.listIcons();
1904
+ return textResult(JSON.stringify(icons, null, 2));
1905
+ } catch (error) {
1906
+ return textResult(
1907
+ `Failed to list icons: ${error instanceof Error ? error.message : "Unknown error"}`
1908
+ );
1909
+ }
1910
+ },
1911
+ { annotations: { readOnlyHint: true } }
1912
+ ),
1913
+ tool4(
1914
+ "set_task_icon",
1915
+ "Assign an existing icon to this task by its ID. Use list_icons first to find a matching icon.",
1916
+ {
1917
+ iconId: z4.string().describe("The icon ID to assign")
1918
+ },
1919
+ async ({ iconId }) => {
1920
+ try {
1921
+ await Promise.resolve(connection.updateTaskProperties({ iconId }));
1922
+ return textResult("Icon assigned to task.");
1923
+ } catch (error) {
1924
+ return textResult(
1925
+ `Failed to set icon: ${error instanceof Error ? error.message : "Unknown error"}`
1926
+ );
1927
+ }
1928
+ }
1929
+ ),
1930
+ tool4(
1931
+ "generate_task_icon",
1932
+ "Generate a new SVG icon using AI and assign it to this task. Only use if no existing icon from list_icons is a good fit. Provide a concise visual description.",
1933
+ {
1934
+ prompt: z4.string().describe(
1935
+ "Description of the icon to generate (e.g. 'minimal flat gear and wrench icon')"
1936
+ ),
1937
+ aspectRatio: z4.enum(["auto", "portrait", "landscape", "square"]).optional().describe("Icon aspect ratio, defaults to square")
1938
+ },
1939
+ async ({ prompt, aspectRatio }) => {
1940
+ try {
1941
+ const result = await connection.generateTaskIcon(prompt, aspectRatio ?? "square");
1942
+ return textResult(`Icon generated and assigned: ${result.iconId}`);
1943
+ } catch (error) {
1944
+ return textResult(
1945
+ `Failed to generate icon: ${error instanceof Error ? error.message : "Unknown error"}`
1946
+ );
1947
+ }
1948
+ }
1949
+ )
1950
+ ];
1951
+ }
1955
1952
  function buildDiscoveryTools(connection, context) {
1956
1953
  const spDescription = buildStoryPointDescription(context?.storyPoints);
1957
1954
  return [
@@ -1961,7 +1958,7 @@ function buildDiscoveryTools(connection, context) {
1961
1958
  { value: z4.number().describe(spDescription) },
1962
1959
  async ({ value }) => {
1963
1960
  try {
1964
- connection.updateTaskProperties({ storyPointValue: value });
1961
+ await Promise.resolve(connection.updateTaskProperties({ storyPointValue: value }));
1965
1962
  return textResult(`Story points set to ${value}`);
1966
1963
  } catch (error) {
1967
1964
  return textResult(
@@ -1978,7 +1975,7 @@ function buildDiscoveryTools(connection, context) {
1978
1975
  },
1979
1976
  async ({ tagIds }) => {
1980
1977
  try {
1981
- connection.updateTaskProperties({ tagIds });
1978
+ await Promise.resolve(connection.updateTaskProperties({ tagIds }));
1982
1979
  return textResult(`Tags assigned: ${tagIds.length} tag(s)`);
1983
1980
  } catch (error) {
1984
1981
  return textResult(
@@ -1995,7 +1992,7 @@ function buildDiscoveryTools(connection, context) {
1995
1992
  },
1996
1993
  async ({ title }) => {
1997
1994
  try {
1998
- connection.updateTaskProperties({ title });
1995
+ await Promise.resolve(connection.updateTaskProperties({ title }));
1999
1996
  return textResult(`Task title updated to: ${title}`);
2000
1997
  } catch (error) {
2001
1998
  return textResult(
@@ -2004,59 +2001,7 @@ function buildDiscoveryTools(connection, context) {
2004
2001
  }
2005
2002
  }
2006
2003
  ),
2007
- tool4(
2008
- "list_icons",
2009
- "List available icons (default library + user-created). Returns icon IDs, names, and whether they're defaults. Call this FIRST before set_task_icon to check for existing matches.",
2010
- {},
2011
- async () => {
2012
- try {
2013
- const icons = await connection.listIcons();
2014
- return textResult(JSON.stringify(icons, null, 2));
2015
- } catch (error) {
2016
- return textResult(
2017
- `Failed to list icons: ${error instanceof Error ? error.message : "Unknown error"}`
2018
- );
2019
- }
2020
- },
2021
- { annotations: { readOnlyHint: true } }
2022
- ),
2023
- tool4(
2024
- "set_task_icon",
2025
- "Assign an existing icon to this task by its ID. Use list_icons first to find a matching icon.",
2026
- {
2027
- iconId: z4.string().describe("The icon ID to assign")
2028
- },
2029
- async ({ iconId }) => {
2030
- try {
2031
- connection.updateTaskProperties({ iconId });
2032
- return textResult("Icon assigned to task.");
2033
- } catch (error) {
2034
- return textResult(
2035
- `Failed to set icon: ${error instanceof Error ? error.message : "Unknown error"}`
2036
- );
2037
- }
2038
- }
2039
- ),
2040
- tool4(
2041
- "generate_task_icon",
2042
- "Generate a new SVG icon using AI and assign it to this task. Only use if no existing icon from list_icons is a good fit. Provide a concise visual description.",
2043
- {
2044
- prompt: z4.string().describe(
2045
- "Description of the icon to generate (e.g. 'minimal flat gear and wrench icon')"
2046
- ),
2047
- aspectRatio: z4.enum(["auto", "portrait", "landscape", "square"]).optional().describe("Icon aspect ratio, defaults to square")
2048
- },
2049
- async ({ prompt, aspectRatio }) => {
2050
- try {
2051
- const result = await connection.generateTaskIcon(prompt, aspectRatio ?? "square");
2052
- return textResult(`Icon generated and assigned: ${result.iconId}`);
2053
- } catch (error) {
2054
- return textResult(
2055
- `Failed to generate icon: ${error instanceof Error ? error.message : "Unknown error"}`
2056
- );
2057
- }
2058
- }
2059
- )
2004
+ ...buildIconTools(connection)
2060
2005
  ];
2061
2006
  }
2062
2007
 
@@ -2067,37 +2012,28 @@ function textResult(text) {
2067
2012
  function imageBlock(data, mimeType) {
2068
2013
  return { type: "image", data, mimeType };
2069
2014
  }
2070
- function createConveyorMcpServer(connection, config, context) {
2071
- const commonTools = buildCommonTools(connection, config);
2072
- const agentMode = context?.agentMode;
2073
- let modeTools;
2015
+ function getModeTools(agentMode, connection, config, context) {
2074
2016
  switch (agentMode) {
2075
2017
  case "building":
2076
- modeTools = context?.isParentTask ? [
2018
+ return context?.isParentTask ? [
2077
2019
  ...buildTaskTools(connection),
2078
2020
  ...buildPmTools(connection, context?.storyPoints, { includePackTools: true })
2079
2021
  ] : buildTaskTools(connection);
2080
- break;
2081
2022
  case "review":
2082
- modeTools = buildPmTools(connection, context?.storyPoints, {
2083
- includePackTools: !!context?.isParentTask
2084
- });
2085
- break;
2086
2023
  case "auto":
2087
- modeTools = buildPmTools(connection, context?.storyPoints, {
2088
- includePackTools: !!context?.isParentTask
2089
- });
2090
- break;
2091
2024
  case "discovery":
2092
2025
  case "help":
2093
- modeTools = buildPmTools(connection, context?.storyPoints, {
2026
+ return buildPmTools(connection, context?.storyPoints, {
2094
2027
  includePackTools: !!context?.isParentTask
2095
2028
  });
2096
- break;
2097
2029
  default:
2098
- modeTools = config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: false }) : buildTaskTools(connection);
2099
- break;
2030
+ return config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: false }) : buildTaskTools(connection);
2100
2031
  }
2032
+ }
2033
+ function createConveyorMcpServer(connection, config, context) {
2034
+ const commonTools = buildCommonTools(connection, config);
2035
+ const agentMode = context?.agentMode ?? void 0;
2036
+ const modeTools = getModeTools(agentMode, connection, config, context);
2101
2037
  const discoveryTools = agentMode === "discovery" || agentMode === "auto" ? buildDiscoveryTools(connection, context) : [];
2102
2038
  return createSdkMcpServer({
2103
2039
  name: "conveyor",
@@ -2105,16 +2041,17 @@ function createConveyorMcpServer(connection, config, context) {
2105
2041
  });
2106
2042
  }
2107
2043
 
2108
- // src/execution/query-executor.ts
2109
- var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
2110
- var IMAGE_ERROR_PATTERN = /Could not process image/i;
2111
- var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
2044
+ // src/execution/tool-access.ts
2045
+ import { randomUUID } from "crypto";
2112
2046
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
2113
2047
  var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
2048
+ function isPlanFile(input) {
2049
+ const filePath = String(input.file_path ?? input.path ?? "");
2050
+ return filePath.includes(".claude/plans/");
2051
+ }
2114
2052
  function handleDiscoveryToolAccess(toolName, input) {
2115
2053
  if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2116
- const filePath = String(input.file_path ?? input.path ?? "");
2117
- if (filePath.includes(".claude/plans/")) {
2054
+ if (isPlanFile(input)) {
2118
2055
  return { behavior: "allow", updatedInput: input };
2119
2056
  }
2120
2057
  return {
@@ -2141,8 +2078,7 @@ function handleReviewToolAccess(toolName, input, isParentTask) {
2141
2078
  return handleBuildingToolAccess(toolName, input);
2142
2079
  }
2143
2080
  if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2144
- const filePath = String(input.file_path ?? input.path ?? "");
2145
- if (filePath.includes(".claude/plans/")) {
2081
+ if (isPlanFile(input)) {
2146
2082
  return { behavior: "allow", updatedInput: input };
2147
2083
  }
2148
2084
  return {
@@ -2163,8 +2099,7 @@ function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask)
2163
2099
  return isParentTask ? handleReviewToolAccess(toolName, input, true) : handleBuildingToolAccess(toolName, input);
2164
2100
  }
2165
2101
  if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2166
- const filePath = String(input.file_path ?? input.path ?? "");
2167
- if (filePath.includes(".claude/plans/")) {
2102
+ if (isPlanFile(input)) {
2168
2103
  return { behavior: "allow", updatedInput: input };
2169
2104
  }
2170
2105
  return {
@@ -2174,65 +2109,71 @@ function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask)
2174
2109
  }
2175
2110
  return { behavior: "allow", updatedInput: input };
2176
2111
  }
2177
- function buildCanUseTool(host) {
2112
+ async function handleExitPlanMode(host, input) {
2113
+ try {
2114
+ const taskProps = await host.connection.getTaskProperties();
2115
+ const missingProps = [];
2116
+ if (!taskProps.plan?.trim()) missingProps.push("plan (save via update_task)");
2117
+ if (!taskProps.storyPointId) missingProps.push("story points (use set_story_points)");
2118
+ if (!taskProps.title || taskProps.title === "Untitled")
2119
+ missingProps.push("title (use set_task_title)");
2120
+ if (missingProps.length > 0) {
2121
+ return {
2122
+ behavior: "deny",
2123
+ message: [
2124
+ "Cannot exit plan mode yet. Required task properties are missing:",
2125
+ ...missingProps.map((p) => `- ${p}`),
2126
+ "",
2127
+ "Fill these in using MCP tools, then try ExitPlanMode again."
2128
+ ].join("\n")
2129
+ };
2130
+ }
2131
+ await host.connection.triggerIdentification();
2132
+ host.hasExitedPlanMode = true;
2133
+ const newMode = host.isParentTask ? "review" : "building";
2134
+ host.pendingModeRestart = true;
2135
+ if (host.onModeTransition) {
2136
+ host.onModeTransition(newMode);
2137
+ }
2138
+ return { behavior: "allow", updatedInput: input };
2139
+ } catch (err) {
2140
+ return {
2141
+ behavior: "deny",
2142
+ message: `Identification failed: ${err instanceof Error ? err.message : String(err)}. Fix the issue and try again.`
2143
+ };
2144
+ }
2145
+ }
2146
+ async function handleAskUserQuestion(host, input) {
2178
2147
  const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
2148
+ const questions = input.questions;
2149
+ const requestId = randomUUID();
2150
+ host.connection.emitStatus("waiting_for_input");
2151
+ host.connection.sendEvent({
2152
+ type: "tool_use",
2153
+ tool: "AskUserQuestion",
2154
+ input: JSON.stringify(input)
2155
+ });
2156
+ const answerPromise = host.connection.askUserQuestion(requestId, questions);
2157
+ const timeoutPromise = new Promise((resolve2) => {
2158
+ setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
2159
+ });
2160
+ const answers = await Promise.race([answerPromise, timeoutPromise]);
2161
+ host.connection.emitStatus("running");
2162
+ if (!answers) {
2163
+ return {
2164
+ behavior: "deny",
2165
+ message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
2166
+ };
2167
+ }
2168
+ return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
2169
+ }
2170
+ function buildCanUseTool(host) {
2179
2171
  return async (toolName, input) => {
2180
2172
  if (toolName === "ExitPlanMode" && host.agentMode === "auto" && !host.hasExitedPlanMode) {
2181
- try {
2182
- const taskProps = await host.connection.getTaskProperties();
2183
- const missingProps = [];
2184
- if (!taskProps.plan?.trim()) missingProps.push("plan (save via update_task)");
2185
- if (!taskProps.storyPointId) missingProps.push("story points (use set_story_points)");
2186
- if (!taskProps.title || taskProps.title === "Untitled")
2187
- missingProps.push("title (use set_task_title)");
2188
- if (missingProps.length > 0) {
2189
- return {
2190
- behavior: "deny",
2191
- message: [
2192
- "Cannot exit plan mode yet. Required task properties are missing:",
2193
- ...missingProps.map((p) => `- ${p}`),
2194
- "",
2195
- "Fill these in using MCP tools, then try ExitPlanMode again."
2196
- ].join("\n")
2197
- };
2198
- }
2199
- await host.connection.triggerIdentification();
2200
- host.hasExitedPlanMode = true;
2201
- const newMode = host.isParentTask ? "review" : "building";
2202
- host.pendingModeRestart = true;
2203
- if (host.onModeTransition) {
2204
- host.onModeTransition(newMode);
2205
- }
2206
- return { behavior: "allow", updatedInput: input };
2207
- } catch (err) {
2208
- return {
2209
- behavior: "deny",
2210
- message: `Identification failed: ${err instanceof Error ? err.message : String(err)}. Fix the issue and try again.`
2211
- };
2212
- }
2173
+ return await handleExitPlanMode(host, input);
2213
2174
  }
2214
2175
  if (toolName === "AskUserQuestion") {
2215
- const questions = input.questions;
2216
- const requestId = randomUUID();
2217
- host.connection.emitStatus("waiting_for_input");
2218
- host.connection.sendEvent({
2219
- type: "tool_use",
2220
- tool: "AskUserQuestion",
2221
- input: JSON.stringify(input)
2222
- });
2223
- const answerPromise = host.connection.askUserQuestion(requestId, questions);
2224
- const timeoutPromise = new Promise((resolve2) => {
2225
- setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
2226
- });
2227
- const answers = await Promise.race([answerPromise, timeoutPromise]);
2228
- host.connection.emitStatus("running");
2229
- if (!answers) {
2230
- return {
2231
- behavior: "deny",
2232
- message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
2233
- };
2234
- }
2235
- return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
2176
+ return await handleAskUserQuestion(host, input);
2236
2177
  }
2237
2178
  switch (host.agentMode) {
2238
2179
  case "discovery":
@@ -2248,24 +2189,13 @@ function buildCanUseTool(host) {
2248
2189
  }
2249
2190
  };
2250
2191
  }
2251
- function buildQueryOptions(host, context) {
2252
- const settings = context.agentSettings ?? host.config.agentSettings ?? {};
2253
- const mode = host.agentMode;
2254
- const isActiveMode = mode === "building" || mode === "review" || mode === "auto" && host.hasExitedPlanMode;
2255
- const shouldSandbox = host.config.mode === "pm" && isActiveMode && context.useSandbox !== false;
2256
- const systemPromptText = buildSystemPrompt(
2257
- host.config.mode,
2258
- context,
2259
- host.config,
2260
- host.setupLog,
2261
- mode
2262
- );
2263
- const conveyorMcp = createConveyorMcpServer(host.connection, host.config, context);
2264
- const isReadOnlyMode = mode === "discovery" || mode === "help" || mode === "auto" && !host.hasExitedPlanMode;
2265
- const modeDisallowedTools = isReadOnlyMode ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
2266
- const disallowedTools = [...settings.disallowedTools ?? [], ...modeDisallowedTools];
2267
- const settingSources = settings.settingSources ?? ["user", "project"];
2268
- const hooks = {
2192
+
2193
+ // src/execution/query-executor.ts
2194
+ var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
2195
+ var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
2196
+ var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
2197
+ function buildHooks(host) {
2198
+ return {
2269
2199
  PostToolUse: [
2270
2200
  {
2271
2201
  hooks: [
@@ -2279,13 +2209,56 @@ function buildQueryOptions(host, context) {
2279
2209
  isError: false
2280
2210
  });
2281
2211
  }
2282
- return { continue: true };
2212
+ return await Promise.resolve({ continue: true });
2283
2213
  }
2284
2214
  ],
2285
2215
  timeout: 5
2286
2216
  }
2287
2217
  ]
2288
2218
  };
2219
+ }
2220
+ function buildSandboxConfig(host) {
2221
+ const apiHostname = new URL(host.config.conveyorApiUrl).hostname;
2222
+ return {
2223
+ enabled: true,
2224
+ autoAllowBashIfSandboxed: true,
2225
+ allowUnsandboxedCommands: false,
2226
+ filesystem: {
2227
+ allowWrite: [`${host.config.workspaceDir}/**`],
2228
+ denyRead: ["/etc/shadow", "/etc/passwd", "**/.env", "**/.env.*"],
2229
+ denyWrite: ["**/.env", "**/.env.*", "**/node_modules/**"]
2230
+ },
2231
+ network: {
2232
+ allowedDomains: [apiHostname, "api.anthropic.com"],
2233
+ allowManagedDomainsOnly: true,
2234
+ allowLocalBinding: true
2235
+ }
2236
+ };
2237
+ }
2238
+ function isActiveBuildMode(mode, hasExitedPlanMode) {
2239
+ return mode === "building" || mode === "review" || mode === "auto" && hasExitedPlanMode;
2240
+ }
2241
+ function isReadOnlyMode(mode, hasExitedPlanMode) {
2242
+ return mode === "discovery" || mode === "help" || mode === "auto" && !hasExitedPlanMode;
2243
+ }
2244
+ function buildDisallowedTools(settings, mode, hasExitedPlanMode) {
2245
+ const modeDisallowed = isReadOnlyMode(mode, hasExitedPlanMode) ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
2246
+ const configured = settings.disallowedTools ?? [];
2247
+ const combined = [...configured, ...modeDisallowed];
2248
+ return combined.length > 0 ? combined : void 0;
2249
+ }
2250
+ function buildQueryOptions(host, context) {
2251
+ const settings = context.agentSettings ?? host.config.agentSettings ?? {};
2252
+ const mode = host.agentMode;
2253
+ const shouldSandbox = host.config.mode === "pm" && isActiveBuildMode(mode, host.hasExitedPlanMode) && context.useSandbox !== false;
2254
+ const systemPromptText = buildSystemPrompt(
2255
+ host.config.mode,
2256
+ context,
2257
+ host.config,
2258
+ host.setupLog,
2259
+ mode
2260
+ );
2261
+ const settingSources = settings.settingSources ?? ["user", "project"];
2289
2262
  const baseOptions = {
2290
2263
  model: context.model || host.config.model,
2291
2264
  systemPrompt: {
@@ -2299,33 +2272,18 @@ function buildQueryOptions(host, context) {
2299
2272
  allowDangerouslySkipPermissions: !shouldSandbox,
2300
2273
  canUseTool: buildCanUseTool(host),
2301
2274
  tools: { type: "preset", preset: "claude_code" },
2302
- mcpServers: { conveyor: conveyorMcp },
2303
- hooks,
2275
+ mcpServers: { conveyor: createConveyorMcpServer(host.connection, host.config, context) },
2276
+ hooks: buildHooks(host),
2304
2277
  maxTurns: settings.maxTurns,
2305
2278
  effort: settings.effort,
2306
2279
  thinking: settings.thinking,
2307
2280
  betas: settings.betas,
2308
2281
  maxBudgetUsd: settings.maxBudgetUsd ?? 50,
2309
- disallowedTools: disallowedTools.length > 0 ? disallowedTools : void 0,
2282
+ disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
2310
2283
  enableFileCheckpointing: settings.enableFileCheckpointing
2311
2284
  };
2312
2285
  if (shouldSandbox) {
2313
- const apiHostname = new URL(host.config.conveyorApiUrl).hostname;
2314
- baseOptions.sandbox = {
2315
- enabled: true,
2316
- autoAllowBashIfSandboxed: true,
2317
- allowUnsandboxedCommands: false,
2318
- filesystem: {
2319
- allowWrite: [`${host.config.workspaceDir}/**`],
2320
- denyRead: ["/etc/shadow", "/etc/passwd", "**/.env", "**/.env.*"],
2321
- denyWrite: ["**/.env", "**/.env.*", "**/node_modules/**"]
2322
- },
2323
- network: {
2324
- allowedDomains: [apiHostname, "api.anthropic.com"],
2325
- allowManagedDomainsOnly: true,
2326
- allowLocalBinding: true
2327
- }
2328
- };
2286
+ baseOptions.sandbox = buildSandboxConfig(host);
2329
2287
  }
2330
2288
  return baseOptions;
2331
2289
  }
@@ -2350,34 +2308,47 @@ function buildMultimodalPrompt(textPrompt, context, skipImages = false) {
2350
2308
  source: {
2351
2309
  type: "base64",
2352
2310
  media_type: file.mimeType,
2353
- data: file.content
2311
+ data: file.content ?? ""
2354
2312
  }
2355
2313
  });
2356
- blocks.push({
2357
- type: "text",
2358
- text: `[Attached image: ${file.fileName} (${file.mimeType})]`
2359
- });
2314
+ blocks.push({ type: "text", text: `[Attached image: ${file.fileName} (${file.mimeType})]` });
2360
2315
  }
2361
2316
  for (const file of chatImages) {
2362
2317
  blocks.push({
2363
2318
  type: "image",
2364
- source: {
2365
- type: "base64",
2366
- media_type: file.mimeType,
2367
- data: file.content
2368
- }
2369
- });
2370
- blocks.push({
2371
- type: "text",
2372
- text: `[Chat image: ${file.fileName} (${file.mimeType})]`
2319
+ source: { type: "base64", media_type: file.mimeType, data: file.content }
2373
2320
  });
2321
+ blocks.push({ type: "text", text: `[Chat image: ${file.fileName} (${file.mimeType})]` });
2374
2322
  }
2375
2323
  return blocks;
2376
2324
  }
2325
+ function buildFollowUpPrompt(host, context, followUpContent) {
2326
+ const isPmMode = host.config.mode === "pm";
2327
+ const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
2328
+ const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
2329
+ (b) => b.type === "image"
2330
+ );
2331
+ const textPrompt = isPmMode ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode)}
2332
+
2333
+ ---
2334
+
2335
+ The team says:
2336
+ ${followUpText}` : followUpText;
2337
+ if (isPmMode) {
2338
+ const prompt = buildMultimodalPrompt(textPrompt, context);
2339
+ if (followUpImages.length > 0 && Array.isArray(prompt)) {
2340
+ prompt.push(...followUpImages);
2341
+ }
2342
+ return prompt;
2343
+ }
2344
+ if (followUpImages.length > 0) {
2345
+ return [{ type: "text", text: textPrompt }, ...followUpImages];
2346
+ }
2347
+ return textPrompt;
2348
+ }
2377
2349
  async function runSdkQuery(host, context, followUpContent) {
2378
2350
  if (host.isStopped()) return;
2379
2351
  const mode = host.agentMode;
2380
- const isPmMode = host.config.mode === "pm";
2381
2352
  const isDiscoveryLike = mode === "discovery" || mode === "help";
2382
2353
  if (isDiscoveryLike) {
2383
2354
  host.snapshotPlanFiles();
@@ -2385,27 +2356,7 @@ async function runSdkQuery(host, context, followUpContent) {
2385
2356
  const options = buildQueryOptions(host, context);
2386
2357
  const resume = context.claudeSessionId ?? void 0;
2387
2358
  if (followUpContent) {
2388
- const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
2389
- const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
2390
- (b) => b.type === "image"
2391
- );
2392
- const textPrompt = isPmMode ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto, mode)}
2393
-
2394
- ---
2395
-
2396
- The team says:
2397
- ${followUpText}` : followUpText;
2398
- let prompt;
2399
- if (isPmMode) {
2400
- prompt = buildMultimodalPrompt(textPrompt, context);
2401
- if (followUpImages.length > 0 && Array.isArray(prompt)) {
2402
- prompt.push(...followUpImages);
2403
- }
2404
- } else if (followUpImages.length > 0) {
2405
- prompt = [{ type: "text", text: textPrompt }, ...followUpImages];
2406
- } else {
2407
- prompt = textPrompt;
2408
- }
2359
+ const prompt = buildFollowUpPrompt(host, context, followUpContent);
2409
2360
  const agentQuery = query({
2410
2361
  prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
2411
2362
  options: { ...options, resume }
@@ -2426,55 +2377,94 @@ ${followUpText}` : followUpText;
2426
2377
  host.syncPlanFile();
2427
2378
  }
2428
2379
  }
2380
+ function buildRetryQuery(host, context, options, lastErrorWasImage) {
2381
+ if (lastErrorWasImage) {
2382
+ host.connection.postChatMessage(
2383
+ "An attached image could not be processed. Retrying without images..."
2384
+ );
2385
+ }
2386
+ const retryPrompt = buildMultimodalPrompt(
2387
+ buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2388
+ context,
2389
+ lastErrorWasImage
2390
+ );
2391
+ return query({
2392
+ prompt: host.createInputStream(retryPrompt),
2393
+ options: { ...options, resume: void 0 }
2394
+ });
2395
+ }
2396
+ function handleStaleSession(context, host, options) {
2397
+ context.claudeSessionId = null;
2398
+ host.connection.storeSessionId("");
2399
+ const freshPrompt = buildMultimodalPrompt(
2400
+ buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2401
+ context
2402
+ );
2403
+ const freshQuery = query({
2404
+ prompt: host.createInputStream(freshPrompt),
2405
+ options: { ...options, resume: void 0 }
2406
+ });
2407
+ return runWithRetry(freshQuery, context, host, options);
2408
+ }
2409
+ async function waitForRetryDelay(host, delayMs) {
2410
+ await new Promise((resolve2) => {
2411
+ const timer = setTimeout(resolve2, delayMs);
2412
+ const checkStopped = setInterval(() => {
2413
+ if (host.isStopped()) {
2414
+ clearTimeout(timer);
2415
+ clearInterval(checkStopped);
2416
+ resolve2();
2417
+ }
2418
+ }, 1e3);
2419
+ setTimeout(() => clearInterval(checkStopped), delayMs + 100);
2420
+ });
2421
+ }
2422
+ function isStaleOrExitedSession(error, context) {
2423
+ if (!(error instanceof Error)) return false;
2424
+ if (error.message.includes("No conversation found with session ID")) return true;
2425
+ return !!context.claudeSessionId && error.message.includes("process exited");
2426
+ }
2427
+ function isRetriableError(error) {
2428
+ if (!(error instanceof Error)) return false;
2429
+ return API_ERROR_PATTERN2.test(error.message) || IMAGE_ERROR_PATTERN2.test(error.message);
2430
+ }
2431
+ function classifyImageError(error) {
2432
+ return error instanceof Error && IMAGE_ERROR_PATTERN2.test(error.message);
2433
+ }
2434
+ async function emitRetryStatus(host, attempt, delayMs) {
2435
+ const delayMin = Math.round(delayMs / 6e4);
2436
+ host.connection.postChatMessage(
2437
+ `API error encountered. Retrying in ${delayMin} minute${delayMin > 1 ? "s" : ""}... (attempt ${attempt + 1}/${RETRY_DELAYS_MS.length})`
2438
+ );
2439
+ host.connection.sendEvent({
2440
+ type: "error",
2441
+ message: `API error, retrying in ${delayMin}m (${attempt + 1}/${RETRY_DELAYS_MS.length})`
2442
+ });
2443
+ host.connection.emitStatus("waiting_for_input");
2444
+ await host.callbacks.onStatusChange("waiting_for_input");
2445
+ await waitForRetryDelay(host, delayMs);
2446
+ host.connection.emitStatus("running");
2447
+ await host.callbacks.onStatusChange("running");
2448
+ }
2429
2449
  async function runWithRetry(initialQuery, context, host, options) {
2430
2450
  let lastErrorWasImage = false;
2431
2451
  for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
2432
2452
  if (host.isStopped()) return;
2433
- const agentQuery = attempt === 0 ? initialQuery : (() => {
2434
- if (lastErrorWasImage) {
2435
- host.connection.postChatMessage(
2436
- "An attached image could not be processed. Retrying without images..."
2437
- );
2438
- }
2439
- const retryPrompt = buildMultimodalPrompt(
2440
- buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2441
- context,
2442
- lastErrorWasImage
2443
- );
2444
- return query({
2445
- prompt: host.createInputStream(retryPrompt),
2446
- options: { ...options, resume: void 0 }
2447
- });
2448
- })();
2453
+ const agentQuery = attempt === 0 ? initialQuery : buildRetryQuery(host, context, options, lastErrorWasImage);
2449
2454
  try {
2450
2455
  const { retriable, resultSummary, modeRestart } = await processEvents(
2451
2456
  agentQuery,
2452
2457
  context,
2453
2458
  host
2454
2459
  );
2455
- if (modeRestart) return;
2456
- if (!retriable || host.isStopped()) return;
2457
- lastErrorWasImage = IMAGE_ERROR_PATTERN.test(resultSummary ?? "");
2460
+ if (modeRestart || !retriable || host.isStopped()) return;
2461
+ lastErrorWasImage = IMAGE_ERROR_PATTERN2.test(resultSummary ?? "");
2458
2462
  } catch (error) {
2459
- const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
2460
- const isResumeProcessExit = !isStaleSession && !!context.claudeSessionId && error instanceof Error && error.message.includes("process exited");
2461
- if ((isStaleSession || isResumeProcessExit) && context.claudeSessionId) {
2462
- context.claudeSessionId = null;
2463
- host.connection.storeSessionId("");
2464
- const freshPrompt = buildMultimodalPrompt(
2465
- buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2466
- context
2467
- );
2468
- const freshQuery = query({
2469
- prompt: host.createInputStream(freshPrompt),
2470
- options: { ...options, resume: void 0 }
2471
- });
2472
- return runWithRetry(freshQuery, context, host, options);
2463
+ if (isStaleOrExitedSession(error, context) && context.claudeSessionId) {
2464
+ return handleStaleSession(context, host, options);
2473
2465
  }
2474
- const isApiError = error instanceof Error && API_ERROR_PATTERN2.test(error.message);
2475
- const isImageError = error instanceof Error && IMAGE_ERROR_PATTERN.test(error.message);
2476
- if (isImageError) lastErrorWasImage = true;
2477
- if (!isApiError && !isImageError) throw error;
2466
+ if (classifyImageError(error)) lastErrorWasImage = true;
2467
+ if (!isRetriableError(error)) throw error;
2478
2468
  }
2479
2469
  if (attempt >= RETRY_DELAYS_MS.length) {
2480
2470
  host.connection.postChatMessage(
@@ -2482,30 +2472,7 @@ async function runWithRetry(initialQuery, context, host, options) {
2482
2472
  );
2483
2473
  return;
2484
2474
  }
2485
- const delayMs = RETRY_DELAYS_MS[attempt];
2486
- const delayMin = Math.round(delayMs / 6e4);
2487
- host.connection.postChatMessage(
2488
- `API error encountered. Retrying in ${delayMin} minute${delayMin > 1 ? "s" : ""}... (attempt ${attempt + 1}/${RETRY_DELAYS_MS.length})`
2489
- );
2490
- host.connection.sendEvent({
2491
- type: "error",
2492
- message: `API error, retrying in ${delayMin}m (${attempt + 1}/${RETRY_DELAYS_MS.length})`
2493
- });
2494
- host.connection.emitStatus("waiting_for_input");
2495
- await host.callbacks.onStatusChange("waiting_for_input");
2496
- await new Promise((resolve2) => {
2497
- const timer = setTimeout(resolve2, delayMs);
2498
- const checkStopped = setInterval(() => {
2499
- if (host.isStopped()) {
2500
- clearTimeout(timer);
2501
- clearInterval(checkStopped);
2502
- resolve2();
2503
- }
2504
- }, 1e3);
2505
- setTimeout(() => clearInterval(checkStopped), delayMs + 100);
2506
- });
2507
- host.connection.emitStatus("running");
2508
- await host.callbacks.onStatusChange("running");
2475
+ await emitRetryStatus(host, attempt, RETRY_DELAYS_MS[attempt]);
2509
2476
  }
2510
2477
  }
2511
2478
 
@@ -2582,19 +2549,7 @@ var PlanSync = class {
2582
2549
  }
2583
2550
  }
2584
2551
  }
2585
- syncPlanFile() {
2586
- if (this.lockedPlanFile) {
2587
- try {
2588
- const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
2589
- if (content) {
2590
- this.connection.updateTaskFields({ plan: content });
2591
- const fileName = this.lockedPlanFile.split("/").pop();
2592
- this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
2593
- }
2594
- } catch {
2595
- }
2596
- return;
2597
- }
2552
+ findNewestPlanFile() {
2598
2553
  let newest = null;
2599
2554
  for (const plansDir of this.getPlanDirs()) {
2600
2555
  let files;
@@ -2617,24 +2572,245 @@ var PlanSync = class {
2617
2572
  }
2618
2573
  }
2619
2574
  }
2575
+ return newest;
2576
+ }
2577
+ syncPlanFile() {
2578
+ if (this.lockedPlanFile) {
2579
+ try {
2580
+ const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
2581
+ if (content) {
2582
+ this.connection.updateTaskFields({ plan: content });
2583
+ const fileName = this.lockedPlanFile.split("/").pop() ?? "plan";
2584
+ this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
2585
+ }
2586
+ } catch {
2587
+ }
2588
+ return;
2589
+ }
2590
+ const newest = this.findNewestPlanFile();
2620
2591
  if (newest) {
2621
2592
  this.lockedPlanFile = newest.path;
2622
2593
  const content = readFileSync(newest.path, "utf-8").trim();
2623
2594
  if (content) {
2624
2595
  this.connection.updateTaskFields({ plan: content });
2625
- const fileName = newest.path.split("/").pop();
2596
+ const fileName = newest.path.split("/").pop() ?? "plan";
2626
2597
  this.connection.postChatMessage(
2627
2598
  `Detected local plan file (${fileName}) and synced it to the task plan.`
2628
2599
  );
2629
2600
  }
2630
2601
  }
2631
2602
  }
2632
- };
2603
+ };
2604
+
2605
+ // src/runner/runner-setup.ts
2606
+ var MAX_SETUP_LOG_LINES = 50;
2607
+ function pushSetupLog(setupLog, line) {
2608
+ setupLog.push(line);
2609
+ if (setupLog.length > MAX_SETUP_LOG_LINES) {
2610
+ setupLog.splice(0, setupLog.length - MAX_SETUP_LOG_LINES);
2611
+ }
2612
+ }
2613
+ async function executeSetupConfig(config, runnerConfig, connection, setupLog, effectiveAgentMode) {
2614
+ let deferredStartConfig = null;
2615
+ if (config.setupCommand) {
2616
+ pushSetupLog(setupLog, `$ ${config.setupCommand}`);
2617
+ await runSetupCommand(config.setupCommand, runnerConfig.workspaceDir, (stream, data) => {
2618
+ connection.sendEvent({ type: "setup_output", stream, data });
2619
+ for (const line of data.split("\n").filter(Boolean)) {
2620
+ pushSetupLog(setupLog, `[${stream}] ${line}`);
2621
+ }
2622
+ });
2623
+ pushSetupLog(setupLog, "(exit 0)");
2624
+ }
2625
+ if (config.startCommand) {
2626
+ if (effectiveAgentMode === "auto") {
2627
+ deferredStartConfig = config;
2628
+ pushSetupLog(setupLog, `[conveyor] startCommand deferred (auto mode)`);
2629
+ } else {
2630
+ pushSetupLog(setupLog, `$ ${config.startCommand} & (background)`);
2631
+ runStartCommand(config.startCommand, runnerConfig.workspaceDir, (stream, data) => {
2632
+ connection.sendEvent({ type: "start_command_output", stream, data });
2633
+ });
2634
+ }
2635
+ }
2636
+ return deferredStartConfig;
2637
+ }
2638
+ async function runSetupSafe(runnerConfig, connection, callbacks, setupLog, effectiveAgentMode, setState) {
2639
+ await setState("setup");
2640
+ const ports = await loadForwardPorts(runnerConfig.workspaceDir);
2641
+ if (ports.length > 0 && process.env.CODESPACE_NAME) {
2642
+ const visibility = ports.map((p) => `${p}:public`).join(" ");
2643
+ runStartCommand(
2644
+ `gh codespace ports visibility ${visibility} -c "${process.env.CODESPACE_NAME}" 2>/dev/null`,
2645
+ runnerConfig.workspaceDir,
2646
+ () => void 0
2647
+ );
2648
+ }
2649
+ const config = await loadConveyorConfig(runnerConfig.workspaceDir);
2650
+ if (!config) {
2651
+ connection.sendEvent({ type: "setup_complete" });
2652
+ await callbacks.onEvent({ type: "setup_complete" });
2653
+ return { ok: true, deferredStartConfig: null };
2654
+ }
2655
+ try {
2656
+ const deferredStartConfig = await executeSetupConfig(
2657
+ config,
2658
+ runnerConfig,
2659
+ connection,
2660
+ setupLog,
2661
+ effectiveAgentMode
2662
+ );
2663
+ const setupEvent = {
2664
+ type: "setup_complete",
2665
+ previewPort: config.previewPort ?? void 0,
2666
+ startCommandDeferred: deferredStartConfig === null ? void 0 : true
2667
+ };
2668
+ connection.sendEvent(setupEvent);
2669
+ await callbacks.onEvent(setupEvent);
2670
+ return { ok: true, deferredStartConfig };
2671
+ } catch (error) {
2672
+ const message = error instanceof Error ? error.message : "Setup failed";
2673
+ connection.sendEvent({ type: "setup_error", message });
2674
+ await callbacks.onEvent({ type: "setup_error", message });
2675
+ connection.postChatMessage(
2676
+ `Environment setup failed: ${message}
2677
+ The agent cannot start until this is resolved.`
2678
+ );
2679
+ return { ok: false, deferredStartConfig: null };
2680
+ }
2681
+ }
2682
+ function runDeferredStartCommand(deferredStartConfig, runnerConfig, connection, setupLog) {
2683
+ if (!deferredStartConfig?.startCommand) return;
2684
+ pushSetupLog(setupLog, `$ ${deferredStartConfig.startCommand} & (background, deferred)`);
2685
+ connection.sendEvent({ type: "start_command_started" });
2686
+ runStartCommand(deferredStartConfig.startCommand, runnerConfig.workspaceDir, (stream, data) => {
2687
+ connection.sendEvent({ type: "start_command_output", stream, data });
2688
+ });
2689
+ const setupEvent = {
2690
+ type: "setup_complete",
2691
+ previewPort: deferredStartConfig.previewPort ?? void 0
2692
+ };
2693
+ connection.sendEvent(setupEvent);
2694
+ }
2695
+
2696
+ // src/runner/runner-helpers.ts
2697
+ function buildFileBlock(f) {
2698
+ if (f.content && f.contentEncoding === "base64") {
2699
+ return [
2700
+ {
2701
+ type: "image",
2702
+ source: { type: "base64", media_type: f.mimeType, data: f.content }
2703
+ },
2704
+ { type: "text", text: `[Attached image: ${f.fileName} (${f.mimeType})]` }
2705
+ ];
2706
+ }
2707
+ if (f.content && f.contentEncoding === "utf-8") {
2708
+ return [
2709
+ {
2710
+ type: "text",
2711
+ text: `[Attached file: ${f.fileName} (${f.mimeType})]
2712
+ \`\`\`
2713
+ ${f.content}
2714
+ \`\`\``
2715
+ }
2716
+ ];
2717
+ }
2718
+ return [
2719
+ {
2720
+ type: "text",
2721
+ text: `[Attached file: ${f.fileName} (${f.mimeType})] Download: ${f.downloadUrl}`
2722
+ }
2723
+ ];
2724
+ }
2725
+ function buildMessageContent(content, files) {
2726
+ if (!files?.length) return content;
2727
+ const blocks = [{ type: "text", text: content }];
2728
+ for (const f of files) blocks.push(...buildFileBlock(f));
2729
+ return blocks;
2730
+ }
2731
+ function formatThinkingSetting(thinking) {
2732
+ if (thinking?.type === "enabled") return `enabled(${thinking.budgetTokens ?? "?"})`;
2733
+ return thinking?.type ?? "default";
2734
+ }
2735
+ function getModelForMode(context, mode, isParentTask) {
2736
+ if (mode === "building" && !isParentTask) {
2737
+ return context.builderModel ?? context.model;
2738
+ }
2739
+ return context.pmModel ?? context.model;
2740
+ }
2741
+ async function handleAutoModeRestart(state, connection, setState, runQuerySafe, fetchFreshContext) {
2742
+ const { taskContext } = state;
2743
+ const currentModel = taskContext.model;
2744
+ const currentSessionId = taskContext.claudeSessionId;
2745
+ if (currentSessionId && currentModel) {
2746
+ state.sessionIds.set(currentModel, currentSessionId);
2747
+ }
2748
+ const newMode = state.agentMode;
2749
+ const isParentTask = !!taskContext.isParentTask;
2750
+ const newModel = getModelForMode(taskContext, newMode, isParentTask);
2751
+ const resumeSessionId = newModel ? state.sessionIds.get(newModel) : null;
2752
+ if (resumeSessionId) {
2753
+ taskContext.claudeSessionId = resumeSessionId;
2754
+ } else {
2755
+ taskContext.claudeSessionId = null;
2756
+ connection.storeSessionId("");
2757
+ }
2758
+ if (newModel) taskContext.model = newModel;
2759
+ taskContext.agentMode = newMode;
2760
+ connection.emitModeTransition({ fromMode: "auto", toMode: newMode ?? "building" });
2761
+ connection.emitModeChanged(newMode);
2762
+ connection.postChatMessage(
2763
+ `Transitioning to **${newMode}** mode${newModel ? ` with ${newModel}` : ""}. Restarting session...`
2764
+ );
2765
+ const freshContext = await fetchFreshContext();
2766
+ if (freshContext) {
2767
+ freshContext._runnerSessionId = taskContext._runnerSessionId;
2768
+ freshContext.claudeSessionId = taskContext.claudeSessionId;
2769
+ freshContext.agentMode = newMode;
2770
+ if (newModel) freshContext.model = newModel;
2771
+ state.taskContext = freshContext;
2772
+ }
2773
+ await setState("running");
2774
+ await runQuerySafe(state.taskContext);
2775
+ }
2776
+ function buildQueryHost(deps) {
2777
+ return {
2778
+ config: deps.config,
2779
+ connection: deps.connection,
2780
+ callbacks: deps.callbacks,
2781
+ setupLog: deps.setupLog,
2782
+ costTracker: deps.costTracker,
2783
+ get agentMode() {
2784
+ return deps.getEffectiveAgentMode();
2785
+ },
2786
+ get isParentTask() {
2787
+ return deps.getIsParentTask();
2788
+ },
2789
+ get hasExitedPlanMode() {
2790
+ return deps.getHasExitedPlanMode();
2791
+ },
2792
+ set hasExitedPlanMode(val) {
2793
+ deps.setHasExitedPlanMode(val);
2794
+ },
2795
+ get pendingModeRestart() {
2796
+ return deps.getPendingModeRestart();
2797
+ },
2798
+ set pendingModeRestart(val) {
2799
+ deps.setPendingModeRestart(val);
2800
+ },
2801
+ sessionIds: deps.sessionIds,
2802
+ isStopped: deps.isStopped,
2803
+ createInputStream: deps.createInputStream,
2804
+ snapshotPlanFiles: () => deps.planSync.snapshotPlanFiles(),
2805
+ syncPlanFile: () => deps.planSync.syncPlanFile(),
2806
+ onModeTransition: deps.onModeTransition
2807
+ };
2808
+ }
2633
2809
 
2634
2810
  // src/runner/agent-runner.ts
2635
2811
  var HEARTBEAT_INTERVAL_MS = 3e4;
2636
2812
  var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
2637
- var AgentRunner = class _AgentRunner {
2813
+ var AgentRunner = class {
2638
2814
  config;
2639
2815
  connection;
2640
2816
  callbacks;
@@ -2653,11 +2829,10 @@ var AgentRunner = class _AgentRunner {
2653
2829
  pendingModeRestart = false;
2654
2830
  sessionIds = /* @__PURE__ */ new Map();
2655
2831
  lastQueryModeRestart = false;
2656
- deferredStartConfig = null;
2657
2832
  startCommandStarted = false;
2658
2833
  idleTimer = null;
2659
2834
  idleCheckInterval = null;
2660
- static MAX_SETUP_LOG_LINES = 50;
2835
+ deferredStartConfig = null;
2661
2836
  constructor(config, callbacks) {
2662
2837
  this.config = config;
2663
2838
  this.connection = new ConveyorConnection(config);
@@ -2667,15 +2842,10 @@ var AgentRunner = class _AgentRunner {
2667
2842
  get state() {
2668
2843
  return this._state;
2669
2844
  }
2670
- /**
2671
- * Resolve the effective AgentMode from explicit agentMode or legacy config flags.
2672
- * This is the single axis of behavior for all execution path decisions.
2673
- */
2674
2845
  get effectiveAgentMode() {
2675
2846
  if (this.agentMode) return this.agentMode;
2676
2847
  if (this.config.mode === "pm") {
2677
- if (this.config.isAuto) return "auto";
2678
- return "discovery";
2848
+ return this.config.isAuto ? "auto" : "discovery";
2679
2849
  }
2680
2850
  return "building";
2681
2851
  }
@@ -2686,9 +2856,7 @@ var AgentRunner = class _AgentRunner {
2686
2856
  }
2687
2857
  startHeartbeat() {
2688
2858
  this.heartbeatTimer = setInterval(() => {
2689
- if (!this.stopped) {
2690
- this.connection.sendHeartbeat();
2691
- }
2859
+ if (!this.stopped) this.connection.sendHeartbeat();
2692
2860
  }, HEARTBEAT_INTERVAL_MS);
2693
2861
  }
2694
2862
  stopHeartbeat() {
@@ -2715,32 +2883,44 @@ var AgentRunner = class _AgentRunner {
2715
2883
  (message) => this.injectHumanMessage(message.content, message.files)
2716
2884
  );
2717
2885
  this.connection.onModeChange((data) => this.handleModeChange(data.agentMode));
2718
- this.connection.onRunStartCommand(() => this.runDeferredStartCommand());
2886
+ this.connection.onRunStartCommand(() => this.handleRunStartCommand());
2719
2887
  await this.setState("connected");
2720
2888
  this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
2721
2889
  this.startHeartbeat();
2722
2890
  if (this.config.mode !== "pm" && process.env.CODESPACES === "true") {
2723
- const setupOk = await this.runSetupSafe();
2724
- if (!setupOk) {
2891
+ const result = await runSetupSafe(
2892
+ this.config,
2893
+ this.connection,
2894
+ this.callbacks,
2895
+ this.setupLog,
2896
+ this.effectiveAgentMode,
2897
+ (s) => this.setState(s)
2898
+ );
2899
+ if (!result.ok) {
2725
2900
  this.stopHeartbeat();
2726
2901
  await this.setState("error");
2727
2902
  this.connection.disconnect();
2728
2903
  return;
2729
2904
  }
2905
+ this.deferredStartConfig = result.deferredStartConfig;
2730
2906
  }
2731
2907
  initRtk();
2908
+ this.tryInitWorktree();
2909
+ if (!await this.fetchAndInitContext()) return;
2910
+ this.tryPostContextWorktree();
2911
+ this.checkoutWorktreeBranch();
2912
+ await this.executeInitialMode();
2913
+ await this.runCoreLoop();
2914
+ this.stopHeartbeat();
2915
+ await this.setState("finished");
2916
+ this.connection.disconnect();
2917
+ }
2918
+ tryInitWorktree() {
2732
2919
  if (process.env.CODESPACES !== "true" && process.env.CONVEYOR_USE_WORKTREE !== "false" && (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true")) {
2733
- try {
2734
- const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
2735
- this.config = { ...this.config, workspaceDir: worktreePath };
2736
- this.planSync.updateWorkspaceDir(worktreePath);
2737
- this.worktreeActive = true;
2738
- this.setupLog.push(`[conveyor] Using worktree: ${worktreePath}`);
2739
- } catch (error) {
2740
- const msg = error instanceof Error ? error.message : "Unknown error";
2741
- this.setupLog.push(`[conveyor] Worktree creation failed, using shared workspace: ${msg}`);
2742
- }
2920
+ this.activateWorktree("[conveyor] Using worktree:");
2743
2921
  }
2922
+ }
2923
+ async fetchAndInitContext() {
2744
2924
  await this.setState("fetching_context");
2745
2925
  try {
2746
2926
  this.taskContext = await this.connection.fetchTaskContext();
@@ -2751,67 +2931,56 @@ var AgentRunner = class _AgentRunner {
2751
2931
  this.stopHeartbeat();
2752
2932
  await this.setState("error");
2753
2933
  this.connection.disconnect();
2754
- return;
2934
+ return false;
2755
2935
  }
2756
2936
  this.taskContext._runnerSessionId = randomUUID2();
2757
- if (this.taskContext.agentMode) {
2758
- this.agentMode = this.taskContext.agentMode;
2759
- }
2937
+ if (this.taskContext.agentMode) this.agentMode = this.taskContext.agentMode;
2760
2938
  this.logEffectiveSettings();
2761
- if (process.env.CODESPACES === "true") {
2762
- unshallowRepo(this.config.workspaceDir);
2939
+ if (process.env.CODESPACES === "true") unshallowRepo(this.config.workspaceDir);
2940
+ return true;
2941
+ }
2942
+ tryPostContextWorktree() {
2943
+ if (process.env.CODESPACES !== "true" && !this.worktreeActive && this.taskContext?.useWorktree) {
2944
+ this.activateWorktree("[conveyor] Using worktree (from task config):");
2763
2945
  }
2764
- if (process.env.CODESPACES !== "true" && !this.worktreeActive && this.taskContext.useWorktree) {
2765
- try {
2766
- const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
2767
- this.config = { ...this.config, workspaceDir: worktreePath };
2768
- this.planSync.updateWorkspaceDir(worktreePath);
2769
- this.worktreeActive = true;
2770
- this.setupLog.push(`[conveyor] Using worktree (from task config): ${worktreePath}`);
2771
- } catch (error) {
2772
- const msg = error instanceof Error ? error.message : "Unknown error";
2773
- this.setupLog.push(`[conveyor] Worktree creation failed, using shared workspace: ${msg}`);
2774
- }
2946
+ }
2947
+ activateWorktree(logPrefix) {
2948
+ try {
2949
+ const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
2950
+ this.config = { ...this.config, workspaceDir: worktreePath };
2951
+ this.planSync.updateWorkspaceDir(worktreePath);
2952
+ this.worktreeActive = true;
2953
+ pushSetupLog(this.setupLog, `${logPrefix} ${worktreePath}`);
2954
+ } catch (error) {
2955
+ const msg = error instanceof Error ? error.message : "Unknown error";
2956
+ pushSetupLog(
2957
+ this.setupLog,
2958
+ `[conveyor] Worktree creation failed, using shared workspace: ${msg}`
2959
+ );
2775
2960
  }
2776
- if (this.worktreeActive && this.taskContext.githubBranch) {
2777
- try {
2778
- const branch = this.taskContext.githubBranch;
2779
- execSync3(`git fetch origin ${branch} && git checkout ${branch}`, {
2780
- cwd: this.config.workspaceDir,
2781
- stdio: "ignore"
2782
- });
2783
- } catch {
2784
- }
2961
+ }
2962
+ checkoutWorktreeBranch() {
2963
+ if (!this.worktreeActive || !this.taskContext?.githubBranch) return;
2964
+ try {
2965
+ const branch = this.taskContext.githubBranch;
2966
+ execSync3(`git fetch origin ${branch} && git checkout ${branch}`, {
2967
+ cwd: this.config.workspaceDir,
2968
+ stdio: "ignore"
2969
+ });
2970
+ } catch {
2785
2971
  }
2786
- switch (this.effectiveAgentMode) {
2787
- case "discovery":
2788
- case "help":
2789
- await this.setState("idle");
2790
- break;
2791
- case "building":
2792
- await this.setState("running");
2793
- await this.runQuerySafe(this.taskContext);
2794
- if (!this.stopped) await this.setState("idle");
2795
- break;
2796
- case "review":
2797
- if (this.taskContext.isParentTask) {
2798
- await this.setState("running");
2799
- await this.runQuerySafe(this.taskContext);
2800
- if (!this.stopped) await this.setState("idle");
2801
- } else {
2802
- await this.setState("idle");
2803
- }
2804
- break;
2805
- case "auto":
2806
- await this.setState("running");
2807
- await this.runQuerySafe(this.taskContext);
2808
- if (!this.stopped) await this.setState("idle");
2809
- break;
2972
+ }
2973
+ async executeInitialMode() {
2974
+ if (!this.taskContext) return;
2975
+ const mode = this.effectiveAgentMode;
2976
+ const shouldRun = mode === "building" || mode === "auto" || mode === "review" && !!this.taskContext.isParentTask;
2977
+ if (shouldRun) {
2978
+ await this.setState("running");
2979
+ await this.runQuerySafe(this.taskContext);
2980
+ if (!this.stopped) await this.setState("idle");
2981
+ } else {
2982
+ await this.setState("idle");
2810
2983
  }
2811
- await this.runCoreLoop();
2812
- this.stopHeartbeat();
2813
- await this.setState("finished");
2814
- this.connection.disconnect();
2815
2984
  }
2816
2985
  async runQuerySafe(context, followUp) {
2817
2986
  this.lastQueryModeRestart = false;
@@ -2833,7 +3002,21 @@ var AgentRunner = class _AgentRunner {
2833
3002
  while (!this.stopped) {
2834
3003
  if (this.lastQueryModeRestart) {
2835
3004
  this.lastQueryModeRestart = false;
2836
- await this.handleAutoModeRestart();
3005
+ await handleAutoModeRestart(
3006
+ { agentMode: this.agentMode, sessionIds: this.sessionIds, taskContext: this.taskContext },
3007
+ this.connection,
3008
+ (s) => this.setState(s),
3009
+ (ctx) => this.runQuerySafe(ctx),
3010
+ async () => {
3011
+ try {
3012
+ return await this.connection.fetchTaskContext();
3013
+ } catch {
3014
+ return null;
3015
+ }
3016
+ }
3017
+ );
3018
+ this.taskContext = await this.connection.fetchTaskContext().catch(() => null) ?? this.taskContext;
3019
+ if (!this.stopped && this._state !== "error") await this.setState("idle");
2837
3020
  continue;
2838
3021
  }
2839
3022
  if (this._state === "idle") {
@@ -2849,108 +3032,17 @@ var AgentRunner = class _AgentRunner {
2849
3032
  }
2850
3033
  }
2851
3034
  }
2852
- /**
2853
- * Handle auto mode transition after ExitPlanMode.
2854
- * Saves the current session, switches model/mode, and restarts with a fresh query.
2855
- */
2856
- async handleAutoModeRestart() {
2857
- if (!this.taskContext) return;
2858
- const currentModel = this.taskContext.model;
2859
- const currentSessionId = this.taskContext.claudeSessionId;
2860
- if (currentSessionId && currentModel) {
2861
- this.sessionIds.set(currentModel, currentSessionId);
2862
- }
2863
- const newMode = this.agentMode;
2864
- const isParentTask = !!this.taskContext.isParentTask;
2865
- const newModel = this.getModelForMode(newMode, isParentTask);
2866
- const resumeSessionId = newModel ? this.sessionIds.get(newModel) : null;
2867
- if (resumeSessionId) {
2868
- this.taskContext.claudeSessionId = resumeSessionId;
2869
- } else {
2870
- this.taskContext.claudeSessionId = null;
2871
- this.connection.storeSessionId("");
2872
- }
2873
- if (newModel) {
2874
- this.taskContext.model = newModel;
2875
- }
2876
- this.taskContext.agentMode = newMode;
2877
- this.connection.emitModeTransition({
2878
- fromMode: "auto",
2879
- toMode: newMode ?? "building"
2880
- });
2881
- this.connection.emitModeChanged(newMode);
2882
- this.connection.postChatMessage(
2883
- `Transitioning to **${newMode}** mode${newModel ? ` with ${newModel}` : ""}. Restarting session...`
2884
- );
2885
- try {
2886
- const freshContext = await this.connection.fetchTaskContext();
2887
- freshContext._runnerSessionId = this.taskContext._runnerSessionId;
2888
- freshContext.claudeSessionId = this.taskContext.claudeSessionId;
2889
- freshContext.agentMode = newMode;
2890
- if (newModel) freshContext.model = newModel;
2891
- this.taskContext = freshContext;
2892
- } catch {
2893
- }
2894
- await this.setState("running");
2895
- await this.runQuerySafe(this.taskContext);
2896
- if (!this.stopped && this._state !== "error") {
2897
- await this.setState("idle");
2898
- }
2899
- }
2900
- /**
2901
- * Get the appropriate model for a given mode.
2902
- * Building uses the builder model (Sonnet), Discovery/Review use PM model (Opus).
2903
- */
2904
- getModelForMode(mode, isParentTask) {
2905
- if (!this.taskContext) return null;
2906
- if (mode === "building" && !isParentTask) {
2907
- return this.taskContext.builderModel ?? this.taskContext.model;
2908
- }
2909
- return this.taskContext.pmModel ?? this.taskContext.model;
2910
- }
2911
- async runSetupSafe() {
2912
- await this.setState("setup");
2913
- const ports = await loadForwardPorts(this.config.workspaceDir);
2914
- if (ports.length > 0 && process.env.CODESPACE_NAME) {
2915
- const visibility = ports.map((p) => `${p}:public`).join(" ");
2916
- runStartCommand(
2917
- `gh codespace ports visibility ${visibility} -c "${process.env.CODESPACE_NAME}" 2>/dev/null`,
2918
- this.config.workspaceDir,
2919
- () => void 0
2920
- );
2921
- }
2922
- const config = await loadConveyorConfig(this.config.workspaceDir);
2923
- if (!config) {
2924
- this.connection.sendEvent({ type: "setup_complete" });
2925
- await this.callbacks.onEvent({ type: "setup_complete" });
2926
- return true;
2927
- }
2928
- try {
2929
- await this.executeSetupConfig(config);
2930
- const setupEvent = {
2931
- type: "setup_complete",
2932
- previewPort: config.previewPort ?? void 0,
2933
- startCommandDeferred: this.deferredStartConfig !== null ? true : void 0
2934
- };
2935
- this.connection.sendEvent(setupEvent);
2936
- await this.callbacks.onEvent(setupEvent);
2937
- return true;
2938
- } catch (error) {
2939
- const message = error instanceof Error ? error.message : "Setup failed";
2940
- this.connection.sendEvent({ type: "setup_error", message });
2941
- await this.callbacks.onEvent({ type: "setup_error", message });
2942
- this.connection.postChatMessage(
2943
- `Environment setup failed: ${message}
2944
- The agent cannot start until this is resolved.`
2945
- );
2946
- return false;
2947
- }
3035
+ handleRunStartCommand() {
3036
+ if (!this.deferredStartConfig?.startCommand || this.startCommandStarted) return;
3037
+ this.startCommandStarted = true;
3038
+ runDeferredStartCommand(this.deferredStartConfig, this.config, this.connection, this.setupLog);
3039
+ this.deferredStartConfig = null;
2948
3040
  }
2949
3041
  logEffectiveSettings() {
2950
3042
  if (!this.taskContext) return;
2951
3043
  const s = this.taskContext.agentSettings ?? this.config.agentSettings ?? {};
2952
3044
  const model = this.taskContext.model || this.config.model;
2953
- const thinking = s.thinking?.type === "enabled" ? `enabled(${s.thinking.budgetTokens ?? "?"})` : s.thinking?.type ?? "default";
3045
+ const thinking = formatThinkingSetting(s.thinking);
2954
3046
  const parts = [
2955
3047
  `model=${model}`,
2956
3048
  `mode=${this.config.mode ?? "task"}`,
@@ -2964,84 +3056,8 @@ The agent cannot start until this is resolved.`
2964
3056
  if (s.enableFileCheckpointing) parts.push(`checkpointing=on`);
2965
3057
  console.log(`[conveyor-agent] ${parts.join(", ")}`);
2966
3058
  }
2967
- pushSetupLog(line) {
2968
- this.setupLog.push(line);
2969
- if (this.setupLog.length > _AgentRunner.MAX_SETUP_LOG_LINES) {
2970
- this.setupLog.splice(0, this.setupLog.length - _AgentRunner.MAX_SETUP_LOG_LINES);
2971
- }
2972
- }
2973
- async executeSetupConfig(config) {
2974
- if (config.setupCommand) {
2975
- this.pushSetupLog(`$ ${config.setupCommand}`);
2976
- await runSetupCommand(config.setupCommand, this.config.workspaceDir, (stream, data) => {
2977
- this.connection.sendEvent({ type: "setup_output", stream, data });
2978
- for (const line of data.split("\n").filter(Boolean)) {
2979
- this.pushSetupLog(`[${stream}] ${line}`);
2980
- }
2981
- });
2982
- this.pushSetupLog("(exit 0)");
2983
- }
2984
- if (config.startCommand) {
2985
- if (this.effectiveAgentMode === "auto") {
2986
- this.deferredStartConfig = config;
2987
- this.pushSetupLog(`[conveyor] startCommand deferred (auto mode)`);
2988
- } else {
2989
- this.pushSetupLog(`$ ${config.startCommand} & (background)`);
2990
- runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
2991
- this.connection.sendEvent({ type: "start_command_output", stream, data });
2992
- });
2993
- }
2994
- }
2995
- }
2996
- runDeferredStartCommand() {
2997
- if (!this.deferredStartConfig?.startCommand || this.startCommandStarted) return;
2998
- this.startCommandStarted = true;
2999
- const config = this.deferredStartConfig;
3000
- this.deferredStartConfig = null;
3001
- this.pushSetupLog(`$ ${config.startCommand} & (background, deferred)`);
3002
- this.connection.sendEvent({ type: "start_command_started" });
3003
- runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
3004
- this.connection.sendEvent({ type: "start_command_output", stream, data });
3005
- });
3006
- const setupEvent = {
3007
- type: "setup_complete",
3008
- previewPort: config.previewPort ?? void 0
3009
- };
3010
- this.connection.sendEvent(setupEvent);
3011
- }
3012
3059
  injectHumanMessage(content, files) {
3013
- let messageContent;
3014
- if (files?.length) {
3015
- const blocks = [{ type: "text", text: content }];
3016
- for (const f of files) {
3017
- if (f.content && f.contentEncoding === "base64") {
3018
- blocks.push({
3019
- type: "image",
3020
- source: { type: "base64", media_type: f.mimeType, data: f.content }
3021
- });
3022
- blocks.push({
3023
- type: "text",
3024
- text: `[Attached image: ${f.fileName} (${f.mimeType})]`
3025
- });
3026
- } else if (f.content && f.contentEncoding === "utf-8") {
3027
- blocks.push({
3028
- type: "text",
3029
- text: `[Attached file: ${f.fileName} (${f.mimeType})]
3030
- \`\`\`
3031
- ${f.content}
3032
- \`\`\``
3033
- });
3034
- } else {
3035
- blocks.push({
3036
- type: "text",
3037
- text: `[Attached file: ${f.fileName} (${f.mimeType})] Download: ${f.downloadUrl}`
3038
- });
3039
- }
3040
- }
3041
- messageContent = blocks;
3042
- } else {
3043
- messageContent = content;
3044
- }
3060
+ const messageContent = buildMessageContent(content, files);
3045
3061
  const msg = {
3046
3062
  type: "user",
3047
3063
  session_id: "",
@@ -3085,16 +3101,11 @@ ${f.content}
3085
3101
  }
3086
3102
  async waitForUserContent() {
3087
3103
  if (this.pendingMessages.length > 0) {
3088
- const next = this.pendingMessages.shift();
3089
- const content2 = next?.message.content;
3090
- if (!content2) return null;
3091
- return content2;
3104
+ const content = this.pendingMessages.shift()?.message.content;
3105
+ return content ?? null;
3092
3106
  }
3093
3107
  const msg = await this.waitForMessage();
3094
- if (!msg) return null;
3095
- const content = msg.message.content;
3096
- if (!content) return null;
3097
- return content;
3108
+ return msg?.message.content ?? null;
3098
3109
  }
3099
3110
  async *createInputStream(initialPrompt) {
3100
3111
  const makeUserMessage = (content) => ({
@@ -3124,55 +3135,34 @@ ${f.content}
3124
3135
  }
3125
3136
  }
3126
3137
  asQueryHost() {
3127
- const getEffectiveAgentMode = () => this.effectiveAgentMode;
3128
- const getHasExitedPlanMode = () => this.hasExitedPlanMode;
3129
- const setHasExitedPlanMode = (val) => {
3130
- this.hasExitedPlanMode = val;
3131
- };
3132
- const getPendingModeRestart = () => this.pendingModeRestart;
3133
- const setPendingModeRestart = (val) => {
3134
- this.pendingModeRestart = val;
3135
- };
3136
- const getIsParentTask = () => !!this.taskContext?.isParentTask;
3137
- return {
3138
+ return buildQueryHost({
3138
3139
  config: this.config,
3139
3140
  connection: this.connection,
3140
3141
  callbacks: this.callbacks,
3141
3142
  setupLog: this.setupLog,
3142
3143
  costTracker: this.costTracker,
3143
- get agentMode() {
3144
- return getEffectiveAgentMode();
3145
- },
3146
- get isParentTask() {
3147
- return getIsParentTask();
3148
- },
3149
- get hasExitedPlanMode() {
3150
- return getHasExitedPlanMode();
3151
- },
3152
- set hasExitedPlanMode(val) {
3153
- setHasExitedPlanMode(val);
3154
- },
3155
- get pendingModeRestart() {
3156
- return getPendingModeRestart();
3144
+ planSync: this.planSync,
3145
+ sessionIds: this.sessionIds,
3146
+ getEffectiveAgentMode: () => this.effectiveAgentMode,
3147
+ getIsParentTask: () => !!this.taskContext?.isParentTask,
3148
+ getHasExitedPlanMode: () => this.hasExitedPlanMode,
3149
+ setHasExitedPlanMode: (val) => {
3150
+ this.hasExitedPlanMode = val;
3157
3151
  },
3158
- set pendingModeRestart(val) {
3159
- setPendingModeRestart(val);
3152
+ getPendingModeRestart: () => this.pendingModeRestart,
3153
+ setPendingModeRestart: (val) => {
3154
+ this.pendingModeRestart = val;
3160
3155
  },
3161
- sessionIds: this.sessionIds,
3162
3156
  isStopped: () => this.stopped,
3163
3157
  createInputStream: (prompt) => this.createInputStream(prompt),
3164
- snapshotPlanFiles: () => this.planSync.snapshotPlanFiles(),
3165
- syncPlanFile: () => this.planSync.syncPlanFile(),
3166
3158
  onModeTransition: (newMode) => {
3167
3159
  this.agentMode = newMode;
3168
3160
  }
3169
- };
3161
+ });
3170
3162
  }
3171
3163
  handleModeChange(newAgentMode) {
3172
3164
  if (this.config.mode !== "pm") return;
3173
- if (newAgentMode) {
3174
- this.agentMode = newAgentMode;
3175
- }
3165
+ if (newAgentMode) this.agentMode = newAgentMode;
3176
3166
  const effectiveMode = this.effectiveAgentMode;
3177
3167
  this.connection.emitModeChanged(effectiveMode);
3178
3168
  this.connection.postChatMessage(
@@ -3239,87 +3229,103 @@ function buildPrompt(message, chatHistory) {
3239
3229
  ${message.content}`);
3240
3230
  return parts.join("\n");
3241
3231
  }
3242
- async function handleProjectChatMessage(message, connection, projectDir) {
3243
- connection.emitAgentStatus("busy");
3232
+ function processContentBlock(block, responseParts, turnToolCalls) {
3233
+ if (block.type === "text" && block.text) {
3234
+ responseParts.push(block.text);
3235
+ } else if (block.type === "tool_use" && block.name) {
3236
+ const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
3237
+ turnToolCalls.push({
3238
+ tool: block.name,
3239
+ input: inputStr.slice(0, 1e4),
3240
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3241
+ });
3242
+ console.log(`[project-chat] [tool_use] ${block.name}`);
3243
+ }
3244
+ }
3245
+ async function fetchContext(connection) {
3246
+ let agentCtx = null;
3244
3247
  try {
3245
- let agentCtx = null;
3246
- try {
3247
- agentCtx = await connection.fetchAgentContext();
3248
- } catch {
3249
- console.log("[project-chat] Could not fetch agent context, using defaults");
3248
+ agentCtx = await connection.fetchAgentContext();
3249
+ } catch {
3250
+ console.log("[project-chat] Could not fetch agent context, using defaults");
3251
+ }
3252
+ let chatHistory = [];
3253
+ try {
3254
+ chatHistory = await connection.fetchChatHistory(30);
3255
+ } catch {
3256
+ console.log("[project-chat] Could not fetch chat history, proceeding without it");
3257
+ }
3258
+ return { agentCtx, chatHistory };
3259
+ }
3260
+ function buildChatQueryOptions(agentCtx, projectDir) {
3261
+ const model = agentCtx?.model || FALLBACK_MODEL;
3262
+ const settings = agentCtx?.agentSettings ?? {};
3263
+ return {
3264
+ model,
3265
+ systemPrompt: {
3266
+ type: "preset",
3267
+ preset: "claude_code",
3268
+ append: buildSystemPrompt2(projectDir, agentCtx)
3269
+ },
3270
+ cwd: projectDir,
3271
+ permissionMode: "bypassPermissions",
3272
+ allowDangerouslySkipPermissions: true,
3273
+ tools: { type: "preset", preset: "claude_code" },
3274
+ maxTurns: settings.maxTurns ?? 15,
3275
+ maxBudgetUsd: settings.maxBudgetUsd ?? 5,
3276
+ effort: settings.effort,
3277
+ thinking: settings.thinking
3278
+ };
3279
+ }
3280
+ function processEventStream(event, connection, responseParts, turnToolCalls, isTyping) {
3281
+ if (event.type === "assistant") {
3282
+ if (!isTyping.value) {
3283
+ setTimeout(() => connection.emitEvent({ type: "agent_typing_start" }), 200);
3284
+ isTyping.value = true;
3250
3285
  }
3251
- let chatHistory = [];
3252
- try {
3253
- chatHistory = await connection.fetchChatHistory(30);
3254
- } catch {
3255
- console.log("[project-chat] Could not fetch chat history, proceeding without it");
3256
- }
3257
- const model = agentCtx?.model || FALLBACK_MODEL;
3258
- const settings = agentCtx?.agentSettings ?? {};
3259
- const systemPrompt = buildSystemPrompt2(projectDir, agentCtx);
3260
- const prompt = buildPrompt(message, chatHistory);
3261
- const events = query2({
3262
- prompt,
3263
- options: {
3264
- model,
3265
- systemPrompt: {
3266
- type: "preset",
3267
- preset: "claude_code",
3268
- append: systemPrompt
3269
- },
3270
- cwd: projectDir,
3271
- permissionMode: "bypassPermissions",
3272
- allowDangerouslySkipPermissions: true,
3273
- tools: { type: "preset", preset: "claude_code" },
3274
- maxTurns: settings.maxTurns ?? 15,
3275
- maxBudgetUsd: settings.maxBudgetUsd ?? 5,
3276
- effort: settings.effort,
3277
- thinking: settings.thinking
3278
- }
3279
- });
3280
- const responseParts = [];
3281
- const turnToolCalls = [];
3282
- let isTyping = false;
3283
- for await (const event of events) {
3284
- if (event.type === "assistant") {
3285
- if (!isTyping) {
3286
- setTimeout(() => connection.emitEvent({ type: "agent_typing_start" }), 200);
3287
- isTyping = true;
3288
- }
3289
- const assistantEvent = event;
3290
- const { content } = assistantEvent.message;
3291
- for (const block of content) {
3292
- if (block.type === "text") {
3293
- responseParts.push(block.text);
3294
- } else if (block.type === "tool_use") {
3295
- const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
3296
- turnToolCalls.push({
3297
- tool: block.name,
3298
- input: inputStr.slice(0, 1e4),
3299
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3300
- });
3301
- console.log(`[project-chat] [tool_use] ${block.name}`);
3302
- }
3303
- }
3304
- if (turnToolCalls.length > 0) {
3305
- connection.emitEvent({ type: "activity_block", events: [...turnToolCalls] });
3306
- turnToolCalls.length = 0;
3307
- }
3308
- } else if (event.type === "result") {
3309
- if (isTyping) {
3310
- connection.emitEvent({ type: "agent_typing_stop" });
3311
- isTyping = false;
3312
- }
3313
- break;
3314
- }
3286
+ const assistantEvent = event;
3287
+ for (const block of assistantEvent.message.content) {
3288
+ processContentBlock(block, responseParts, turnToolCalls);
3315
3289
  }
3316
- if (isTyping) {
3317
- connection.emitEvent({ type: "agent_typing_stop" });
3290
+ if (turnToolCalls.length > 0) {
3291
+ connection.emitEvent({ type: "activity_block", events: [...turnToolCalls] });
3292
+ turnToolCalls.length = 0;
3318
3293
  }
3319
- const responseText = responseParts.join("\n\n").trim();
3320
- if (responseText) {
3321
- await connection.emitChatMessage(responseText);
3294
+ return false;
3295
+ }
3296
+ if (event.type === "result") {
3297
+ if (isTyping.value) {
3298
+ connection.emitEvent({ type: "agent_typing_stop" });
3299
+ isTyping.value = false;
3322
3300
  }
3301
+ return true;
3302
+ }
3303
+ return false;
3304
+ }
3305
+ async function runChatQuery(message, connection, projectDir) {
3306
+ const { agentCtx, chatHistory } = await fetchContext(connection);
3307
+ const options = buildChatQueryOptions(agentCtx, projectDir);
3308
+ const prompt = buildPrompt(message, chatHistory);
3309
+ const events = query2({ prompt, options });
3310
+ const responseParts = [];
3311
+ const turnToolCalls = [];
3312
+ const isTyping = { value: false };
3313
+ for await (const event of events) {
3314
+ const done = processEventStream(event, connection, responseParts, turnToolCalls, isTyping);
3315
+ if (done) break;
3316
+ }
3317
+ if (isTyping.value) {
3318
+ connection.emitEvent({ type: "agent_typing_stop" });
3319
+ }
3320
+ const responseText = responseParts.join("\n\n").trim();
3321
+ if (responseText) {
3322
+ await connection.emitChatMessage(responseText);
3323
+ }
3324
+ }
3325
+ async function handleProjectChatMessage(message, connection, projectDir) {
3326
+ connection.emitAgentStatus("busy");
3327
+ try {
3328
+ await runChatQuery(message, connection, projectDir);
3323
3329
  } catch (error) {
3324
3330
  console.error(
3325
3331
  "[project-chat] Failed to handle message:",
@@ -3342,6 +3348,72 @@ var __dirname = path.dirname(__filename);
3342
3348
  var HEARTBEAT_INTERVAL_MS2 = 3e4;
3343
3349
  var MAX_CONCURRENT = 5;
3344
3350
  var STOP_TIMEOUT_MS = 3e4;
3351
+ function setupWorkDir(projectDir, assignment) {
3352
+ const { taskId, branch, devBranch, useWorktree } = assignment;
3353
+ const shortId = taskId.slice(0, 8);
3354
+ const shouldWorktree = useWorktree === true;
3355
+ let workDir;
3356
+ if (shouldWorktree) {
3357
+ workDir = ensureWorktree(projectDir, taskId, devBranch);
3358
+ } else {
3359
+ workDir = projectDir;
3360
+ }
3361
+ if (branch && branch !== devBranch) {
3362
+ try {
3363
+ execSync4(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
3364
+ } catch {
3365
+ try {
3366
+ execSync4(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
3367
+ } catch {
3368
+ console.log(`[task:${shortId}] Warning: could not checkout branch ${branch}`);
3369
+ }
3370
+ }
3371
+ }
3372
+ return { workDir, usesWorktree: shouldWorktree };
3373
+ }
3374
+ function spawnChildAgent(assignment, workDir) {
3375
+ const { taskToken, apiUrl, taskId, mode, isAuto, useSandbox } = assignment;
3376
+ const cliPath = path.resolve(__dirname, "cli.js");
3377
+ const childEnv = { ...process.env };
3378
+ delete childEnv.CONVEYOR_PROJECT_TOKEN;
3379
+ delete childEnv.CONVEYOR_PROJECT_ID;
3380
+ const child = fork(cliPath, [], {
3381
+ env: {
3382
+ ...childEnv,
3383
+ CONVEYOR_API_URL: apiUrl,
3384
+ CONVEYOR_TASK_TOKEN: taskToken,
3385
+ CONVEYOR_TASK_ID: taskId,
3386
+ CONVEYOR_MODE: mode,
3387
+ CONVEYOR_WORKSPACE: workDir,
3388
+ CONVEYOR_USE_WORKTREE: "false",
3389
+ CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
3390
+ CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
3391
+ CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false"
3392
+ },
3393
+ cwd: workDir,
3394
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
3395
+ });
3396
+ child.stdin?.on("error", () => {
3397
+ });
3398
+ child.stdout?.on("error", () => {
3399
+ });
3400
+ child.stderr?.on("error", () => {
3401
+ });
3402
+ const shortId = taskId.slice(0, 8);
3403
+ child.stdout?.on("data", (data) => {
3404
+ const lines = data.toString().trimEnd().split("\n");
3405
+ for (const line of lines) {
3406
+ console.log(`[task:${shortId}] ${line}`);
3407
+ }
3408
+ });
3409
+ child.stderr?.on("data", (data) => {
3410
+ const lines = data.toString().trimEnd().split("\n");
3411
+ for (const line of lines) {
3412
+ console.error(`[task:${shortId}] ${line}`);
3413
+ }
3414
+ });
3415
+ return child;
3416
+ }
3345
3417
  var ProjectRunner = class {
3346
3418
  connection;
3347
3419
  projectDir;
@@ -3360,7 +3432,7 @@ var ProjectRunner = class {
3360
3432
  async start() {
3361
3433
  await this.connection.connect();
3362
3434
  this.connection.onTaskAssignment((assignment) => {
3363
- void this.handleAssignment(assignment);
3435
+ this.handleAssignment(assignment);
3364
3436
  });
3365
3437
  this.connection.onStopTask((data) => {
3366
3438
  this.handleStopTask(data.taskId);
@@ -3383,8 +3455,8 @@ var ProjectRunner = class {
3383
3455
  process.on("SIGINT", () => void this.stop());
3384
3456
  });
3385
3457
  }
3386
- async handleAssignment(assignment) {
3387
- const { taskId, taskToken, apiUrl, mode, branch, devBranch, useWorktree, isAuto, useSandbox } = assignment;
3458
+ handleAssignment(assignment) {
3459
+ const { taskId, mode } = assignment;
3388
3460
  const shortId = taskId.slice(0, 8);
3389
3461
  if (this.activeAgents.has(taskId)) {
3390
3462
  console.log(`[project-runner] Task ${shortId} already running, skipping`);
@@ -3403,67 +3475,13 @@ var ProjectRunner = class {
3403
3475
  } catch {
3404
3476
  console.log(`[task:${shortId}] Warning: git fetch failed`);
3405
3477
  }
3406
- let workDir;
3407
- const shouldWorktree = useWorktree === true;
3408
- if (shouldWorktree) {
3409
- workDir = ensureWorktree(this.projectDir, taskId, devBranch);
3410
- } else {
3411
- workDir = this.projectDir;
3412
- }
3413
- if (branch && branch !== devBranch) {
3414
- try {
3415
- execSync4(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
3416
- } catch {
3417
- try {
3418
- execSync4(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
3419
- } catch {
3420
- console.log(`[task:${shortId}] Warning: could not checkout branch ${branch}`);
3421
- }
3422
- }
3423
- }
3424
- const cliPath = path.resolve(__dirname, "cli.js");
3425
- const childEnv = { ...process.env };
3426
- delete childEnv.CONVEYOR_PROJECT_TOKEN;
3427
- delete childEnv.CONVEYOR_PROJECT_ID;
3428
- const child = fork(cliPath, [], {
3429
- env: {
3430
- ...childEnv,
3431
- CONVEYOR_API_URL: apiUrl,
3432
- CONVEYOR_TASK_TOKEN: taskToken,
3433
- CONVEYOR_TASK_ID: taskId,
3434
- CONVEYOR_MODE: mode,
3435
- CONVEYOR_WORKSPACE: workDir,
3436
- CONVEYOR_USE_WORKTREE: "false",
3437
- CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
3438
- CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
3439
- CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false"
3440
- },
3441
- cwd: workDir,
3442
- stdio: ["pipe", "pipe", "pipe", "ipc"]
3443
- });
3444
- child.stdin?.on("error", () => {
3445
- });
3446
- child.stdout?.on("error", () => {
3447
- });
3448
- child.stderr?.on("error", () => {
3449
- });
3450
- child.stdout?.on("data", (data) => {
3451
- const lines = data.toString().trimEnd().split("\n");
3452
- for (const line of lines) {
3453
- console.log(`[task:${shortId}] ${line}`);
3454
- }
3455
- });
3456
- child.stderr?.on("data", (data) => {
3457
- const lines = data.toString().trimEnd().split("\n");
3458
- for (const line of lines) {
3459
- console.error(`[task:${shortId}] ${line}`);
3460
- }
3461
- });
3478
+ const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
3479
+ const child = spawnChildAgent(assignment, workDir);
3462
3480
  this.activeAgents.set(taskId, {
3463
3481
  process: child,
3464
3482
  worktreePath: workDir,
3465
3483
  mode,
3466
- usesWorktree: shouldWorktree
3484
+ usesWorktree
3467
3485
  });
3468
3486
  this.connection.emitTaskStarted(taskId);
3469
3487
  console.log(`[project-runner] Started task ${shortId} in ${mode} mode at ${workDir}`);
@@ -3472,7 +3490,7 @@ var ProjectRunner = class {
3472
3490
  const reason = code === 0 ? "completed" : `exited with code ${code}`;
3473
3491
  this.connection.emitTaskStopped(taskId, reason);
3474
3492
  console.log(`[project-runner] Task ${shortId} ${reason}`);
3475
- if (code === 0 && shouldWorktree) {
3493
+ if (code === 0 && usesWorktree) {
3476
3494
  try {
3477
3495
  removeWorktree(this.projectDir, taskId);
3478
3496
  } catch {
@@ -3534,7 +3552,9 @@ var ProjectRunner = class {
3534
3552
  );
3535
3553
  await Promise.race([
3536
3554
  Promise.all(stopPromises),
3537
- new Promise((resolve2) => setTimeout(resolve2, 6e4))
3555
+ new Promise((resolve2) => {
3556
+ setTimeout(resolve2, 6e4);
3557
+ })
3538
3558
  ]);
3539
3559
  this.connection.disconnect();
3540
3560
  console.log("[project-runner] Shutdown complete");
@@ -3612,13 +3632,13 @@ var FileCache = class {
3612
3632
  export {
3613
3633
  ConveyorConnection,
3614
3634
  ProjectConnection,
3635
+ ensureWorktree,
3636
+ removeWorktree,
3615
3637
  loadConveyorConfig,
3616
3638
  runSetupCommand,
3617
3639
  runStartCommand,
3618
- ensureWorktree,
3619
- removeWorktree,
3620
3640
  AgentRunner,
3621
3641
  ProjectRunner,
3622
3642
  FileCache
3623
3643
  };
3624
- //# sourceMappingURL=chunk-NUD2M24L.js.map
3644
+ //# sourceMappingURL=chunk-FS3A4THO.js.map