@rallycry/conveyor-agent 2.13.0 → 2.14.1

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.
@@ -15,7 +15,7 @@ var ConveyorConnection = class _ConveyorConnection {
15
15
  this.config = config;
16
16
  }
17
17
  connect() {
18
- return new Promise((resolve, reject) => {
18
+ return new Promise((resolve2, reject) => {
19
19
  let settled = false;
20
20
  let attempts = 0;
21
21
  const maxInitialAttempts = 30;
@@ -61,7 +61,7 @@ var ConveyorConnection = class _ConveyorConnection {
61
61
  this.socket.on("connect", () => {
62
62
  if (!settled) {
63
63
  settled = true;
64
- resolve();
64
+ resolve2();
65
65
  }
66
66
  });
67
67
  this.socket.io.on("reconnect_attempt", () => {
@@ -76,13 +76,13 @@ var ConveyorConnection = class _ConveyorConnection {
76
76
  fetchChatMessages(limit) {
77
77
  const socket = this.socket;
78
78
  if (!socket) throw new Error("Not connected");
79
- return new Promise((resolve, reject) => {
79
+ return new Promise((resolve2, reject) => {
80
80
  socket.emit(
81
81
  "agentRunner:getChatMessages",
82
82
  { taskId: this.config.taskId, limit },
83
83
  (response) => {
84
84
  if (response.success && response.data) {
85
- resolve(response.data);
85
+ resolve2(response.data);
86
86
  } else {
87
87
  reject(new Error(response.error ?? "Failed to fetch chat messages"));
88
88
  }
@@ -93,13 +93,13 @@ var ConveyorConnection = class _ConveyorConnection {
93
93
  fetchTaskContext() {
94
94
  const socket = this.socket;
95
95
  if (!socket) throw new Error("Not connected");
96
- return new Promise((resolve, reject) => {
96
+ return new Promise((resolve2, reject) => {
97
97
  socket.emit(
98
98
  "agentRunner:getTaskContext",
99
99
  { taskId: this.config.taskId },
100
100
  (response) => {
101
101
  if (response.success && response.data) {
102
- resolve(response.data);
102
+ resolve2(response.data);
103
103
  } else {
104
104
  reject(new Error(response.error ?? "Failed to fetch task context"));
105
105
  }
@@ -142,13 +142,13 @@ var ConveyorConnection = class _ConveyorConnection {
142
142
  createPR(params) {
143
143
  const socket = this.socket;
144
144
  if (!socket) throw new Error("Not connected");
145
- return new Promise((resolve, reject) => {
145
+ return new Promise((resolve2, reject) => {
146
146
  socket.emit(
147
147
  "agentRunner:createPR",
148
148
  { taskId: this.config.taskId, ...params },
149
149
  (response) => {
150
150
  if (response.success && response.data) {
151
- resolve(response.data);
151
+ resolve2(response.data);
152
152
  } else {
153
153
  reject(new Error(response.error ?? "Failed to create pull request"));
154
154
  }
@@ -163,8 +163,8 @@ var ConveyorConnection = class _ConveyorConnection {
163
163
  requestId,
164
164
  questions
165
165
  });
166
- return new Promise((resolve) => {
167
- this.pendingQuestionResolvers.set(requestId, resolve);
166
+ return new Promise((resolve2) => {
167
+ this.pendingQuestionResolvers.set(requestId, resolve2);
168
168
  });
169
169
  }
170
170
  cancelPendingQuestions() {
@@ -224,6 +224,45 @@ var ConveyorConnection = class _ConveyorConnection {
224
224
  sendTypingStop() {
225
225
  this.sendEvent({ type: "agent_typing_stop" });
226
226
  }
227
+ createSubtask(data) {
228
+ const socket = this.socket;
229
+ if (!socket) throw new Error("Not connected");
230
+ return new Promise((resolve2, reject) => {
231
+ socket.emit(
232
+ "agentRunner:createSubtask",
233
+ data,
234
+ (response) => {
235
+ if (response.success && response.data) resolve2(response.data);
236
+ else reject(new Error(response.error ?? "Failed to create subtask"));
237
+ }
238
+ );
239
+ });
240
+ }
241
+ updateSubtask(subtaskId, fields) {
242
+ if (!this.socket) throw new Error("Not connected");
243
+ this.socket.emit("agentRunner:updateSubtask", {
244
+ subtaskId,
245
+ fields
246
+ });
247
+ }
248
+ deleteSubtask(subtaskId) {
249
+ if (!this.socket) throw new Error("Not connected");
250
+ this.socket.emit("agentRunner:deleteSubtask", { subtaskId });
251
+ }
252
+ listSubtasks() {
253
+ const socket = this.socket;
254
+ if (!socket) throw new Error("Not connected");
255
+ return new Promise((resolve2, reject) => {
256
+ socket.emit(
257
+ "agentRunner:listSubtasks",
258
+ {},
259
+ (response) => {
260
+ if (response.success && response.data) resolve2(response.data);
261
+ else reject(new Error(response.error ?? "Failed to list subtasks"));
262
+ }
263
+ );
264
+ });
265
+ }
227
266
  disconnect() {
228
267
  this.flushEvents();
229
268
  this.socket?.disconnect();
@@ -265,7 +304,7 @@ async function loadConveyorConfig(workspaceDir) {
265
304
  return null;
266
305
  }
267
306
  function runSetupCommand(cmd, cwd, onOutput) {
268
- return new Promise((resolve, reject) => {
307
+ return new Promise((resolve2, reject) => {
269
308
  const child = spawn("sh", ["-c", cmd], {
270
309
  cwd,
271
310
  stdio: ["ignore", "pipe", "pipe"],
@@ -279,7 +318,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
279
318
  });
280
319
  child.on("close", (code) => {
281
320
  if (code === 0) {
282
- resolve();
321
+ resolve2();
283
322
  } else {
284
323
  reject(new Error(`Setup command exited with code ${code}`));
285
324
  }
@@ -349,14 +388,17 @@ function ensureWorktree(projectDir, taskId, branch) {
349
388
  if (existsSync(worktreePath)) {
350
389
  if (branch) {
351
390
  try {
352
- execSync2(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
391
+ execSync2(`git checkout --detach origin/${branch}`, {
392
+ cwd: worktreePath,
393
+ stdio: "ignore"
394
+ });
353
395
  } catch {
354
396
  }
355
397
  }
356
398
  return worktreePath;
357
399
  }
358
- const target = branch ?? "HEAD";
359
- execSync2(`git worktree add "${worktreePath}" ${target}`, {
400
+ const ref = branch ? `origin/${branch}` : "HEAD";
401
+ execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
360
402
  cwd: projectDir,
361
403
  stdio: "ignore"
362
404
  });
@@ -385,6 +427,7 @@ import { randomUUID } from "crypto";
385
427
  import { query } from "@anthropic-ai/claude-agent-sdk";
386
428
 
387
429
  // src/prompt-builder.ts
430
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
388
431
  function findLastAgentMessageIndex(history) {
389
432
  for (let i = history.length - 1; i >= 0; i--) {
390
433
  if (history[i].role === "assistant") return i;
@@ -394,7 +437,7 @@ function findLastAgentMessageIndex(history) {
394
437
  function detectRelaunchScenario(context) {
395
438
  const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
396
439
  if (lastAgentIdx === -1) return "fresh";
397
- const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId;
440
+ const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId || ACTIVE_STATUSES.has(context.status ?? "");
398
441
  if (!hasPriorWork) return "fresh";
399
442
  const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
400
443
  const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
@@ -424,12 +467,13 @@ You are the project manager for this task.`,
424
467
  const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
425
468
  parts.push(
426
469
  `You have been relaunched with new feedback.`,
427
- `Work on the git branch "${context.githubBranch}".`,
470
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
428
471
  `
429
472
  New messages since your last run:`,
430
473
  ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
431
474
  `
432
- Address the requested changes. Commit and push your updates.`
475
+ 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.`,
476
+ `Commit and push your updates.`
433
477
  );
434
478
  if (context.githubPRUrl) {
435
479
  parts.push(
@@ -443,7 +487,8 @@ Address the requested changes. Commit and push your updates.`
443
487
  } else {
444
488
  parts.push(
445
489
  `You were relaunched but no new instructions have been given since your last run.`,
446
- `Work on the git branch "${context.githubBranch}".`,
490
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
491
+ `Run \`git log --oneline -10\` to review what you already committed.`,
447
492
  `Review the current state of the codebase and verify everything is working correctly.`,
448
493
  `Post a brief status update to the chat, then wait for further instructions.`
449
494
  );
@@ -515,7 +560,7 @@ function buildInstructions(mode, context, scenario) {
515
560
  parts.push(
516
561
  `Begin executing the task plan above immediately.`,
517
562
  `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.`,
518
- `Work on the git branch "${context.githubBranch}".`,
563
+ `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
519
564
  `Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
520
565
  `When finished, commit your changes, push the branch, and use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
521
566
  );
@@ -530,9 +575,9 @@ function buildInstructions(mode, context, scenario) {
530
575
  } else {
531
576
  parts.push(
532
577
  `You were relaunched but no new instructions have been given since your last run.`,
533
- `Work on the git branch "${context.githubBranch}".`,
534
- `Review the current state of the codebase and verify everything is working correctly (e.g. tests pass, the web server starts on port 3000).`,
535
- `Post a brief status update to the chat summarizing the current state.`,
578
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
579
+ `Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
580
+ `Post a brief status update to the chat summarizing where things stand.`,
536
581
  `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
537
582
  );
538
583
  if (context.githubPRUrl) {
@@ -554,13 +599,15 @@ Review these messages and wait for the team to provide instructions before takin
554
599
  );
555
600
  } else {
556
601
  parts.push(
557
- `You were relaunched with new feedback since your last run.`,
558
- `Work on the git branch "${context.githubBranch}".`,
602
+ `You have been relaunched to address feedback on your previous work.`,
603
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
604
+ `Start by running \`git log --oneline -10\` and \`git diff HEAD~3 HEAD --stat\` to review what you already committed.`,
559
605
  `
560
606
  New messages since your last run:`,
561
607
  ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
562
608
  `
563
- Address the requested changes. Commit and push your updates.`
609
+ 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.`,
610
+ `Commit and push your updates.`
564
611
  );
565
612
  if (context.githubPRUrl) {
566
613
  parts.push(
@@ -617,7 +664,14 @@ IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the follow
617
664
  `- bun dev, npm start, or any dev server startup commands`,
618
665
  `- pwd, ls, echo, or exploratory shell commands to "check" the environment`,
619
666
  `Only run these if you encounter a specific error that requires it.`,
620
- `Start reading the task plan and writing code immediately.`
667
+ `Start reading the task plan and writing code immediately.`,
668
+ `
669
+ Git safety \u2014 STRICT rules:`,
670
+ `- NEVER run \`git checkout main\`, \`git checkout dev\`, or switch to any branch other than \`${context.githubBranch}\`.`,
671
+ `- NEVER create new branches (no \`git checkout -b\`, \`git switch -c\`, etc.).`,
672
+ `- This branch was created from \`${context.baseBranch}\`. PRs will automatically target that branch.`,
673
+ `- If \`git push\` fails with "non-fast-forward" or similar, run \`git pull --rebase origin ${context.githubBranch}\` and retry. If that also fails, use \`git push --force-with-lease origin ${context.githubBranch}\`.`,
674
+ `- If you encounter merge conflicts during rebase, resolve them in place \u2014 do NOT abandon the branch.`
621
675
  ];
622
676
  if (setupLog.length > 0) {
623
677
  parts.push(
@@ -675,7 +729,7 @@ function buildCommonTools(connection, config) {
675
729
  );
676
730
  }
677
731
  },
678
- { annotations: { readOnly: true } }
732
+ { annotations: { readOnlyHint: true } }
679
733
  ),
680
734
  tool(
681
735
  "post_to_chat",
@@ -709,7 +763,7 @@ function buildCommonTools(connection, config) {
709
763
  return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
710
764
  }
711
765
  },
712
- { annotations: { readOnly: true } }
766
+ { annotations: { readOnlyHint: true } }
713
767
  )
714
768
  ];
715
769
  }
@@ -730,6 +784,74 @@ function buildPmTools(connection) {
730
784
  return textResult("Failed to update task.");
731
785
  }
732
786
  }
787
+ ),
788
+ tool(
789
+ "create_subtask",
790
+ "Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
791
+ {
792
+ title: z.string().describe("Subtask title"),
793
+ description: z.string().optional().describe("Brief description"),
794
+ plan: z.string().optional().describe("Implementation plan in markdown"),
795
+ ordinal: z.number().optional().describe("Step/order number (0-based)"),
796
+ storyPointValue: z.number().optional().describe("Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)")
797
+ },
798
+ async (params) => {
799
+ try {
800
+ const result = await connection.createSubtask(params);
801
+ return textResult(`Subtask created with ID: ${result.id}`);
802
+ } catch (error) {
803
+ return textResult(
804
+ `Failed to create subtask: ${error instanceof Error ? error.message : "Unknown error"}`
805
+ );
806
+ }
807
+ }
808
+ ),
809
+ tool(
810
+ "update_subtask",
811
+ "Update an existing subtask's fields",
812
+ {
813
+ subtaskId: z.string().describe("The subtask ID to update"),
814
+ title: z.string().optional(),
815
+ description: z.string().optional(),
816
+ plan: z.string().optional(),
817
+ ordinal: z.number().optional(),
818
+ storyPointValue: z.number().optional()
819
+ },
820
+ async ({ subtaskId, ...fields }) => {
821
+ try {
822
+ await Promise.resolve(connection.updateSubtask(subtaskId, fields));
823
+ return textResult("Subtask updated.");
824
+ } catch (error) {
825
+ return textResult(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
826
+ }
827
+ }
828
+ ),
829
+ tool(
830
+ "delete_subtask",
831
+ "Delete a subtask",
832
+ { subtaskId: z.string().describe("The subtask ID to delete") },
833
+ async ({ subtaskId }) => {
834
+ try {
835
+ await Promise.resolve(connection.deleteSubtask(subtaskId));
836
+ return textResult("Subtask deleted.");
837
+ } catch (error) {
838
+ return textResult(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
839
+ }
840
+ }
841
+ ),
842
+ tool(
843
+ "list_subtasks",
844
+ "List all subtasks under the current parent task",
845
+ {},
846
+ async () => {
847
+ try {
848
+ const subtasks = await connection.listSubtasks();
849
+ return textResult(JSON.stringify(subtasks, null, 2));
850
+ } catch {
851
+ return textResult("Failed to list subtasks.");
852
+ }
853
+ },
854
+ { annotations: { readOnlyHint: true } }
733
855
  )
734
856
  ];
735
857
  }
@@ -812,6 +934,7 @@ async function processAssistantEvent(event, host, turnToolCalls) {
812
934
  function handleResultEvent(event, host, context, startTime) {
813
935
  const resultEvent = event;
814
936
  let totalCostUsd = 0;
937
+ let deltaCost = 0;
815
938
  let retriable = false;
816
939
  if (resultEvent.subtype === "success") {
817
940
  totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
@@ -820,15 +943,18 @@ function handleResultEvent(event, host, context, startTime) {
820
943
  if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
821
944
  retriable = true;
822
945
  }
823
- host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
824
- if (totalCostUsd > 0 && context.agentId) {
825
- const estimatedTotalTokens = Math.round(totalCostUsd * 1e5);
946
+ const lastCost = context._lastReportedCostUsd ?? 0;
947
+ deltaCost = totalCostUsd - lastCost;
948
+ context._lastReportedCostUsd = totalCostUsd;
949
+ host.connection.sendEvent({ type: "completed", summary, costUsd: deltaCost, durationMs });
950
+ if (deltaCost > 0 && context.agentId) {
951
+ const estimatedDeltaTokens = Math.round(deltaCost * 1e5);
826
952
  host.connection.trackSpending({
827
953
  agentId: context.agentId,
828
- inputTokens: Math.round(estimatedTotalTokens * 0.7),
829
- outputTokens: Math.round(estimatedTotalTokens * 0.3),
830
- totalTokens: estimatedTotalTokens,
831
- totalCostUsd,
954
+ inputTokens: Math.round(estimatedDeltaTokens * 0.7),
955
+ outputTokens: Math.round(estimatedDeltaTokens * 0.3),
956
+ totalTokens: estimatedDeltaTokens,
957
+ totalCostUsd: deltaCost,
832
958
  onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
833
959
  });
834
960
  }
@@ -840,16 +966,16 @@ function handleResultEvent(event, host, context, startTime) {
840
966
  }
841
967
  host.connection.sendEvent({ type: "error", message: errorMsg });
842
968
  }
843
- return { totalCostUsd, retriable };
969
+ return { totalCostUsd, deltaCost, retriable };
844
970
  }
845
971
  async function emitResultEvent(event, host, context, startTime) {
846
972
  const result = handleResultEvent(event, host, context, startTime);
847
973
  const durationMs = Date.now() - startTime;
848
- if (result.totalCostUsd > 0 && context.agentId) {
974
+ if (result.deltaCost > 0 && context.agentId) {
849
975
  await host.callbacks.onEvent({
850
976
  type: "completed",
851
977
  summary: "Task completed.",
852
- costUsd: result.totalCostUsd,
978
+ costUsd: result.deltaCost,
853
979
  durationMs
854
980
  });
855
981
  } else {
@@ -885,6 +1011,9 @@ async function processEvents(events, context, host) {
885
1011
  if (sessionId && !sessionIdStored) {
886
1012
  sessionIdStored = true;
887
1013
  host.connection.storeSessionId(sessionId);
1014
+ if (sessionId !== context.claudeSessionId) {
1015
+ context._lastReportedCostUsd = 0;
1016
+ }
888
1017
  }
889
1018
  await host.callbacks.onEvent({
890
1019
  type: "thinking",
@@ -941,8 +1070,8 @@ function buildCanUseTool(host) {
941
1070
  input: JSON.stringify(input)
942
1071
  });
943
1072
  const answerPromise = host.connection.askUserQuestion(requestId, questions);
944
- const timeoutPromise = new Promise((resolve) => {
945
- setTimeout(() => resolve(null), QUESTION_TIMEOUT_MS);
1073
+ const timeoutPromise = new Promise((resolve2) => {
1074
+ setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
946
1075
  });
947
1076
  const answers = await Promise.race([answerPromise, timeoutPromise]);
948
1077
  host.connection.emitStatus("running");
@@ -1020,23 +1149,27 @@ ${followUpContent}` : followUpContent;
1020
1149
  async function runWithRetry(initialQuery, context, host, options) {
1021
1150
  for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
1022
1151
  if (host.isStopped()) return;
1023
- const agentQuery = attempt === 0 ? initialQuery : query({
1024
- prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1025
- options: { ...options, resume: void 0 }
1026
- });
1152
+ const agentQuery = attempt === 0 ? initialQuery : (() => {
1153
+ context._lastReportedCostUsd = 0;
1154
+ return query({
1155
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1156
+ options: { ...options, resume: void 0 }
1157
+ });
1158
+ })();
1027
1159
  try {
1028
1160
  const { retriable } = await processEvents(agentQuery, context, host);
1029
1161
  if (!retriable || host.isStopped()) return;
1030
1162
  } catch (error) {
1031
1163
  const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
1032
1164
  if (isStaleSession && context.claudeSessionId) {
1165
+ context.claudeSessionId = null;
1166
+ context._lastReportedCostUsd = 0;
1033
1167
  host.connection.storeSessionId("");
1034
- const freshCtx = { ...context, claudeSessionId: null };
1035
1168
  const freshQuery = query({
1036
- prompt: host.createInputStream(buildInitialPrompt(host.config.mode, freshCtx)),
1169
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1037
1170
  options: { ...options, resume: void 0 }
1038
1171
  });
1039
- return runWithRetry(freshQuery, freshCtx, host, options);
1172
+ return runWithRetry(freshQuery, context, host, options);
1040
1173
  }
1041
1174
  const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
1042
1175
  if (!isApiError) throw error;
@@ -1058,13 +1191,13 @@ async function runWithRetry(initialQuery, context, host, options) {
1058
1191
  });
1059
1192
  host.connection.emitStatus("waiting_for_input");
1060
1193
  await host.callbacks.onStatusChange("waiting_for_input");
1061
- await new Promise((resolve) => {
1062
- const timer = setTimeout(resolve, delayMs);
1194
+ await new Promise((resolve2) => {
1195
+ const timer = setTimeout(resolve2, delayMs);
1063
1196
  const checkStopped = setInterval(() => {
1064
1197
  if (host.isStopped()) {
1065
1198
  clearTimeout(timer);
1066
1199
  clearInterval(checkStopped);
1067
- resolve();
1200
+ resolve2();
1068
1201
  }
1069
1202
  }, 1e3);
1070
1203
  setTimeout(() => clearInterval(checkStopped), delayMs + 100);
@@ -1160,6 +1293,9 @@ var AgentRunner = class _AgentRunner {
1160
1293
  this.connection.disconnect();
1161
1294
  return;
1162
1295
  }
1296
+ if (this.taskContext.claudeSessionId && this.taskContext._existingSpendingTotal !== null) {
1297
+ this.taskContext._lastReportedCostUsd = this.taskContext._existingSpendingTotal;
1298
+ }
1163
1299
  if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
1164
1300
  const result = cleanDevcontainerFromGit(
1165
1301
  this.config.workspaceDir,
@@ -1328,25 +1464,25 @@ ${f.content}
1328
1464
  parent_tool_use_id: null
1329
1465
  };
1330
1466
  if (this.inputResolver) {
1331
- const resolve = this.inputResolver;
1467
+ const resolve2 = this.inputResolver;
1332
1468
  this.inputResolver = null;
1333
- resolve(msg);
1469
+ resolve2(msg);
1334
1470
  } else {
1335
1471
  this.pendingMessages.push(msg);
1336
1472
  }
1337
1473
  }
1338
1474
  waitForMessage() {
1339
- return new Promise((resolve) => {
1475
+ return new Promise((resolve2) => {
1340
1476
  const checkStopped = setInterval(() => {
1341
1477
  if (this.stopped) {
1342
1478
  clearInterval(checkStopped);
1343
1479
  this.inputResolver = null;
1344
- resolve(null);
1480
+ resolve2(null);
1345
1481
  }
1346
1482
  }, 1e3);
1347
1483
  this.inputResolver = (msg) => {
1348
1484
  clearInterval(checkStopped);
1349
- resolve(msg);
1485
+ resolve2(msg);
1350
1486
  };
1351
1487
  });
1352
1488
  }
@@ -1478,6 +1614,275 @@ ${f.content}
1478
1614
  }
1479
1615
  };
1480
1616
 
1617
+ // src/project-connection.ts
1618
+ import { io as io2 } from "socket.io-client";
1619
+ var ProjectConnection = class {
1620
+ socket = null;
1621
+ config;
1622
+ taskAssignmentCallback = null;
1623
+ stopTaskCallback = null;
1624
+ shutdownCallback = null;
1625
+ constructor(config) {
1626
+ this.config = config;
1627
+ }
1628
+ connect() {
1629
+ return new Promise((resolve2, reject) => {
1630
+ let settled = false;
1631
+ let attempts = 0;
1632
+ const maxInitialAttempts = 30;
1633
+ this.socket = io2(this.config.apiUrl, {
1634
+ auth: { projectToken: this.config.projectToken },
1635
+ transports: ["websocket"],
1636
+ reconnection: true,
1637
+ reconnectionAttempts: Infinity,
1638
+ reconnectionDelay: 2e3,
1639
+ reconnectionDelayMax: 3e4,
1640
+ randomizationFactor: 0.3,
1641
+ extraHeaders: {
1642
+ "ngrok-skip-browser-warning": "true"
1643
+ }
1644
+ });
1645
+ this.socket.on("projectRunner:assignTask", (data) => {
1646
+ if (this.taskAssignmentCallback) {
1647
+ this.taskAssignmentCallback(data);
1648
+ }
1649
+ });
1650
+ this.socket.on("projectRunner:stopTask", (data) => {
1651
+ if (this.stopTaskCallback) {
1652
+ this.stopTaskCallback(data);
1653
+ }
1654
+ });
1655
+ this.socket.on("projectRunner:shutdown", () => {
1656
+ if (this.shutdownCallback) {
1657
+ this.shutdownCallback();
1658
+ }
1659
+ });
1660
+ this.socket.on("connect", () => {
1661
+ if (!settled) {
1662
+ settled = true;
1663
+ resolve2();
1664
+ }
1665
+ });
1666
+ this.socket.io.on("reconnect_attempt", () => {
1667
+ attempts++;
1668
+ if (!settled && attempts >= maxInitialAttempts) {
1669
+ settled = true;
1670
+ reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
1671
+ }
1672
+ });
1673
+ });
1674
+ }
1675
+ onTaskAssignment(callback) {
1676
+ this.taskAssignmentCallback = callback;
1677
+ }
1678
+ onStopTask(callback) {
1679
+ this.stopTaskCallback = callback;
1680
+ }
1681
+ onShutdown(callback) {
1682
+ this.shutdownCallback = callback;
1683
+ }
1684
+ sendHeartbeat() {
1685
+ if (!this.socket) return;
1686
+ this.socket.emit("projectRunner:heartbeat", {});
1687
+ }
1688
+ emitTaskStarted(taskId) {
1689
+ if (!this.socket) return;
1690
+ this.socket.emit("projectRunner:taskStarted", { taskId });
1691
+ }
1692
+ emitTaskStopped(taskId, reason) {
1693
+ if (!this.socket) return;
1694
+ this.socket.emit("projectRunner:taskStopped", { taskId, reason });
1695
+ }
1696
+ disconnect() {
1697
+ this.socket?.disconnect();
1698
+ this.socket = null;
1699
+ }
1700
+ };
1701
+
1702
+ // src/project-runner.ts
1703
+ import { fork } from "child_process";
1704
+ import { execSync as execSync4 } from "child_process";
1705
+ import * as path from "path";
1706
+ import { fileURLToPath } from "url";
1707
+ var __filename = fileURLToPath(import.meta.url);
1708
+ var __dirname = path.dirname(__filename);
1709
+ var HEARTBEAT_INTERVAL_MS2 = 3e4;
1710
+ var MAX_CONCURRENT = 3;
1711
+ var STOP_TIMEOUT_MS = 3e4;
1712
+ var ProjectRunner = class {
1713
+ connection;
1714
+ projectDir;
1715
+ activeAgents = /* @__PURE__ */ new Map();
1716
+ heartbeatTimer = null;
1717
+ stopping = false;
1718
+ resolveLifecycle = null;
1719
+ constructor(config) {
1720
+ this.projectDir = config.projectDir;
1721
+ this.connection = new ProjectConnection({
1722
+ apiUrl: config.conveyorApiUrl,
1723
+ projectToken: config.projectToken,
1724
+ projectId: config.projectId
1725
+ });
1726
+ }
1727
+ async start() {
1728
+ await this.connection.connect();
1729
+ this.connection.onTaskAssignment((assignment) => {
1730
+ void this.handleAssignment(assignment);
1731
+ });
1732
+ this.connection.onStopTask((data) => {
1733
+ this.handleStopTask(data.taskId);
1734
+ });
1735
+ this.connection.onShutdown(() => {
1736
+ console.log("[project-runner] Received shutdown signal from server");
1737
+ void this.stop();
1738
+ });
1739
+ this.heartbeatTimer = setInterval(() => {
1740
+ this.connection.sendHeartbeat();
1741
+ }, HEARTBEAT_INTERVAL_MS2);
1742
+ console.log("[project-runner] Connected, waiting for task assignments...");
1743
+ await new Promise((resolve2) => {
1744
+ this.resolveLifecycle = resolve2;
1745
+ process.on("SIGTERM", () => void this.stop());
1746
+ process.on("SIGINT", () => void this.stop());
1747
+ });
1748
+ }
1749
+ async handleAssignment(assignment) {
1750
+ const { taskId, taskToken, apiUrl, mode, branch, devBranch } = assignment;
1751
+ const shortId = taskId.slice(0, 8);
1752
+ if (this.activeAgents.has(taskId)) {
1753
+ console.log(`[project-runner] Task ${shortId} already running, skipping`);
1754
+ return;
1755
+ }
1756
+ if (this.activeAgents.size >= MAX_CONCURRENT) {
1757
+ console.log(
1758
+ `[project-runner] Max concurrent agents (${MAX_CONCURRENT}) reached, rejecting task ${shortId}`
1759
+ );
1760
+ this.connection.emitTaskStopped(taskId, "max_concurrent_reached");
1761
+ return;
1762
+ }
1763
+ try {
1764
+ try {
1765
+ execSync4("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
1766
+ } catch {
1767
+ console.log(`[task:${shortId}] Warning: git fetch failed`);
1768
+ }
1769
+ const worktreePath = ensureWorktree(this.projectDir, taskId, devBranch);
1770
+ if (branch && branch !== devBranch) {
1771
+ try {
1772
+ execSync4(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
1773
+ } catch {
1774
+ try {
1775
+ execSync4(`git checkout -b ${branch}`, { cwd: worktreePath, stdio: "ignore" });
1776
+ } catch {
1777
+ console.log(`[task:${shortId}] Warning: could not checkout branch ${branch}`);
1778
+ }
1779
+ }
1780
+ }
1781
+ const cliPath = path.resolve(__dirname, "cli.js");
1782
+ const child = fork(cliPath, [], {
1783
+ env: {
1784
+ ...process.env,
1785
+ CONVEYOR_API_URL: apiUrl,
1786
+ CONVEYOR_TASK_TOKEN: taskToken,
1787
+ CONVEYOR_TASK_ID: taskId,
1788
+ CONVEYOR_MODE: mode,
1789
+ CONVEYOR_WORKSPACE: worktreePath,
1790
+ CONVEYOR_USE_WORKTREE: "false"
1791
+ },
1792
+ cwd: worktreePath,
1793
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
1794
+ });
1795
+ child.stdout?.on("data", (data) => {
1796
+ const lines = data.toString().trimEnd().split("\n");
1797
+ for (const line of lines) {
1798
+ console.log(`[task:${shortId}] ${line}`);
1799
+ }
1800
+ });
1801
+ child.stderr?.on("data", (data) => {
1802
+ const lines = data.toString().trimEnd().split("\n");
1803
+ for (const line of lines) {
1804
+ console.error(`[task:${shortId}] ${line}`);
1805
+ }
1806
+ });
1807
+ this.activeAgents.set(taskId, { process: child, worktreePath, mode });
1808
+ this.connection.emitTaskStarted(taskId);
1809
+ console.log(`[project-runner] Started task ${shortId} in ${mode} mode at ${worktreePath}`);
1810
+ child.on("exit", (code) => {
1811
+ this.activeAgents.delete(taskId);
1812
+ const reason = code === 0 ? "completed" : `exited with code ${code}`;
1813
+ this.connection.emitTaskStopped(taskId, reason);
1814
+ console.log(`[project-runner] Task ${shortId} ${reason}`);
1815
+ if (code === 0) {
1816
+ try {
1817
+ removeWorktree(this.projectDir, taskId);
1818
+ } catch {
1819
+ }
1820
+ }
1821
+ });
1822
+ } catch (error) {
1823
+ console.error(
1824
+ `[project-runner] Failed to start task ${shortId}:`,
1825
+ error instanceof Error ? error.message : error
1826
+ );
1827
+ this.connection.emitTaskStopped(
1828
+ taskId,
1829
+ `start_failed: ${error instanceof Error ? error.message : "Unknown"}`
1830
+ );
1831
+ }
1832
+ }
1833
+ handleStopTask(taskId) {
1834
+ const agent = this.activeAgents.get(taskId);
1835
+ if (!agent) return;
1836
+ const shortId = taskId.slice(0, 8);
1837
+ console.log(`[project-runner] Stopping task ${shortId}`);
1838
+ agent.process.kill("SIGTERM");
1839
+ const timer = setTimeout(() => {
1840
+ if (this.activeAgents.has(taskId)) {
1841
+ agent.process.kill("SIGKILL");
1842
+ }
1843
+ }, STOP_TIMEOUT_MS);
1844
+ agent.process.on("exit", () => {
1845
+ clearTimeout(timer);
1846
+ try {
1847
+ removeWorktree(this.projectDir, taskId);
1848
+ } catch {
1849
+ }
1850
+ });
1851
+ }
1852
+ async stop() {
1853
+ if (this.stopping) return;
1854
+ this.stopping = true;
1855
+ console.log("[project-runner] Shutting down...");
1856
+ if (this.heartbeatTimer) {
1857
+ clearInterval(this.heartbeatTimer);
1858
+ this.heartbeatTimer = null;
1859
+ }
1860
+ const stopPromises = [...this.activeAgents.keys()].map(
1861
+ (taskId) => new Promise((resolve2) => {
1862
+ const agent = this.activeAgents.get(taskId);
1863
+ if (!agent) {
1864
+ resolve2();
1865
+ return;
1866
+ }
1867
+ agent.process.on("exit", () => {
1868
+ resolve2();
1869
+ });
1870
+ this.handleStopTask(taskId);
1871
+ })
1872
+ );
1873
+ await Promise.race([
1874
+ Promise.all(stopPromises),
1875
+ new Promise((resolve2) => setTimeout(resolve2, 6e4))
1876
+ ]);
1877
+ this.connection.disconnect();
1878
+ console.log("[project-runner] Shutdown complete");
1879
+ if (this.resolveLifecycle) {
1880
+ this.resolveLifecycle();
1881
+ this.resolveLifecycle = null;
1882
+ }
1883
+ }
1884
+ };
1885
+
1481
1886
  export {
1482
1887
  ConveyorConnection,
1483
1888
  loadConveyorConfig,
@@ -1485,6 +1890,8 @@ export {
1485
1890
  runStartCommand,
1486
1891
  ensureWorktree,
1487
1892
  removeWorktree,
1488
- AgentRunner
1893
+ AgentRunner,
1894
+ ProjectConnection,
1895
+ ProjectRunner
1489
1896
  };
1490
- //# sourceMappingURL=chunk-SBAEYHPL.js.map
1897
+ //# sourceMappingURL=chunk-N6QGELGE.js.map