@rallycry/conveyor-agent 2.12.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(
@@ -596,8 +640,9 @@ Environment (ready, no setup required):`,
596
640
  `- Check the dev branch (e.g. run: git fetch && git checkout dev || git checkout main) to understand the current state of the codebase that agents will branch off of.`,
597
641
  `
598
642
  Workflow:`,
599
- `- When you have a complete plan, save it using the update_task tool and post a summary to chat.`,
600
- `- After saving the plan, end your turn. Do NOT attempt to execute the plan yourself.`,
643
+ `- You can draft and iterate on plans in .claude/plans/*.md \u2014 these files are automatically synced to the task.`,
644
+ `- You can also use update_task directly to save the plan to the task.`,
645
+ `- After saving the plan, post a summary to chat and end your turn. Do NOT attempt to execute the plan yourself.`,
601
646
  `- A separate task agent will handle execution after the team reviews and approves your plan.`
602
647
  ] : [
603
648
  `You are an AI agent working on a task for the "${context.title}" project.`,
@@ -616,7 +661,14 @@ IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the follow
616
661
  `- bun dev, npm start, or any dev server startup commands`,
617
662
  `- pwd, ls, echo, or exploratory shell commands to "check" the environment`,
618
663
  `Only run these if you encounter a specific error that requires it.`,
619
- `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.`
620
672
  ];
621
673
  if (setupLog.length > 0) {
622
674
  parts.push(
@@ -674,7 +726,7 @@ function buildCommonTools(connection, config) {
674
726
  );
675
727
  }
676
728
  },
677
- { annotations: { readOnly: true } }
729
+ { annotations: { readOnlyHint: true } }
678
730
  ),
679
731
  tool(
680
732
  "post_to_chat",
@@ -708,7 +760,7 @@ function buildCommonTools(connection, config) {
708
760
  return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
709
761
  }
710
762
  },
711
- { annotations: { readOnly: true } }
763
+ { annotations: { readOnlyHint: true } }
712
764
  )
713
765
  ];
714
766
  }
@@ -729,6 +781,74 @@ function buildPmTools(connection) {
729
781
  return textResult("Failed to update task.");
730
782
  }
731
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 } }
732
852
  )
733
853
  ];
734
854
  }
@@ -811,6 +931,7 @@ async function processAssistantEvent(event, host, turnToolCalls) {
811
931
  function handleResultEvent(event, host, context, startTime) {
812
932
  const resultEvent = event;
813
933
  let totalCostUsd = 0;
934
+ let deltaCost = 0;
814
935
  let retriable = false;
815
936
  if (resultEvent.subtype === "success") {
816
937
  totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
@@ -819,15 +940,18 @@ function handleResultEvent(event, host, context, startTime) {
819
940
  if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
820
941
  retriable = true;
821
942
  }
822
- host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
823
- if (totalCostUsd > 0 && context.agentId) {
824
- 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);
825
949
  host.connection.trackSpending({
826
950
  agentId: context.agentId,
827
- inputTokens: Math.round(estimatedTotalTokens * 0.7),
828
- outputTokens: Math.round(estimatedTotalTokens * 0.3),
829
- totalTokens: estimatedTotalTokens,
830
- totalCostUsd,
951
+ inputTokens: Math.round(estimatedDeltaTokens * 0.7),
952
+ outputTokens: Math.round(estimatedDeltaTokens * 0.3),
953
+ totalTokens: estimatedDeltaTokens,
954
+ totalCostUsd: deltaCost,
831
955
  onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
832
956
  });
833
957
  }
@@ -839,16 +963,16 @@ function handleResultEvent(event, host, context, startTime) {
839
963
  }
840
964
  host.connection.sendEvent({ type: "error", message: errorMsg });
841
965
  }
842
- return { totalCostUsd, retriable };
966
+ return { totalCostUsd, deltaCost, retriable };
843
967
  }
844
968
  async function emitResultEvent(event, host, context, startTime) {
845
969
  const result = handleResultEvent(event, host, context, startTime);
846
970
  const durationMs = Date.now() - startTime;
847
- if (result.totalCostUsd > 0 && context.agentId) {
971
+ if (result.deltaCost > 0 && context.agentId) {
848
972
  await host.callbacks.onEvent({
849
973
  type: "completed",
850
974
  summary: "Task completed.",
851
- costUsd: result.totalCostUsd,
975
+ costUsd: result.deltaCost,
852
976
  durationMs
853
977
  });
854
978
  } else {
@@ -884,6 +1008,9 @@ async function processEvents(events, context, host) {
884
1008
  if (sessionId && !sessionIdStored) {
885
1009
  sessionIdStored = true;
886
1010
  host.connection.storeSessionId(sessionId);
1011
+ if (sessionId !== context.claudeSessionId) {
1012
+ context._lastReportedCostUsd = 0;
1013
+ }
887
1014
  }
888
1015
  await host.callbacks.onEvent({
889
1016
  type: "thinking",
@@ -940,8 +1067,8 @@ function buildCanUseTool(host) {
940
1067
  input: JSON.stringify(input)
941
1068
  });
942
1069
  const answerPromise = host.connection.askUserQuestion(requestId, questions);
943
- const timeoutPromise = new Promise((resolve) => {
944
- setTimeout(() => resolve(null), QUESTION_TIMEOUT_MS);
1070
+ const timeoutPromise = new Promise((resolve2) => {
1071
+ setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
945
1072
  });
946
1073
  const answers = await Promise.race([answerPromise, timeoutPromise]);
947
1074
  host.connection.emitStatus("running");
@@ -1019,23 +1146,27 @@ ${followUpContent}` : followUpContent;
1019
1146
  async function runWithRetry(initialQuery, context, host, options) {
1020
1147
  for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
1021
1148
  if (host.isStopped()) return;
1022
- const agentQuery = attempt === 0 ? initialQuery : query({
1023
- prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1024
- options: { ...options, resume: void 0 }
1025
- });
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
+ })();
1026
1156
  try {
1027
1157
  const { retriable } = await processEvents(agentQuery, context, host);
1028
1158
  if (!retriable || host.isStopped()) return;
1029
1159
  } catch (error) {
1030
1160
  const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
1031
1161
  if (isStaleSession && context.claudeSessionId) {
1162
+ context.claudeSessionId = null;
1163
+ context._lastReportedCostUsd = 0;
1032
1164
  host.connection.storeSessionId("");
1033
- const freshCtx = { ...context, claudeSessionId: null };
1034
1165
  const freshQuery = query({
1035
- prompt: host.createInputStream(buildInitialPrompt(host.config.mode, freshCtx)),
1166
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1036
1167
  options: { ...options, resume: void 0 }
1037
1168
  });
1038
- return runWithRetry(freshQuery, freshCtx, host, options);
1169
+ return runWithRetry(freshQuery, context, host, options);
1039
1170
  }
1040
1171
  const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
1041
1172
  if (!isApiError) throw error;
@@ -1057,13 +1188,13 @@ async function runWithRetry(initialQuery, context, host, options) {
1057
1188
  });
1058
1189
  host.connection.emitStatus("waiting_for_input");
1059
1190
  await host.callbacks.onStatusChange("waiting_for_input");
1060
- await new Promise((resolve) => {
1061
- const timer = setTimeout(resolve, delayMs);
1191
+ await new Promise((resolve2) => {
1192
+ const timer = setTimeout(resolve2, delayMs);
1062
1193
  const checkStopped = setInterval(() => {
1063
1194
  if (host.isStopped()) {
1064
1195
  clearTimeout(timer);
1065
1196
  clearInterval(checkStopped);
1066
- resolve();
1197
+ resolve2();
1067
1198
  }
1068
1199
  }, 1e3);
1069
1200
  setTimeout(() => clearInterval(checkStopped), delayMs + 100);
@@ -1159,6 +1290,9 @@ var AgentRunner = class _AgentRunner {
1159
1290
  this.connection.disconnect();
1160
1291
  return;
1161
1292
  }
1293
+ if (this.taskContext.claudeSessionId && this.taskContext._existingSpendingTotal !== null) {
1294
+ this.taskContext._lastReportedCostUsd = this.taskContext._existingSpendingTotal;
1295
+ }
1162
1296
  if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
1163
1297
  const result = cleanDevcontainerFromGit(
1164
1298
  this.config.workspaceDir,
@@ -1327,25 +1461,25 @@ ${f.content}
1327
1461
  parent_tool_use_id: null
1328
1462
  };
1329
1463
  if (this.inputResolver) {
1330
- const resolve = this.inputResolver;
1464
+ const resolve2 = this.inputResolver;
1331
1465
  this.inputResolver = null;
1332
- resolve(msg);
1466
+ resolve2(msg);
1333
1467
  } else {
1334
1468
  this.pendingMessages.push(msg);
1335
1469
  }
1336
1470
  }
1337
1471
  waitForMessage() {
1338
- return new Promise((resolve) => {
1472
+ return new Promise((resolve2) => {
1339
1473
  const checkStopped = setInterval(() => {
1340
1474
  if (this.stopped) {
1341
1475
  clearInterval(checkStopped);
1342
1476
  this.inputResolver = null;
1343
- resolve(null);
1477
+ resolve2(null);
1344
1478
  }
1345
1479
  }, 1e3);
1346
1480
  this.inputResolver = (msg) => {
1347
1481
  clearInterval(checkStopped);
1348
- resolve(msg);
1482
+ resolve2(msg);
1349
1483
  };
1350
1484
  });
1351
1485
  }
@@ -1381,64 +1515,78 @@ ${f.content}
1381
1515
  yield msg;
1382
1516
  }
1383
1517
  }
1518
+ getPlanDirs() {
1519
+ return [
1520
+ join3(homedir(), ".claude", "plans"),
1521
+ join3(this.config.workspaceDir, ".claude", "plans")
1522
+ ];
1523
+ }
1384
1524
  /**
1385
1525
  * Snapshot current plan files so syncPlanFile can distinguish files created
1386
1526
  * by THIS session from ones created by a concurrent agent.
1387
1527
  */
1388
1528
  snapshotPlanFiles() {
1389
- const plansDir = join3(homedir(), ".claude", "plans");
1390
1529
  this.planFileSnapshot.clear();
1391
1530
  this.lockedPlanFile = null;
1392
- try {
1393
- for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
1394
- try {
1395
- const stat = statSync(join3(plansDir, file));
1396
- this.planFileSnapshot.set(file, stat.mtimeMs);
1397
- } catch {
1398
- continue;
1531
+ for (const plansDir of this.getPlanDirs()) {
1532
+ try {
1533
+ for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
1534
+ try {
1535
+ const fullPath = join3(plansDir, file);
1536
+ const stat = statSync(fullPath);
1537
+ this.planFileSnapshot.set(fullPath, stat.mtimeMs);
1538
+ } catch {
1539
+ continue;
1540
+ }
1399
1541
  }
1542
+ } catch {
1400
1543
  }
1401
- } catch {
1402
1544
  }
1403
1545
  }
1404
1546
  syncPlanFile() {
1405
- const plansDir = join3(homedir(), ".claude", "plans");
1406
1547
  if (this.lockedPlanFile) {
1407
- const fullPath = join3(plansDir, this.lockedPlanFile);
1408
1548
  try {
1409
- const content = readFileSync(fullPath, "utf-8").trim();
1549
+ const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
1410
1550
  if (content) {
1411
1551
  this.connection.updateTaskFields({ plan: content });
1552
+ const fileName = this.lockedPlanFile.split("/").pop();
1553
+ this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
1412
1554
  }
1413
1555
  } catch {
1414
1556
  }
1415
1557
  return;
1416
1558
  }
1417
- let files;
1418
- try {
1419
- files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1420
- } catch {
1421
- return;
1422
- }
1423
1559
  let newest = null;
1424
- for (const file of files) {
1425
- const fullPath = join3(plansDir, file);
1560
+ for (const plansDir of this.getPlanDirs()) {
1561
+ let files;
1426
1562
  try {
1427
- const stat = statSync(fullPath);
1428
- const prevMtime = this.planFileSnapshot.get(file);
1429
- const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
1430
- if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
1431
- newest = { path: fullPath, mtime: stat.mtimeMs };
1432
- }
1563
+ files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1433
1564
  } catch {
1434
1565
  continue;
1435
1566
  }
1567
+ for (const file of files) {
1568
+ const fullPath = join3(plansDir, file);
1569
+ try {
1570
+ const stat = statSync(fullPath);
1571
+ const prevMtime = this.planFileSnapshot.get(fullPath);
1572
+ const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
1573
+ if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
1574
+ newest = { path: fullPath, mtime: stat.mtimeMs };
1575
+ }
1576
+ } catch {
1577
+ continue;
1578
+ }
1579
+ }
1436
1580
  }
1437
1581
  if (newest) {
1438
- this.lockedPlanFile = newest.path.split("/").pop();
1582
+ this.lockedPlanFile = newest.path;
1439
1583
  const content = readFileSync(newest.path, "utf-8").trim();
1440
1584
  if (content) {
1441
1585
  this.connection.updateTaskFields({ plan: content });
1586
+ const fileName = newest.path.split("/").pop();
1587
+ this.connection.postChatMessage(
1588
+ `Detected local plan file (${fileName}) and synced it to the task plan.`
1589
+ );
1442
1590
  }
1443
1591
  }
1444
1592
  }
@@ -1463,6 +1611,260 @@ ${f.content}
1463
1611
  }
1464
1612
  };
1465
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
+
1466
1868
  export {
1467
1869
  ConveyorConnection,
1468
1870
  loadConveyorConfig,
@@ -1470,6 +1872,8 @@ export {
1470
1872
  runStartCommand,
1471
1873
  ensureWorktree,
1472
1874
  removeWorktree,
1473
- AgentRunner
1875
+ AgentRunner,
1876
+ ProjectConnection,
1877
+ ProjectRunner
1474
1878
  };
1475
- //# sourceMappingURL=chunk-WD6A5AYJ.js.map
1879
+ //# sourceMappingURL=chunk-KFCGF2SX.js.map