@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.
- package/dist/{chunk-WD6A5AYJ.js → chunk-KFCGF2SX.js} +488 -84
- package/dist/chunk-KFCGF2SX.js.map +1 -0
- package/dist/cli.js +78 -54
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +80 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-WD6A5AYJ.js.map +0 -1
|
@@ -15,7 +15,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
15
15
|
this.config = config;
|
|
16
16
|
}
|
|
17
17
|
connect() {
|
|
18
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
167
|
-
this.pendingQuestionResolvers.set(requestId,
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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
|
-
`
|
|
535
|
-
`Post a brief status update to the chat summarizing
|
|
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
|
|
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.
|
|
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
|
-
`-
|
|
600
|
-
`-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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(
|
|
828
|
-
outputTokens: Math.round(
|
|
829
|
-
totalTokens:
|
|
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.
|
|
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.
|
|
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((
|
|
944
|
-
setTimeout(() =>
|
|
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 :
|
|
1023
|
-
|
|
1024
|
-
|
|
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,
|
|
1166
|
+
prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
|
|
1036
1167
|
options: { ...options, resume: void 0 }
|
|
1037
1168
|
});
|
|
1038
|
-
return runWithRetry(freshQuery,
|
|
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((
|
|
1061
|
-
const timer = setTimeout(
|
|
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
|
-
|
|
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
|
|
1464
|
+
const resolve2 = this.inputResolver;
|
|
1331
1465
|
this.inputResolver = null;
|
|
1332
|
-
|
|
1466
|
+
resolve2(msg);
|
|
1333
1467
|
} else {
|
|
1334
1468
|
this.pendingMessages.push(msg);
|
|
1335
1469
|
}
|
|
1336
1470
|
}
|
|
1337
1471
|
waitForMessage() {
|
|
1338
|
-
return new Promise((
|
|
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
|
-
|
|
1477
|
+
resolve2(null);
|
|
1344
1478
|
}
|
|
1345
1479
|
}, 1e3);
|
|
1346
1480
|
this.inputResolver = (msg) => {
|
|
1347
1481
|
clearInterval(checkStopped);
|
|
1348
|
-
|
|
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
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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(
|
|
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
|
|
1425
|
-
|
|
1560
|
+
for (const plansDir of this.getPlanDirs()) {
|
|
1561
|
+
let files;
|
|
1426
1562
|
try {
|
|
1427
|
-
|
|
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
|
|
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-
|
|
1879
|
+
//# sourceMappingURL=chunk-KFCGF2SX.js.map
|