@rallycry/conveyor-agent 2.13.0 → 2.14.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.
@@ -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
  }
@@ -385,6 +424,7 @@ import { randomUUID } from "crypto";
385
424
  import { query } from "@anthropic-ai/claude-agent-sdk";
386
425
 
387
426
  // src/prompt-builder.ts
427
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
388
428
  function findLastAgentMessageIndex(history) {
389
429
  for (let i = history.length - 1; i >= 0; i--) {
390
430
  if (history[i].role === "assistant") return i;
@@ -394,7 +434,7 @@ function findLastAgentMessageIndex(history) {
394
434
  function detectRelaunchScenario(context) {
395
435
  const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
396
436
  if (lastAgentIdx === -1) return "fresh";
397
- const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId;
437
+ const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId || ACTIVE_STATUSES.has(context.status ?? "");
398
438
  if (!hasPriorWork) return "fresh";
399
439
  const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
400
440
  const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
@@ -424,12 +464,13 @@ You are the project manager for this task.`,
424
464
  const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
425
465
  parts.push(
426
466
  `You have been relaunched with new feedback.`,
427
- `Work on the git branch "${context.githubBranch}".`,
467
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
428
468
  `
429
469
  New messages since your last run:`,
430
470
  ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
431
471
  `
432
- Address the requested changes. Commit and push your updates.`
472
+ 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.`,
473
+ `Commit and push your updates.`
433
474
  );
434
475
  if (context.githubPRUrl) {
435
476
  parts.push(
@@ -443,7 +484,8 @@ Address the requested changes. Commit and push your updates.`
443
484
  } else {
444
485
  parts.push(
445
486
  `You were relaunched but no new instructions have been given since your last run.`,
446
- `Work on the git branch "${context.githubBranch}".`,
487
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
488
+ `Run \`git log --oneline -10\` to review what you already committed.`,
447
489
  `Review the current state of the codebase and verify everything is working correctly.`,
448
490
  `Post a brief status update to the chat, then wait for further instructions.`
449
491
  );
@@ -515,7 +557,7 @@ function buildInstructions(mode, context, scenario) {
515
557
  parts.push(
516
558
  `Begin executing the task plan above immediately.`,
517
559
  `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}".`,
560
+ `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
519
561
  `Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
520
562
  `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
563
  );
@@ -530,9 +572,9 @@ function buildInstructions(mode, context, scenario) {
530
572
  } else {
531
573
  parts.push(
532
574
  `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.`,
575
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
576
+ `Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
577
+ `Post a brief status update to the chat summarizing where things stand.`,
536
578
  `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
537
579
  );
538
580
  if (context.githubPRUrl) {
@@ -554,13 +596,15 @@ Review these messages and wait for the team to provide instructions before takin
554
596
  );
555
597
  } else {
556
598
  parts.push(
557
- `You were relaunched with new feedback since your last run.`,
558
- `Work on the git branch "${context.githubBranch}".`,
599
+ `You have been relaunched to address feedback on your previous work.`,
600
+ `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
601
+ `Start by running \`git log --oneline -10\` and \`git diff HEAD~3 HEAD --stat\` to review what you already committed.`,
559
602
  `
560
603
  New messages since your last run:`,
561
604
  ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
562
605
  `
563
- Address the requested changes. Commit and push your updates.`
606
+ 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.`,
607
+ `Commit and push your updates.`
564
608
  );
565
609
  if (context.githubPRUrl) {
566
610
  parts.push(
@@ -617,7 +661,14 @@ IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the follow
617
661
  `- bun dev, npm start, or any dev server startup commands`,
618
662
  `- pwd, ls, echo, or exploratory shell commands to "check" the environment`,
619
663
  `Only run these if you encounter a specific error that requires it.`,
620
- `Start reading the task plan and writing code immediately.`
664
+ `Start reading the task plan and writing code immediately.`,
665
+ `
666
+ Git safety \u2014 STRICT rules:`,
667
+ `- NEVER run \`git checkout main\`, \`git checkout dev\`, or switch to any branch other than \`${context.githubBranch}\`.`,
668
+ `- NEVER create new branches (no \`git checkout -b\`, \`git switch -c\`, etc.).`,
669
+ `- This branch was created from \`${context.baseBranch}\`. PRs will automatically target that branch.`,
670
+ `- 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}\`.`,
671
+ `- If you encounter merge conflicts during rebase, resolve them in place \u2014 do NOT abandon the branch.`
621
672
  ];
622
673
  if (setupLog.length > 0) {
623
674
  parts.push(
@@ -675,7 +726,7 @@ function buildCommonTools(connection, config) {
675
726
  );
676
727
  }
677
728
  },
678
- { annotations: { readOnly: true } }
729
+ { annotations: { readOnlyHint: true } }
679
730
  ),
680
731
  tool(
681
732
  "post_to_chat",
@@ -709,7 +760,7 @@ function buildCommonTools(connection, config) {
709
760
  return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
710
761
  }
711
762
  },
712
- { annotations: { readOnly: true } }
763
+ { annotations: { readOnlyHint: true } }
713
764
  )
714
765
  ];
715
766
  }
@@ -730,6 +781,74 @@ function buildPmTools(connection) {
730
781
  return textResult("Failed to update task.");
731
782
  }
732
783
  }
784
+ ),
785
+ tool(
786
+ "create_subtask",
787
+ "Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
788
+ {
789
+ title: z.string().describe("Subtask title"),
790
+ description: z.string().optional().describe("Brief description"),
791
+ plan: z.string().optional().describe("Implementation plan in markdown"),
792
+ ordinal: z.number().optional().describe("Step/order number (0-based)"),
793
+ storyPointValue: z.number().optional().describe("Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)")
794
+ },
795
+ async (params) => {
796
+ try {
797
+ const result = await connection.createSubtask(params);
798
+ return textResult(`Subtask created with ID: ${result.id}`);
799
+ } catch (error) {
800
+ return textResult(
801
+ `Failed to create subtask: ${error instanceof Error ? error.message : "Unknown error"}`
802
+ );
803
+ }
804
+ }
805
+ ),
806
+ tool(
807
+ "update_subtask",
808
+ "Update an existing subtask's fields",
809
+ {
810
+ subtaskId: z.string().describe("The subtask ID to update"),
811
+ title: z.string().optional(),
812
+ description: z.string().optional(),
813
+ plan: z.string().optional(),
814
+ ordinal: z.number().optional(),
815
+ storyPointValue: z.number().optional()
816
+ },
817
+ async ({ subtaskId, ...fields }) => {
818
+ try {
819
+ await Promise.resolve(connection.updateSubtask(subtaskId, fields));
820
+ return textResult("Subtask updated.");
821
+ } catch (error) {
822
+ return textResult(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
823
+ }
824
+ }
825
+ ),
826
+ tool(
827
+ "delete_subtask",
828
+ "Delete a subtask",
829
+ { subtaskId: z.string().describe("The subtask ID to delete") },
830
+ async ({ subtaskId }) => {
831
+ try {
832
+ await Promise.resolve(connection.deleteSubtask(subtaskId));
833
+ return textResult("Subtask deleted.");
834
+ } catch (error) {
835
+ return textResult(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
836
+ }
837
+ }
838
+ ),
839
+ tool(
840
+ "list_subtasks",
841
+ "List all subtasks under the current parent task",
842
+ {},
843
+ async () => {
844
+ try {
845
+ const subtasks = await connection.listSubtasks();
846
+ return textResult(JSON.stringify(subtasks, null, 2));
847
+ } catch {
848
+ return textResult("Failed to list subtasks.");
849
+ }
850
+ },
851
+ { annotations: { readOnlyHint: true } }
733
852
  )
734
853
  ];
735
854
  }
@@ -812,6 +931,7 @@ async function processAssistantEvent(event, host, turnToolCalls) {
812
931
  function handleResultEvent(event, host, context, startTime) {
813
932
  const resultEvent = event;
814
933
  let totalCostUsd = 0;
934
+ let deltaCost = 0;
815
935
  let retriable = false;
816
936
  if (resultEvent.subtype === "success") {
817
937
  totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
@@ -820,15 +940,18 @@ function handleResultEvent(event, host, context, startTime) {
820
940
  if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
821
941
  retriable = true;
822
942
  }
823
- host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
824
- if (totalCostUsd > 0 && context.agentId) {
825
- const estimatedTotalTokens = Math.round(totalCostUsd * 1e5);
943
+ const lastCost = context._lastReportedCostUsd ?? 0;
944
+ deltaCost = totalCostUsd - lastCost;
945
+ context._lastReportedCostUsd = totalCostUsd;
946
+ host.connection.sendEvent({ type: "completed", summary, costUsd: deltaCost, durationMs });
947
+ if (deltaCost > 0 && context.agentId) {
948
+ const estimatedDeltaTokens = Math.round(deltaCost * 1e5);
826
949
  host.connection.trackSpending({
827
950
  agentId: context.agentId,
828
- inputTokens: Math.round(estimatedTotalTokens * 0.7),
829
- outputTokens: Math.round(estimatedTotalTokens * 0.3),
830
- totalTokens: estimatedTotalTokens,
831
- totalCostUsd,
951
+ inputTokens: Math.round(estimatedDeltaTokens * 0.7),
952
+ outputTokens: Math.round(estimatedDeltaTokens * 0.3),
953
+ totalTokens: estimatedDeltaTokens,
954
+ totalCostUsd: deltaCost,
832
955
  onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
833
956
  });
834
957
  }
@@ -840,16 +963,16 @@ function handleResultEvent(event, host, context, startTime) {
840
963
  }
841
964
  host.connection.sendEvent({ type: "error", message: errorMsg });
842
965
  }
843
- return { totalCostUsd, retriable };
966
+ return { totalCostUsd, deltaCost, retriable };
844
967
  }
845
968
  async function emitResultEvent(event, host, context, startTime) {
846
969
  const result = handleResultEvent(event, host, context, startTime);
847
970
  const durationMs = Date.now() - startTime;
848
- if (result.totalCostUsd > 0 && context.agentId) {
971
+ if (result.deltaCost > 0 && context.agentId) {
849
972
  await host.callbacks.onEvent({
850
973
  type: "completed",
851
974
  summary: "Task completed.",
852
- costUsd: result.totalCostUsd,
975
+ costUsd: result.deltaCost,
853
976
  durationMs
854
977
  });
855
978
  } else {
@@ -885,6 +1008,9 @@ async function processEvents(events, context, host) {
885
1008
  if (sessionId && !sessionIdStored) {
886
1009
  sessionIdStored = true;
887
1010
  host.connection.storeSessionId(sessionId);
1011
+ if (sessionId !== context.claudeSessionId) {
1012
+ context._lastReportedCostUsd = 0;
1013
+ }
888
1014
  }
889
1015
  await host.callbacks.onEvent({
890
1016
  type: "thinking",
@@ -941,8 +1067,8 @@ function buildCanUseTool(host) {
941
1067
  input: JSON.stringify(input)
942
1068
  });
943
1069
  const answerPromise = host.connection.askUserQuestion(requestId, questions);
944
- const timeoutPromise = new Promise((resolve) => {
945
- setTimeout(() => resolve(null), QUESTION_TIMEOUT_MS);
1070
+ const timeoutPromise = new Promise((resolve2) => {
1071
+ setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
946
1072
  });
947
1073
  const answers = await Promise.race([answerPromise, timeoutPromise]);
948
1074
  host.connection.emitStatus("running");
@@ -1020,23 +1146,27 @@ ${followUpContent}` : followUpContent;
1020
1146
  async function runWithRetry(initialQuery, context, host, options) {
1021
1147
  for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
1022
1148
  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
- });
1149
+ const agentQuery = attempt === 0 ? initialQuery : (() => {
1150
+ context._lastReportedCostUsd = 0;
1151
+ return query({
1152
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1153
+ options: { ...options, resume: void 0 }
1154
+ });
1155
+ })();
1027
1156
  try {
1028
1157
  const { retriable } = await processEvents(agentQuery, context, host);
1029
1158
  if (!retriable || host.isStopped()) return;
1030
1159
  } catch (error) {
1031
1160
  const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
1032
1161
  if (isStaleSession && context.claudeSessionId) {
1162
+ context.claudeSessionId = null;
1163
+ context._lastReportedCostUsd = 0;
1033
1164
  host.connection.storeSessionId("");
1034
- const freshCtx = { ...context, claudeSessionId: null };
1035
1165
  const freshQuery = query({
1036
- prompt: host.createInputStream(buildInitialPrompt(host.config.mode, freshCtx)),
1166
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1037
1167
  options: { ...options, resume: void 0 }
1038
1168
  });
1039
- return runWithRetry(freshQuery, freshCtx, host, options);
1169
+ return runWithRetry(freshQuery, context, host, options);
1040
1170
  }
1041
1171
  const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
1042
1172
  if (!isApiError) throw error;
@@ -1058,13 +1188,13 @@ async function runWithRetry(initialQuery, context, host, options) {
1058
1188
  });
1059
1189
  host.connection.emitStatus("waiting_for_input");
1060
1190
  await host.callbacks.onStatusChange("waiting_for_input");
1061
- await new Promise((resolve) => {
1062
- const timer = setTimeout(resolve, delayMs);
1191
+ await new Promise((resolve2) => {
1192
+ const timer = setTimeout(resolve2, delayMs);
1063
1193
  const checkStopped = setInterval(() => {
1064
1194
  if (host.isStopped()) {
1065
1195
  clearTimeout(timer);
1066
1196
  clearInterval(checkStopped);
1067
- resolve();
1197
+ resolve2();
1068
1198
  }
1069
1199
  }, 1e3);
1070
1200
  setTimeout(() => clearInterval(checkStopped), delayMs + 100);
@@ -1160,6 +1290,9 @@ var AgentRunner = class _AgentRunner {
1160
1290
  this.connection.disconnect();
1161
1291
  return;
1162
1292
  }
1293
+ if (this.taskContext.claudeSessionId && this.taskContext._existingSpendingTotal !== null) {
1294
+ this.taskContext._lastReportedCostUsd = this.taskContext._existingSpendingTotal;
1295
+ }
1163
1296
  if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
1164
1297
  const result = cleanDevcontainerFromGit(
1165
1298
  this.config.workspaceDir,
@@ -1328,25 +1461,25 @@ ${f.content}
1328
1461
  parent_tool_use_id: null
1329
1462
  };
1330
1463
  if (this.inputResolver) {
1331
- const resolve = this.inputResolver;
1464
+ const resolve2 = this.inputResolver;
1332
1465
  this.inputResolver = null;
1333
- resolve(msg);
1466
+ resolve2(msg);
1334
1467
  } else {
1335
1468
  this.pendingMessages.push(msg);
1336
1469
  }
1337
1470
  }
1338
1471
  waitForMessage() {
1339
- return new Promise((resolve) => {
1472
+ return new Promise((resolve2) => {
1340
1473
  const checkStopped = setInterval(() => {
1341
1474
  if (this.stopped) {
1342
1475
  clearInterval(checkStopped);
1343
1476
  this.inputResolver = null;
1344
- resolve(null);
1477
+ resolve2(null);
1345
1478
  }
1346
1479
  }, 1e3);
1347
1480
  this.inputResolver = (msg) => {
1348
1481
  clearInterval(checkStopped);
1349
- resolve(msg);
1482
+ resolve2(msg);
1350
1483
  };
1351
1484
  });
1352
1485
  }
@@ -1478,6 +1611,260 @@ ${f.content}
1478
1611
  }
1479
1612
  };
1480
1613
 
1614
+ // src/project-connection.ts
1615
+ import { io as io2 } from "socket.io-client";
1616
+ var ProjectConnection = class {
1617
+ socket = null;
1618
+ config;
1619
+ taskAssignmentCallback = null;
1620
+ stopTaskCallback = null;
1621
+ constructor(config) {
1622
+ this.config = config;
1623
+ }
1624
+ connect() {
1625
+ return new Promise((resolve2, reject) => {
1626
+ let settled = false;
1627
+ let attempts = 0;
1628
+ const maxInitialAttempts = 30;
1629
+ this.socket = io2(this.config.apiUrl, {
1630
+ auth: { projectToken: this.config.projectToken },
1631
+ transports: ["websocket"],
1632
+ reconnection: true,
1633
+ reconnectionAttempts: Infinity,
1634
+ reconnectionDelay: 2e3,
1635
+ reconnectionDelayMax: 3e4,
1636
+ randomizationFactor: 0.3,
1637
+ extraHeaders: {
1638
+ "ngrok-skip-browser-warning": "true"
1639
+ }
1640
+ });
1641
+ this.socket.on("projectRunner:assignTask", (data) => {
1642
+ if (this.taskAssignmentCallback) {
1643
+ this.taskAssignmentCallback(data);
1644
+ }
1645
+ });
1646
+ this.socket.on("projectRunner:stopTask", (data) => {
1647
+ if (this.stopTaskCallback) {
1648
+ this.stopTaskCallback(data);
1649
+ }
1650
+ });
1651
+ this.socket.on("connect", () => {
1652
+ if (!settled) {
1653
+ settled = true;
1654
+ resolve2();
1655
+ }
1656
+ });
1657
+ this.socket.io.on("reconnect_attempt", () => {
1658
+ attempts++;
1659
+ if (!settled && attempts >= maxInitialAttempts) {
1660
+ settled = true;
1661
+ reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
1662
+ }
1663
+ });
1664
+ });
1665
+ }
1666
+ onTaskAssignment(callback) {
1667
+ this.taskAssignmentCallback = callback;
1668
+ }
1669
+ onStopTask(callback) {
1670
+ this.stopTaskCallback = callback;
1671
+ }
1672
+ sendHeartbeat() {
1673
+ if (!this.socket) return;
1674
+ this.socket.emit("projectRunner:heartbeat", {});
1675
+ }
1676
+ emitTaskStarted(taskId) {
1677
+ if (!this.socket) return;
1678
+ this.socket.emit("projectRunner:taskStarted", { taskId });
1679
+ }
1680
+ emitTaskStopped(taskId, reason) {
1681
+ if (!this.socket) return;
1682
+ this.socket.emit("projectRunner:taskStopped", { taskId, reason });
1683
+ }
1684
+ disconnect() {
1685
+ this.socket?.disconnect();
1686
+ this.socket = null;
1687
+ }
1688
+ };
1689
+
1690
+ // src/project-runner.ts
1691
+ import { fork } from "child_process";
1692
+ import { execSync as execSync4 } from "child_process";
1693
+ import * as path from "path";
1694
+ import { fileURLToPath } from "url";
1695
+ var __filename = fileURLToPath(import.meta.url);
1696
+ var __dirname = path.dirname(__filename);
1697
+ var HEARTBEAT_INTERVAL_MS2 = 3e4;
1698
+ var MAX_CONCURRENT = 3;
1699
+ var STOP_TIMEOUT_MS = 3e4;
1700
+ var ProjectRunner = class {
1701
+ connection;
1702
+ projectDir;
1703
+ activeAgents = /* @__PURE__ */ new Map();
1704
+ heartbeatTimer = null;
1705
+ stopping = false;
1706
+ constructor(config) {
1707
+ this.projectDir = config.projectDir;
1708
+ this.connection = new ProjectConnection({
1709
+ apiUrl: config.conveyorApiUrl,
1710
+ projectToken: config.projectToken,
1711
+ projectId: config.projectId
1712
+ });
1713
+ }
1714
+ async start() {
1715
+ await this.connection.connect();
1716
+ this.connection.onTaskAssignment((assignment) => {
1717
+ void this.handleAssignment(assignment);
1718
+ });
1719
+ this.connection.onStopTask((data) => {
1720
+ this.handleStopTask(data.taskId);
1721
+ });
1722
+ this.heartbeatTimer = setInterval(() => {
1723
+ this.connection.sendHeartbeat();
1724
+ }, HEARTBEAT_INTERVAL_MS2);
1725
+ console.log("[project-runner] Connected, waiting for task assignments...");
1726
+ await new Promise((resolve2) => {
1727
+ process.on("SIGTERM", () => {
1728
+ void this.stop().then(resolve2);
1729
+ });
1730
+ process.on("SIGINT", () => {
1731
+ void this.stop().then(resolve2);
1732
+ });
1733
+ });
1734
+ }
1735
+ async handleAssignment(assignment) {
1736
+ const { taskId, taskToken, apiUrl, mode, branch, devBranch } = assignment;
1737
+ const shortId = taskId.slice(0, 8);
1738
+ if (this.activeAgents.has(taskId)) {
1739
+ console.log(`[project-runner] Task ${shortId} already running, skipping`);
1740
+ return;
1741
+ }
1742
+ if (this.activeAgents.size >= MAX_CONCURRENT) {
1743
+ console.log(
1744
+ `[project-runner] Max concurrent agents (${MAX_CONCURRENT}) reached, rejecting task ${shortId}`
1745
+ );
1746
+ this.connection.emitTaskStopped(taskId, "max_concurrent_reached");
1747
+ return;
1748
+ }
1749
+ try {
1750
+ try {
1751
+ execSync4("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
1752
+ } catch {
1753
+ console.log(`[task:${shortId}] Warning: git fetch failed`);
1754
+ }
1755
+ const worktreePath = ensureWorktree(this.projectDir, taskId, devBranch);
1756
+ if (branch && branch !== devBranch) {
1757
+ try {
1758
+ execSync4(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
1759
+ } catch {
1760
+ try {
1761
+ execSync4(`git checkout -b ${branch}`, { cwd: worktreePath, stdio: "ignore" });
1762
+ } catch {
1763
+ console.log(`[task:${shortId}] Warning: could not checkout branch ${branch}`);
1764
+ }
1765
+ }
1766
+ }
1767
+ const cliPath = path.resolve(__dirname, "cli.js");
1768
+ const child = fork(cliPath, [], {
1769
+ env: {
1770
+ ...process.env,
1771
+ CONVEYOR_API_URL: apiUrl,
1772
+ CONVEYOR_TASK_TOKEN: taskToken,
1773
+ CONVEYOR_TASK_ID: taskId,
1774
+ CONVEYOR_MODE: mode,
1775
+ CONVEYOR_WORKSPACE: worktreePath,
1776
+ CONVEYOR_USE_WORKTREE: "false"
1777
+ },
1778
+ cwd: worktreePath,
1779
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
1780
+ });
1781
+ child.stdout?.on("data", (data) => {
1782
+ const lines = data.toString().trimEnd().split("\n");
1783
+ for (const line of lines) {
1784
+ console.log(`[task:${shortId}] ${line}`);
1785
+ }
1786
+ });
1787
+ child.stderr?.on("data", (data) => {
1788
+ const lines = data.toString().trimEnd().split("\n");
1789
+ for (const line of lines) {
1790
+ console.error(`[task:${shortId}] ${line}`);
1791
+ }
1792
+ });
1793
+ this.activeAgents.set(taskId, { process: child, worktreePath, mode });
1794
+ this.connection.emitTaskStarted(taskId);
1795
+ console.log(`[project-runner] Started task ${shortId} in ${mode} mode at ${worktreePath}`);
1796
+ child.on("exit", (code) => {
1797
+ this.activeAgents.delete(taskId);
1798
+ const reason = code === 0 ? "completed" : `exited with code ${code}`;
1799
+ this.connection.emitTaskStopped(taskId, reason);
1800
+ console.log(`[project-runner] Task ${shortId} ${reason}`);
1801
+ if (code === 0) {
1802
+ try {
1803
+ removeWorktree(this.projectDir, taskId);
1804
+ } catch {
1805
+ }
1806
+ }
1807
+ });
1808
+ } catch (error) {
1809
+ console.error(
1810
+ `[project-runner] Failed to start task ${shortId}:`,
1811
+ error instanceof Error ? error.message : error
1812
+ );
1813
+ this.connection.emitTaskStopped(
1814
+ taskId,
1815
+ `start_failed: ${error instanceof Error ? error.message : "Unknown"}`
1816
+ );
1817
+ }
1818
+ }
1819
+ handleStopTask(taskId) {
1820
+ const agent = this.activeAgents.get(taskId);
1821
+ if (!agent) return;
1822
+ const shortId = taskId.slice(0, 8);
1823
+ console.log(`[project-runner] Stopping task ${shortId}`);
1824
+ agent.process.kill("SIGTERM");
1825
+ const timer = setTimeout(() => {
1826
+ if (this.activeAgents.has(taskId)) {
1827
+ agent.process.kill("SIGKILL");
1828
+ }
1829
+ }, STOP_TIMEOUT_MS);
1830
+ agent.process.on("exit", () => {
1831
+ clearTimeout(timer);
1832
+ try {
1833
+ removeWorktree(this.projectDir, taskId);
1834
+ } catch {
1835
+ }
1836
+ });
1837
+ }
1838
+ async stop() {
1839
+ if (this.stopping) return;
1840
+ this.stopping = true;
1841
+ console.log("[project-runner] Shutting down...");
1842
+ if (this.heartbeatTimer) {
1843
+ clearInterval(this.heartbeatTimer);
1844
+ this.heartbeatTimer = null;
1845
+ }
1846
+ const stopPromises = [...this.activeAgents.keys()].map(
1847
+ (taskId) => new Promise((resolve2) => {
1848
+ const agent = this.activeAgents.get(taskId);
1849
+ if (!agent) {
1850
+ resolve2();
1851
+ return;
1852
+ }
1853
+ agent.process.on("exit", () => {
1854
+ resolve2();
1855
+ });
1856
+ this.handleStopTask(taskId);
1857
+ })
1858
+ );
1859
+ await Promise.race([
1860
+ Promise.all(stopPromises),
1861
+ new Promise((resolve2) => setTimeout(resolve2, 6e4))
1862
+ ]);
1863
+ this.connection.disconnect();
1864
+ console.log("[project-runner] Shutdown complete");
1865
+ }
1866
+ };
1867
+
1481
1868
  export {
1482
1869
  ConveyorConnection,
1483
1870
  loadConveyorConfig,
@@ -1485,6 +1872,8 @@ export {
1485
1872
  runStartCommand,
1486
1873
  ensureWorktree,
1487
1874
  removeWorktree,
1488
- AgentRunner
1875
+ AgentRunner,
1876
+ ProjectConnection,
1877
+ ProjectRunner
1489
1878
  };
1490
- //# sourceMappingURL=chunk-SBAEYHPL.js.map
1879
+ //# sourceMappingURL=chunk-KFCGF2SX.js.map