@rallycry/conveyor-agent 2.7.0 → 2.9.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.
@@ -0,0 +1,1415 @@
1
+ // src/connection.ts
2
+ import { io } from "socket.io-client";
3
+ var ConveyorConnection = class _ConveyorConnection {
4
+ socket = null;
5
+ config;
6
+ eventBuffer = [];
7
+ flushTimer = null;
8
+ static EVENT_BATCH_MS = 500;
9
+ earlyMessages = [];
10
+ earlyStop = false;
11
+ chatMessageCallback = null;
12
+ stopCallback = null;
13
+ pendingQuestionResolvers = /* @__PURE__ */ new Map();
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+ connect() {
18
+ return new Promise((resolve, reject) => {
19
+ let settled = false;
20
+ let attempts = 0;
21
+ const maxInitialAttempts = 30;
22
+ this.socket = io(this.config.conveyorApiUrl, {
23
+ auth: { taskToken: this.config.taskToken, runnerMode: this.config.mode ?? "task" },
24
+ transports: ["websocket"],
25
+ reconnection: true,
26
+ reconnectionAttempts: Infinity,
27
+ reconnectionDelay: 2e3,
28
+ reconnectionDelayMax: 3e4,
29
+ randomizationFactor: 0.3,
30
+ extraHeaders: {
31
+ "ngrok-skip-browser-warning": "true"
32
+ }
33
+ });
34
+ this.socket.on("agentRunner:incomingMessage", (msg) => {
35
+ if (this.chatMessageCallback) {
36
+ this.chatMessageCallback(msg);
37
+ } else {
38
+ this.earlyMessages.push(msg);
39
+ }
40
+ });
41
+ this.socket.on("agentRunner:stop", () => {
42
+ if (this.stopCallback) {
43
+ this.stopCallback();
44
+ } else {
45
+ this.earlyStop = true;
46
+ }
47
+ });
48
+ this.socket.on(
49
+ "agentRunner:questionAnswer",
50
+ (data) => {
51
+ const resolver = this.pendingQuestionResolvers.get(data.requestId);
52
+ if (resolver) {
53
+ this.pendingQuestionResolvers.delete(data.requestId);
54
+ resolver(data.answers);
55
+ }
56
+ }
57
+ );
58
+ this.socket.on("connect", () => {
59
+ if (!settled) {
60
+ settled = true;
61
+ resolve();
62
+ }
63
+ });
64
+ this.socket.io.on("reconnect_attempt", () => {
65
+ attempts++;
66
+ if (!settled && attempts >= maxInitialAttempts) {
67
+ settled = true;
68
+ reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
69
+ }
70
+ });
71
+ });
72
+ }
73
+ fetchChatMessages(limit) {
74
+ const socket = this.socket;
75
+ if (!socket) throw new Error("Not connected");
76
+ return new Promise((resolve, reject) => {
77
+ socket.emit(
78
+ "agentRunner:getChatMessages",
79
+ { taskId: this.config.taskId, limit },
80
+ (response) => {
81
+ if (response.success && response.data) {
82
+ resolve(response.data);
83
+ } else {
84
+ reject(new Error(response.error ?? "Failed to fetch chat messages"));
85
+ }
86
+ }
87
+ );
88
+ });
89
+ }
90
+ fetchTaskContext() {
91
+ const socket = this.socket;
92
+ if (!socket) throw new Error("Not connected");
93
+ return new Promise((resolve, reject) => {
94
+ socket.emit(
95
+ "agentRunner:getTaskContext",
96
+ { taskId: this.config.taskId },
97
+ (response) => {
98
+ if (response.success && response.data) {
99
+ resolve(response.data);
100
+ } else {
101
+ reject(new Error(response.error ?? "Failed to fetch task context"));
102
+ }
103
+ }
104
+ );
105
+ });
106
+ }
107
+ sendEvent(event) {
108
+ if (!this.socket) throw new Error("Not connected");
109
+ this.eventBuffer.push({ taskId: this.config.taskId, event });
110
+ if (!this.flushTimer) {
111
+ this.flushTimer = setTimeout(() => this.flushEvents(), _ConveyorConnection.EVENT_BATCH_MS);
112
+ }
113
+ }
114
+ flushEvents() {
115
+ if (this.flushTimer) {
116
+ clearTimeout(this.flushTimer);
117
+ this.flushTimer = null;
118
+ }
119
+ if (!this.socket || this.eventBuffer.length === 0) return;
120
+ for (const entry of this.eventBuffer) {
121
+ this.socket.emit("agentRunner:event", entry);
122
+ }
123
+ this.eventBuffer = [];
124
+ }
125
+ updateStatus(status) {
126
+ if (!this.socket) throw new Error("Not connected");
127
+ this.socket.emit("agentRunner:statusUpdate", {
128
+ taskId: this.config.taskId,
129
+ status
130
+ });
131
+ }
132
+ postChatMessage(content) {
133
+ if (!this.socket) throw new Error("Not connected");
134
+ this.socket.emit("agentRunner:chatMessage", {
135
+ taskId: this.config.taskId,
136
+ content
137
+ });
138
+ }
139
+ createPR(params) {
140
+ const socket = this.socket;
141
+ if (!socket) throw new Error("Not connected");
142
+ return new Promise((resolve, reject) => {
143
+ socket.emit(
144
+ "agentRunner:createPR",
145
+ { taskId: this.config.taskId, ...params },
146
+ (response) => {
147
+ if (response.success && response.data) {
148
+ resolve(response.data);
149
+ } else {
150
+ reject(new Error(response.error ?? "Failed to create pull request"));
151
+ }
152
+ }
153
+ );
154
+ });
155
+ }
156
+ askUserQuestion(requestId, questions) {
157
+ if (!this.socket) throw new Error("Not connected");
158
+ this.socket.emit("agentRunner:askUserQuestion", {
159
+ taskId: this.config.taskId,
160
+ requestId,
161
+ questions
162
+ });
163
+ return new Promise((resolve) => {
164
+ this.pendingQuestionResolvers.set(requestId, resolve);
165
+ });
166
+ }
167
+ cancelPendingQuestions() {
168
+ this.pendingQuestionResolvers.clear();
169
+ }
170
+ storeSessionId(sessionId) {
171
+ if (!this.socket) return;
172
+ this.socket.emit("agentRunner:storeSessionId", {
173
+ taskId: this.config.taskId,
174
+ sessionId
175
+ });
176
+ }
177
+ updateTaskFields(fields) {
178
+ if (!this.socket) throw new Error("Not connected");
179
+ this.socket.emit("agentRunner:updateTaskFields", {
180
+ taskId: this.config.taskId,
181
+ fields
182
+ });
183
+ }
184
+ onChatMessage(callback) {
185
+ this.chatMessageCallback = callback;
186
+ for (const msg of this.earlyMessages) {
187
+ callback(msg);
188
+ }
189
+ this.earlyMessages = [];
190
+ }
191
+ onStopRequested(callback) {
192
+ this.stopCallback = callback;
193
+ if (this.earlyStop) {
194
+ callback();
195
+ this.earlyStop = false;
196
+ }
197
+ }
198
+ trackSpending(params) {
199
+ if (!this.socket) throw new Error("Not connected");
200
+ this.socket.emit("agentRunner:trackSpending", {
201
+ taskId: this.config.taskId,
202
+ ...params
203
+ });
204
+ }
205
+ emitStatus(status) {
206
+ if (!this.socket) return;
207
+ this.socket.emit("agentRunner:statusUpdate", {
208
+ taskId: this.config.taskId,
209
+ status
210
+ });
211
+ }
212
+ sendHeartbeat() {
213
+ if (!this.socket) return;
214
+ this.socket.emit("agentRunner:heartbeat", {
215
+ taskId: this.config.taskId
216
+ });
217
+ }
218
+ sendTypingStart() {
219
+ this.sendEvent({ type: "agent_typing_start" });
220
+ }
221
+ sendTypingStop() {
222
+ this.sendEvent({ type: "agent_typing_stop" });
223
+ }
224
+ disconnect() {
225
+ this.flushEvents();
226
+ this.socket?.disconnect();
227
+ this.socket = null;
228
+ }
229
+ };
230
+
231
+ // src/setup.ts
232
+ import { execSync } from "child_process";
233
+ import { spawn } from "child_process";
234
+ import { readFile } from "fs/promises";
235
+ import { join } from "path";
236
+ var CONVEYOR_CONFIG_PATH = ".conveyor/config.json";
237
+ var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
238
+ async function loadForwardPorts(workspaceDir) {
239
+ try {
240
+ const raw = await readFile(join(workspaceDir, DEVCONTAINER_PATH), "utf-8");
241
+ const parsed = JSON.parse(raw);
242
+ return parsed.forwardPorts ?? [];
243
+ } catch {
244
+ return [];
245
+ }
246
+ }
247
+ async function loadConveyorConfig(workspaceDir) {
248
+ try {
249
+ const raw = await readFile(join(workspaceDir, CONVEYOR_CONFIG_PATH), "utf-8");
250
+ const parsed = JSON.parse(raw);
251
+ if (parsed.setupCommand || parsed.startCommand) return parsed;
252
+ } catch {
253
+ }
254
+ try {
255
+ const raw = await readFile(join(workspaceDir, DEVCONTAINER_PATH), "utf-8");
256
+ const parsed = JSON.parse(raw);
257
+ if (parsed.conveyor && (parsed.conveyor.startCommand || parsed.conveyor.setupCommand)) {
258
+ return parsed.conveyor;
259
+ }
260
+ } catch {
261
+ }
262
+ return null;
263
+ }
264
+ function runSetupCommand(cmd, cwd, onOutput) {
265
+ return new Promise((resolve, reject) => {
266
+ const child = spawn("sh", ["-c", cmd], {
267
+ cwd,
268
+ stdio: ["ignore", "pipe", "pipe"],
269
+ env: { ...process.env }
270
+ });
271
+ child.stdout.on("data", (chunk) => {
272
+ onOutput("stdout", chunk.toString());
273
+ });
274
+ child.stderr.on("data", (chunk) => {
275
+ onOutput("stderr", chunk.toString());
276
+ });
277
+ child.on("close", (code) => {
278
+ if (code === 0) {
279
+ resolve();
280
+ } else {
281
+ reject(new Error(`Setup command exited with code ${code}`));
282
+ }
283
+ });
284
+ child.on("error", (err) => {
285
+ reject(err);
286
+ });
287
+ });
288
+ }
289
+ function runStartCommand(cmd, cwd, onOutput) {
290
+ const child = spawn("sh", ["-c", cmd], {
291
+ cwd,
292
+ stdio: ["ignore", "pipe", "pipe"],
293
+ detached: true,
294
+ env: { ...process.env }
295
+ });
296
+ child.stdout.on("data", (chunk) => {
297
+ onOutput("stdout", chunk.toString());
298
+ });
299
+ child.stderr.on("data", (chunk) => {
300
+ onOutput("stderr", chunk.toString());
301
+ });
302
+ child.unref();
303
+ return child;
304
+ }
305
+ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
306
+ const git = (cmd) => execSync(cmd, { cwd: workspaceDir, encoding: "utf-8", timeout: 3e4 }).trim();
307
+ try {
308
+ git(`git fetch origin ${baseBranch}`);
309
+ } catch {
310
+ return { cleaned: false, message: `Failed to fetch origin/${baseBranch}` };
311
+ }
312
+ try {
313
+ git(`git diff --quiet origin/${baseBranch} -- ${DEVCONTAINER_PATH}`);
314
+ return { cleaned: false, message: "devcontainer.json already matches base" };
315
+ } catch {
316
+ }
317
+ try {
318
+ const ahead = parseInt(git(`git rev-list --count origin/${baseBranch}..HEAD`), 10);
319
+ if (ahead <= 1) {
320
+ git(`git reset --hard origin/${baseBranch}`);
321
+ } else {
322
+ git(`git checkout origin/${baseBranch} -- ${DEVCONTAINER_PATH}`);
323
+ git(`git add ${DEVCONTAINER_PATH}`);
324
+ try {
325
+ git(`git diff --cached --quiet -- ${DEVCONTAINER_PATH}`);
326
+ return { cleaned: false, message: "devcontainer.json already clean in working tree" };
327
+ } catch {
328
+ git(`git commit -m "chore: reset devcontainer config"`);
329
+ }
330
+ }
331
+ git(`git push --force-with-lease origin ${taskBranch}`);
332
+ return { cleaned: true, message: "devcontainer.json cleaned from git history" };
333
+ } catch (err) {
334
+ const msg = err instanceof Error ? err.message : "Unknown error";
335
+ return { cleaned: false, message: `Git cleanup failed: ${msg}` };
336
+ }
337
+ }
338
+
339
+ // src/worktree.ts
340
+ import { execSync as execSync2 } from "child_process";
341
+ import { existsSync } from "fs";
342
+ import { join as join2 } from "path";
343
+ var WORKTREE_DIR = ".worktrees";
344
+ function ensureWorktree(projectDir, taskId, branch) {
345
+ const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
346
+ if (existsSync(worktreePath)) {
347
+ if (branch) {
348
+ try {
349
+ execSync2(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
350
+ } catch {
351
+ }
352
+ }
353
+ return worktreePath;
354
+ }
355
+ const target = branch ?? "HEAD";
356
+ execSync2(`git worktree add "${worktreePath}" ${target}`, {
357
+ cwd: projectDir,
358
+ stdio: "ignore"
359
+ });
360
+ return worktreePath;
361
+ }
362
+ function removeWorktree(projectDir, taskId) {
363
+ const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
364
+ if (!existsSync(worktreePath)) return;
365
+ try {
366
+ execSync2(`git worktree remove "${worktreePath}" --force`, {
367
+ cwd: projectDir,
368
+ stdio: "ignore"
369
+ });
370
+ } catch {
371
+ }
372
+ }
373
+
374
+ // src/runner.ts
375
+ import { execSync as execSync3 } from "child_process";
376
+ import { readdirSync, statSync, readFileSync } from "fs";
377
+ import { homedir } from "os";
378
+ import { join as join3 } from "path";
379
+
380
+ // src/query-executor.ts
381
+ import { randomUUID } from "crypto";
382
+ import { query } from "@anthropic-ai/claude-agent-sdk";
383
+
384
+ // src/prompt-builder.ts
385
+ function findLastAgentMessageIndex(history) {
386
+ for (let i = history.length - 1; i >= 0; i--) {
387
+ if (history[i].role === "assistant") return i;
388
+ }
389
+ return -1;
390
+ }
391
+ function detectRelaunchScenario(context) {
392
+ const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
393
+ if (lastAgentIdx === -1) return "fresh";
394
+ const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId;
395
+ if (!hasPriorWork) return "fresh";
396
+ const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
397
+ const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
398
+ return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
399
+ }
400
+ function buildRelaunchWithSession(mode, context) {
401
+ const scenario = detectRelaunchScenario(context);
402
+ if (!context.claudeSessionId || scenario === "fresh") return null;
403
+ const parts = [];
404
+ const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
405
+ if (mode === "pm") {
406
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
407
+ if (newMessages.length > 0) {
408
+ parts.push(
409
+ `You have been relaunched. Here are new messages since your last session:`,
410
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
411
+ );
412
+ } else {
413
+ parts.push(`You have been relaunched. No new messages since your last session.`);
414
+ }
415
+ parts.push(
416
+ `
417
+ You are the project manager for this task.`,
418
+ `Review the context above and wait for the team to provide instructions before taking action.`
419
+ );
420
+ } else if (scenario === "feedback_relaunch") {
421
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
422
+ parts.push(
423
+ `You have been relaunched with new feedback.`,
424
+ `Work on the git branch "${context.githubBranch}".`,
425
+ `
426
+ New messages since your last run:`,
427
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
428
+ `
429
+ Address the requested changes. Commit and push your updates.`
430
+ );
431
+ if (context.githubPRUrl) {
432
+ parts.push(
433
+ `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch. Do NOT create a new PR.`
434
+ );
435
+ } else {
436
+ parts.push(
437
+ `When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI.`
438
+ );
439
+ }
440
+ } else {
441
+ parts.push(
442
+ `You were relaunched but no new instructions have been given since your last run.`,
443
+ `Work on the git branch "${context.githubBranch}".`,
444
+ `Review the current state of the codebase and verify everything is working correctly.`,
445
+ `Post a brief status update to the chat, then wait for further instructions.`
446
+ );
447
+ if (context.githubPRUrl) {
448
+ parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
449
+ }
450
+ }
451
+ return parts.join("\n");
452
+ }
453
+ function buildTaskBody(context) {
454
+ const parts = [];
455
+ parts.push(`# Task: ${context.title}`);
456
+ if (context.description) {
457
+ parts.push(`
458
+ ## Description
459
+ ${context.description}`);
460
+ }
461
+ if (context.plan) {
462
+ parts.push(`
463
+ ## Plan
464
+ ${context.plan}`);
465
+ }
466
+ if (context.files && context.files.length > 0) {
467
+ parts.push(`
468
+ ## Attached Files`);
469
+ for (const file of context.files) {
470
+ parts.push(`- **${file.fileName}** (${file.mimeType}): ${file.downloadUrl}`);
471
+ }
472
+ }
473
+ if (context.repoRefs && context.repoRefs.length > 0) {
474
+ parts.push(`
475
+ ## Repository References`);
476
+ for (const ref of context.repoRefs) {
477
+ const icon = ref.refType === "folder" ? "folder" : "file";
478
+ parts.push(`- [${icon}] \`${ref.path}\``);
479
+ }
480
+ }
481
+ if (context.chatHistory.length > 0) {
482
+ const relevant = context.chatHistory.slice(-20);
483
+ parts.push(`
484
+ ## Recent Chat Context`);
485
+ for (const msg of relevant) {
486
+ const sender = msg.userName ?? msg.role;
487
+ parts.push(`[${sender}]: ${msg.content}`);
488
+ }
489
+ }
490
+ return parts;
491
+ }
492
+ function buildInstructions(mode, context, scenario) {
493
+ const parts = [`
494
+ ## Instructions`];
495
+ const isPm = mode === "pm";
496
+ if (scenario === "fresh") {
497
+ if (isPm) {
498
+ parts.push(
499
+ `You are the project manager for this task.`,
500
+ `The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`
501
+ );
502
+ } else {
503
+ parts.push(
504
+ `Begin executing the task plan above immediately.`,
505
+ `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.`,
506
+ `Work on the git branch "${context.githubBranch}".`,
507
+ `Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
508
+ `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.`
509
+ );
510
+ }
511
+ } else if (scenario === "idle_relaunch") {
512
+ if (isPm) {
513
+ parts.push(
514
+ `You were relaunched but no new instructions have been given since your last run.`,
515
+ `You are the project manager for this task.`,
516
+ `Wait for the team to provide instructions before taking action.`
517
+ );
518
+ } else {
519
+ parts.push(
520
+ `You were relaunched but no new instructions have been given since your last run.`,
521
+ `Work on the git branch "${context.githubBranch}".`,
522
+ `Review the current state of the codebase and verify everything is working correctly (e.g. tests pass, the web server starts on port 3000).`,
523
+ `Post a brief status update to the chat summarizing the current state.`,
524
+ `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
525
+ );
526
+ if (context.githubPRUrl) {
527
+ parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
528
+ }
529
+ }
530
+ } else {
531
+ const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
532
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
533
+ if (isPm) {
534
+ parts.push(
535
+ `You were relaunched with new feedback since your last run.`,
536
+ `You are the project manager for this task.`,
537
+ `
538
+ New messages since your last run:`,
539
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
540
+ `
541
+ Review these messages and wait for the team to provide instructions before taking action.`
542
+ );
543
+ } else {
544
+ parts.push(
545
+ `You were relaunched with new feedback since your last run.`,
546
+ `Work on the git branch "${context.githubBranch}".`,
547
+ `
548
+ New messages since your last run:`,
549
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
550
+ `
551
+ Address the requested changes. Commit and push your updates.`
552
+ );
553
+ if (context.githubPRUrl) {
554
+ parts.push(
555
+ `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch to update it. Do NOT create a new PR.`
556
+ );
557
+ } else {
558
+ parts.push(
559
+ `When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
560
+ );
561
+ }
562
+ }
563
+ }
564
+ return parts;
565
+ }
566
+ function buildInitialPrompt(mode, context) {
567
+ const sessionRelaunch = buildRelaunchWithSession(mode, context);
568
+ if (sessionRelaunch) return sessionRelaunch;
569
+ const scenario = detectRelaunchScenario(context);
570
+ const body = buildTaskBody(context);
571
+ const instructions = buildInstructions(mode, context, scenario);
572
+ return [...body, ...instructions].join("\n");
573
+ }
574
+ function buildSystemPrompt(mode, context, config, setupLog) {
575
+ const isPm = mode === "pm";
576
+ const parts = isPm ? [
577
+ `You are an AI project manager helping to plan tasks for the "${context.title}" project.`,
578
+ `You are running locally with full access to the repository.`,
579
+ `
580
+ Environment (ready, no setup required):`,
581
+ `- Repository is cloned at your current working directory.`,
582
+ `- You can read files to understand the codebase before writing task plans.`,
583
+ `- 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.`
584
+ ] : [
585
+ `You are an AI agent working on a task for the "${context.title}" project.`,
586
+ `You are running inside a GitHub Codespace with full access to the repository.`,
587
+ `
588
+ Environment (fully ready \u2014 do NOT verify or set up):`,
589
+ `- Repository is cloned at your current working directory.`,
590
+ `- Branch \`${context.githubBranch}\` is already checked out.`,
591
+ `- All dependencies are installed, database is migrated, and the dev server is running.`,
592
+ `- Git is configured. Commit and push directly to this branch.`,
593
+ `
594
+ IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the following:`,
595
+ `- bun/npm install, pip install, or any dependency installation`,
596
+ `- bun build, bun lint, bun test, bun typecheck, or any build/check commands as a "first step"`,
597
+ `- bun db:generate, bun db:push, prisma migrate, or any database setup`,
598
+ `- bun dev, npm start, or any dev server startup commands`,
599
+ `- pwd, ls, echo, or exploratory shell commands to "check" the environment`,
600
+ `Only run these if you encounter a specific error that requires it.`,
601
+ `Start reading the task plan and writing code immediately.`
602
+ ];
603
+ if (setupLog.length > 0) {
604
+ parts.push(
605
+ `
606
+ Environment setup log (already executed before you started \u2014 proof that setup succeeded):`,
607
+ "```",
608
+ ...setupLog,
609
+ "```"
610
+ );
611
+ }
612
+ if (context.agentInstructions) {
613
+ parts.push(`
614
+ Agent Instructions:
615
+ ${context.agentInstructions}`);
616
+ }
617
+ if (config.instructions) {
618
+ parts.push(`
619
+ Additional Instructions:
620
+ ${config.instructions}`);
621
+ }
622
+ parts.push(
623
+ `
624
+ You have access to Conveyor MCP tools to interact with the task management system.`,
625
+ `Use the post_to_chat tool to communicate progress or ask questions.`,
626
+ `Use the read_task_chat tool to check for new messages from the team.`
627
+ );
628
+ if (!isPm) {
629
+ parts.push(
630
+ `Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
631
+ );
632
+ }
633
+ return parts.join("\n");
634
+ }
635
+
636
+ // src/mcp-tools.ts
637
+ import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
638
+ import { z } from "zod";
639
+ function buildCommonTools(connection, config) {
640
+ return [
641
+ tool(
642
+ "read_task_chat",
643
+ "Read recent messages from the task chat to see team feedback or instructions",
644
+ {
645
+ limit: z.number().optional().describe("Number of recent messages to fetch (default 20)")
646
+ },
647
+ async ({ limit }) => {
648
+ try {
649
+ const messages = await connection.fetchChatMessages(limit);
650
+ return textResult(JSON.stringify(messages, null, 2));
651
+ } catch {
652
+ return textResult(
653
+ JSON.stringify({
654
+ note: "Could not fetch live chat. Chat history was provided in the initial context."
655
+ })
656
+ );
657
+ }
658
+ },
659
+ { annotations: { readOnly: true } }
660
+ ),
661
+ tool(
662
+ "post_to_chat",
663
+ "Post a message to the task chat visible to all team members",
664
+ { message: z.string().describe("The message to post to the team") },
665
+ ({ message }) => {
666
+ connection.postChatMessage(message);
667
+ return Promise.resolve(textResult("Message posted to task chat."));
668
+ }
669
+ ),
670
+ tool(
671
+ "update_task_status",
672
+ "Update the task status on the Kanban board",
673
+ {
674
+ status: z.enum(["InProgress", "ReviewPR", "Complete"]).describe("The new status for the task")
675
+ },
676
+ ({ status }) => {
677
+ connection.updateStatus(status);
678
+ return Promise.resolve(textResult(`Task status updated to ${status}.`));
679
+ }
680
+ ),
681
+ tool(
682
+ "get_task_plan",
683
+ "Re-read the latest task plan in case it was updated",
684
+ {},
685
+ async () => {
686
+ try {
687
+ const ctx = await connection.fetchTaskContext();
688
+ return textResult(ctx.plan ?? "No plan available.");
689
+ } catch {
690
+ return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
691
+ }
692
+ },
693
+ { annotations: { readOnly: true } }
694
+ )
695
+ ];
696
+ }
697
+ function buildPmTools(connection) {
698
+ return [
699
+ tool(
700
+ "update_task",
701
+ "Save the finalized task plan and/or description",
702
+ {
703
+ plan: z.string().optional().describe("The task plan in markdown"),
704
+ description: z.string().optional().describe("Updated task description")
705
+ },
706
+ async ({ plan, description }) => {
707
+ try {
708
+ await Promise.resolve(connection.updateTaskFields({ plan, description }));
709
+ return textResult("Task updated successfully.");
710
+ } catch {
711
+ return textResult("Failed to update task.");
712
+ }
713
+ }
714
+ )
715
+ ];
716
+ }
717
+ function buildTaskTools(connection) {
718
+ return [
719
+ tool(
720
+ "create_pull_request",
721
+ "Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
722
+ {
723
+ title: z.string().describe("The PR title"),
724
+ body: z.string().describe("The PR description/body in markdown")
725
+ },
726
+ async ({ title, body }) => {
727
+ try {
728
+ const result = await connection.createPR({ title, body });
729
+ connection.sendEvent({
730
+ type: "pr_created",
731
+ url: result.url,
732
+ number: result.number
733
+ });
734
+ return textResult(`Pull request #${result.number} created: ${result.url}`);
735
+ } catch (error) {
736
+ const msg = error instanceof Error ? error.message : "Unknown error";
737
+ return textResult(`Failed to create pull request: ${msg}`);
738
+ }
739
+ }
740
+ )
741
+ ];
742
+ }
743
+ function textResult(text) {
744
+ return { content: [{ type: "text", text }] };
745
+ }
746
+ function createConveyorMcpServer(connection, config) {
747
+ const commonTools = buildCommonTools(connection, config);
748
+ const modeTools = config.mode === "pm" ? buildPmTools(connection) : buildTaskTools(connection);
749
+ return createSdkMcpServer({
750
+ name: "conveyor",
751
+ tools: [...commonTools, ...modeTools]
752
+ });
753
+ }
754
+
755
+ // src/query-executor.ts
756
+ var API_ERROR_PATTERN = /API Error: [45]\d\d/;
757
+ var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
758
+ var PM_DENIED_TOOLS = /* @__PURE__ */ new Set([
759
+ "Write",
760
+ "Edit",
761
+ "MultiEdit",
762
+ "NotebookEdit",
763
+ "Bash",
764
+ "ExitPlanMode"
765
+ ]);
766
+ async function processAssistantEvent(event, host, turnToolCalls) {
767
+ const msg = event.message;
768
+ const content = msg.content;
769
+ const turnTextParts = [];
770
+ for (const block of content) {
771
+ const blockType = block.type;
772
+ if (blockType === "text") {
773
+ const text = block.text;
774
+ turnTextParts.push(text);
775
+ host.connection.sendEvent({ type: "message", content: text });
776
+ await host.callbacks.onEvent({ type: "message", content: text });
777
+ } else if (blockType === "tool_use") {
778
+ const name = block.name;
779
+ const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
780
+ const isContentTool = ["edit", "write"].includes(name.toLowerCase());
781
+ const inputLimit = isContentTool ? 1e4 : 500;
782
+ const summary = {
783
+ tool: name,
784
+ input: inputStr.slice(0, inputLimit),
785
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
786
+ };
787
+ turnToolCalls.push(summary);
788
+ host.connection.sendEvent({ type: "tool_use", tool: name, input: inputStr });
789
+ await host.callbacks.onEvent({ type: "tool_use", tool: name, input: inputStr });
790
+ }
791
+ }
792
+ if (turnTextParts.length > 0) {
793
+ host.connection.postChatMessage(turnTextParts.join("\n\n"));
794
+ }
795
+ if (turnToolCalls.length > 0) {
796
+ host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
797
+ turnToolCalls.length = 0;
798
+ }
799
+ if (host.config.mode === "pm") {
800
+ host.syncPlanFile();
801
+ }
802
+ }
803
+ function handleResultEvent(event, host, context, startTime) {
804
+ const resultEvent = event;
805
+ let totalCostUsd = 0;
806
+ let retriable = false;
807
+ if (resultEvent.subtype === "success") {
808
+ totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
809
+ const durationMs = Date.now() - startTime;
810
+ const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
811
+ if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
812
+ retriable = true;
813
+ }
814
+ host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
815
+ if (totalCostUsd > 0 && context.agentId) {
816
+ const estimatedTotalTokens = Math.round(totalCostUsd * 1e5);
817
+ host.connection.trackSpending({
818
+ agentId: context.agentId,
819
+ inputTokens: Math.round(estimatedTotalTokens * 0.7),
820
+ outputTokens: Math.round(estimatedTotalTokens * 0.3),
821
+ totalTokens: estimatedTotalTokens,
822
+ totalCostUsd,
823
+ onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
824
+ });
825
+ }
826
+ } else {
827
+ const errors = "errors" in resultEvent ? resultEvent.errors : [];
828
+ const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
829
+ if (API_ERROR_PATTERN.test(errorMsg)) {
830
+ retriable = true;
831
+ }
832
+ host.connection.sendEvent({ type: "error", message: errorMsg });
833
+ }
834
+ return { totalCostUsd, retriable };
835
+ }
836
+ async function emitResultEvent(event, host, context, startTime) {
837
+ const result = handleResultEvent(event, host, context, startTime);
838
+ const durationMs = Date.now() - startTime;
839
+ if (result.totalCostUsd > 0 && context.agentId) {
840
+ await host.callbacks.onEvent({
841
+ type: "completed",
842
+ summary: "Task completed.",
843
+ costUsd: result.totalCostUsd,
844
+ durationMs
845
+ });
846
+ } else {
847
+ const resultEvent = event;
848
+ if (resultEvent.subtype === "success") {
849
+ const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
850
+ await host.callbacks.onEvent({
851
+ type: "completed",
852
+ summary,
853
+ costUsd: 0,
854
+ durationMs
855
+ });
856
+ } else {
857
+ const errors = "errors" in resultEvent ? resultEvent.errors : [];
858
+ const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
859
+ await host.callbacks.onEvent({ type: "error", message: errorMsg });
860
+ }
861
+ }
862
+ return result.retriable;
863
+ }
864
+ async function processEvents(events, context, host) {
865
+ const startTime = Date.now();
866
+ let sessionIdStored = false;
867
+ let isTyping = false;
868
+ let retriable = false;
869
+ const turnToolCalls = [];
870
+ for await (const event of events) {
871
+ if (host.isStopped()) break;
872
+ switch (event.type) {
873
+ case "system": {
874
+ if (event.subtype === "init") {
875
+ const sessionId = event.session_id;
876
+ if (sessionId && !sessionIdStored) {
877
+ sessionIdStored = true;
878
+ host.connection.storeSessionId(sessionId);
879
+ }
880
+ await host.callbacks.onEvent({
881
+ type: "thinking",
882
+ message: `Agent initialized (model: ${event.model})`
883
+ });
884
+ }
885
+ break;
886
+ }
887
+ case "assistant": {
888
+ if (!isTyping) {
889
+ setTimeout(() => host.connection.sendTypingStart(), 200);
890
+ isTyping = true;
891
+ }
892
+ await processAssistantEvent(event, host, turnToolCalls);
893
+ break;
894
+ }
895
+ case "result": {
896
+ if (isTyping) {
897
+ host.connection.sendTypingStop();
898
+ isTyping = false;
899
+ }
900
+ retriable = await emitResultEvent(event, host, context, startTime);
901
+ break;
902
+ }
903
+ }
904
+ }
905
+ if (isTyping) {
906
+ host.connection.sendTypingStop();
907
+ }
908
+ return { retriable };
909
+ }
910
+ function buildCanUseTool(host) {
911
+ const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
912
+ return async (toolName, input) => {
913
+ if (host.config.mode === "pm" && PM_DENIED_TOOLS.has(toolName)) {
914
+ return {
915
+ behavior: "deny",
916
+ message: "PM mode is plan-only. Use the update_task tool to save your plan."
917
+ };
918
+ }
919
+ if (toolName !== "AskUserQuestion") {
920
+ return { behavior: "allow", updatedInput: input };
921
+ }
922
+ const questions = input.questions;
923
+ const requestId = randomUUID();
924
+ host.connection.emitStatus("waiting_for_input");
925
+ host.connection.sendEvent({
926
+ type: "tool_use",
927
+ tool: "AskUserQuestion",
928
+ input: JSON.stringify(input)
929
+ });
930
+ const answerPromise = host.connection.askUserQuestion(requestId, questions);
931
+ const timeoutPromise = new Promise((resolve) => {
932
+ setTimeout(() => resolve(null), QUESTION_TIMEOUT_MS);
933
+ });
934
+ const answers = await Promise.race([answerPromise, timeoutPromise]);
935
+ host.connection.emitStatus("running");
936
+ if (!answers) {
937
+ return {
938
+ behavior: "deny",
939
+ message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
940
+ };
941
+ }
942
+ return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
943
+ };
944
+ }
945
+ function buildQueryOptions(host, context) {
946
+ const settings = context.agentSettings ?? host.config.agentSettings ?? {};
947
+ const systemPromptText = buildSystemPrompt(host.config.mode, context, host.config, host.setupLog);
948
+ const conveyorMcp = createConveyorMcpServer(host.connection, host.config);
949
+ const isPm = host.config.mode === "pm";
950
+ const pmDisallowedTools = isPm ? ["TodoWrite", "TodoRead"] : [];
951
+ const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
952
+ const settingSources = settings.settingSources ?? ["user", "project"];
953
+ return {
954
+ model: context.model || host.config.model,
955
+ systemPrompt: {
956
+ type: "preset",
957
+ preset: "claude_code",
958
+ append: systemPromptText || void 0
959
+ },
960
+ settingSources,
961
+ cwd: host.config.workspaceDir,
962
+ permissionMode: isPm ? "plan" : "bypassPermissions",
963
+ allowDangerouslySkipPermissions: !isPm,
964
+ canUseTool: buildCanUseTool(host),
965
+ tools: { type: "preset", preset: "claude_code" },
966
+ mcpServers: { conveyor: conveyorMcp },
967
+ maxTurns: settings.maxTurns,
968
+ effort: settings.effort,
969
+ thinking: settings.thinking,
970
+ betas: settings.betas,
971
+ maxBudgetUsd: settings.maxBudgetUsd ?? 50,
972
+ disallowedTools: disallowedTools.length > 0 ? disallowedTools : void 0,
973
+ enableFileCheckpointing: settings.enableFileCheckpointing
974
+ };
975
+ }
976
+ async function runSdkQuery(host, context, followUpContent) {
977
+ if (host.isStopped()) return;
978
+ const isPm = host.config.mode === "pm";
979
+ if (isPm) {
980
+ host.snapshotPlanFiles();
981
+ }
982
+ const options = buildQueryOptions(host, context);
983
+ const resume = context.claudeSessionId ?? void 0;
984
+ if (followUpContent) {
985
+ const prompt = isPm ? `${buildInitialPrompt(host.config.mode, context)}
986
+
987
+ ---
988
+
989
+ The team says:
990
+ ${followUpContent}` : followUpContent;
991
+ const agentQuery = query({ prompt, options: { ...options, resume } });
992
+ await runWithRetry(agentQuery, context, host, options);
993
+ } else if (isPm) {
994
+ return;
995
+ } else {
996
+ const initialPrompt = buildInitialPrompt(host.config.mode, context);
997
+ const agentQuery = query({
998
+ prompt: host.createInputStream(initialPrompt),
999
+ options: { ...options, resume }
1000
+ });
1001
+ await runWithRetry(agentQuery, context, host, options);
1002
+ }
1003
+ if (isPm) {
1004
+ host.syncPlanFile();
1005
+ }
1006
+ }
1007
+ async function runWithRetry(initialQuery, context, host, options) {
1008
+ for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
1009
+ if (host.isStopped()) return;
1010
+ const agentQuery = attempt === 0 ? initialQuery : query({
1011
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
1012
+ options: { ...options, resume: void 0 }
1013
+ });
1014
+ try {
1015
+ const { retriable } = await processEvents(agentQuery, context, host);
1016
+ if (!retriable || host.isStopped()) return;
1017
+ } catch (error) {
1018
+ const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
1019
+ if (isStaleSession && context.claudeSessionId) {
1020
+ host.connection.storeSessionId("");
1021
+ const freshCtx = { ...context, claudeSessionId: null };
1022
+ const freshQuery = query({
1023
+ prompt: host.createInputStream(buildInitialPrompt(host.config.mode, freshCtx)),
1024
+ options: { ...options, resume: void 0 }
1025
+ });
1026
+ return runWithRetry(freshQuery, freshCtx, host, options);
1027
+ }
1028
+ const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
1029
+ if (!isApiError) throw error;
1030
+ }
1031
+ if (attempt >= RETRY_DELAYS_MS.length) {
1032
+ host.connection.postChatMessage(
1033
+ `Agent shutting down after ${RETRY_DELAYS_MS.length} failed retry attempts due to API errors. The task will resume automatically when the codespace restarts.`
1034
+ );
1035
+ return;
1036
+ }
1037
+ const delayMs = RETRY_DELAYS_MS[attempt];
1038
+ const delayMin = Math.round(delayMs / 6e4);
1039
+ host.connection.postChatMessage(
1040
+ `API error encountered. Retrying in ${delayMin} minute${delayMin > 1 ? "s" : ""}... (attempt ${attempt + 1}/${RETRY_DELAYS_MS.length})`
1041
+ );
1042
+ host.connection.sendEvent({
1043
+ type: "error",
1044
+ message: `API error, retrying in ${delayMin}m (${attempt + 1}/${RETRY_DELAYS_MS.length})`
1045
+ });
1046
+ host.connection.emitStatus("waiting_for_input");
1047
+ await host.callbacks.onStatusChange("waiting_for_input");
1048
+ await new Promise((resolve) => {
1049
+ const timer = setTimeout(resolve, delayMs);
1050
+ const checkStopped = setInterval(() => {
1051
+ if (host.isStopped()) {
1052
+ clearTimeout(timer);
1053
+ clearInterval(checkStopped);
1054
+ resolve();
1055
+ }
1056
+ }, 1e3);
1057
+ setTimeout(() => clearInterval(checkStopped), delayMs + 100);
1058
+ });
1059
+ host.connection.emitStatus("running");
1060
+ await host.callbacks.onStatusChange("running");
1061
+ }
1062
+ }
1063
+
1064
+ // src/runner.ts
1065
+ var HEARTBEAT_INTERVAL_MS = 3e4;
1066
+ var AgentRunner = class _AgentRunner {
1067
+ config;
1068
+ connection;
1069
+ callbacks;
1070
+ _state = "connecting";
1071
+ stopped = false;
1072
+ inputResolver = null;
1073
+ pendingMessages = [];
1074
+ setupLog = [];
1075
+ heartbeatTimer = null;
1076
+ taskContext = null;
1077
+ planFileSnapshot = /* @__PURE__ */ new Map();
1078
+ worktreeActive = false;
1079
+ static MAX_SETUP_LOG_LINES = 50;
1080
+ constructor(config, callbacks) {
1081
+ this.config = config;
1082
+ this.connection = new ConveyorConnection(config);
1083
+ this.callbacks = callbacks;
1084
+ }
1085
+ get state() {
1086
+ return this._state;
1087
+ }
1088
+ async setState(status) {
1089
+ this._state = status;
1090
+ this.connection.emitStatus(status);
1091
+ await this.callbacks.onStatusChange(status);
1092
+ }
1093
+ startHeartbeat() {
1094
+ this.heartbeatTimer = setInterval(() => {
1095
+ if (!this.stopped) {
1096
+ this.connection.sendHeartbeat();
1097
+ }
1098
+ }, HEARTBEAT_INTERVAL_MS);
1099
+ }
1100
+ stopHeartbeat() {
1101
+ if (this.heartbeatTimer) {
1102
+ clearInterval(this.heartbeatTimer);
1103
+ this.heartbeatTimer = null;
1104
+ }
1105
+ }
1106
+ async start() {
1107
+ await this.setState("connecting");
1108
+ await this.connection.connect();
1109
+ this.connection.onStopRequested(() => this.stop());
1110
+ this.connection.onChatMessage((message) => this.injectHumanMessage(message.content));
1111
+ await this.setState("connected");
1112
+ this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
1113
+ this.startHeartbeat();
1114
+ if (this.config.mode !== "pm" && process.env.CODESPACES === "true") {
1115
+ const setupOk = await this.runSetupSafe();
1116
+ if (!setupOk) {
1117
+ this.stopHeartbeat();
1118
+ await this.setState("error");
1119
+ this.connection.disconnect();
1120
+ return;
1121
+ }
1122
+ }
1123
+ this.initRtk();
1124
+ if (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true") {
1125
+ try {
1126
+ const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
1127
+ this.config = { ...this.config, workspaceDir: worktreePath };
1128
+ this.worktreeActive = true;
1129
+ this.setupLog.push(`[conveyor] Using worktree: ${worktreePath}`);
1130
+ } catch (error) {
1131
+ const msg = error instanceof Error ? error.message : "Unknown error";
1132
+ this.setupLog.push(`[conveyor] Worktree creation failed, using shared workspace: ${msg}`);
1133
+ }
1134
+ }
1135
+ await this.setState("fetching_context");
1136
+ try {
1137
+ this.taskContext = await this.connection.fetchTaskContext();
1138
+ } catch (error) {
1139
+ const message = error instanceof Error ? error.message : "Failed to fetch task context";
1140
+ this.connection.sendEvent({ type: "error", message });
1141
+ await this.callbacks.onEvent({ type: "error", message });
1142
+ this.stopHeartbeat();
1143
+ await this.setState("error");
1144
+ this.connection.disconnect();
1145
+ return;
1146
+ }
1147
+ if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
1148
+ const result = cleanDevcontainerFromGit(
1149
+ this.config.workspaceDir,
1150
+ this.taskContext.githubBranch,
1151
+ this.taskContext.baseBranch
1152
+ );
1153
+ if (result.cleaned) {
1154
+ this.setupLog.push(`[conveyor] ${result.message}`);
1155
+ }
1156
+ }
1157
+ if (!this.worktreeActive && this.taskContext.useWorktree) {
1158
+ try {
1159
+ const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
1160
+ this.config = { ...this.config, workspaceDir: worktreePath };
1161
+ this.worktreeActive = true;
1162
+ this.setupLog.push(`[conveyor] Using worktree (from task config): ${worktreePath}`);
1163
+ } catch (error) {
1164
+ const msg = error instanceof Error ? error.message : "Unknown error";
1165
+ this.setupLog.push(`[conveyor] Worktree creation failed, using shared workspace: ${msg}`);
1166
+ }
1167
+ }
1168
+ if (this.worktreeActive && this.taskContext.githubBranch) {
1169
+ try {
1170
+ const branch = this.taskContext.githubBranch;
1171
+ execSync3(`git fetch origin ${branch} && git checkout ${branch}`, {
1172
+ cwd: this.config.workspaceDir,
1173
+ stdio: "ignore"
1174
+ });
1175
+ } catch {
1176
+ }
1177
+ }
1178
+ const isPm = this.config.mode === "pm";
1179
+ if (isPm) {
1180
+ await this.setState("idle");
1181
+ } else {
1182
+ await this.setState("running");
1183
+ await runSdkQuery(this.asQueryHost(), this.taskContext);
1184
+ if (!this.stopped) await this.setState("idle");
1185
+ }
1186
+ await this.runCoreLoop();
1187
+ this.stopHeartbeat();
1188
+ await this.setState("finished");
1189
+ this.connection.disconnect();
1190
+ }
1191
+ async runCoreLoop() {
1192
+ if (!this.taskContext) return;
1193
+ while (!this.stopped) {
1194
+ if (this._state === "idle") {
1195
+ const msg = await this.waitForUserContent();
1196
+ if (!msg) break;
1197
+ await this.setState("running");
1198
+ await runSdkQuery(this.asQueryHost(), this.taskContext, msg);
1199
+ if (!this.stopped) await this.setState("idle");
1200
+ } else if (this._state === "error") {
1201
+ await this.setState("idle");
1202
+ } else {
1203
+ break;
1204
+ }
1205
+ }
1206
+ }
1207
+ async runSetupSafe() {
1208
+ await this.setState("setup");
1209
+ const ports = await loadForwardPorts(this.config.workspaceDir);
1210
+ if (ports.length > 0 && process.env.CODESPACE_NAME) {
1211
+ const visibility = ports.map((p) => `${p}:public`).join(" ");
1212
+ runStartCommand(
1213
+ `gh codespace ports visibility ${visibility} -c "${process.env.CODESPACE_NAME}" 2>/dev/null`,
1214
+ this.config.workspaceDir,
1215
+ () => void 0
1216
+ );
1217
+ }
1218
+ const config = await loadConveyorConfig(this.config.workspaceDir);
1219
+ if (!config) {
1220
+ this.connection.sendEvent({ type: "setup_complete" });
1221
+ await this.callbacks.onEvent({ type: "setup_complete" });
1222
+ return true;
1223
+ }
1224
+ try {
1225
+ await this.executeSetupConfig(config);
1226
+ const setupEvent = {
1227
+ type: "setup_complete",
1228
+ previewPort: config.previewPort ?? void 0
1229
+ };
1230
+ this.connection.sendEvent(setupEvent);
1231
+ await this.callbacks.onEvent(setupEvent);
1232
+ return true;
1233
+ } catch (error) {
1234
+ const message = error instanceof Error ? error.message : "Setup failed";
1235
+ this.connection.sendEvent({ type: "setup_error", message });
1236
+ await this.callbacks.onEvent({ type: "setup_error", message });
1237
+ this.connection.postChatMessage(
1238
+ `Environment setup failed: ${message}
1239
+ The agent cannot start until this is resolved.`
1240
+ );
1241
+ return false;
1242
+ }
1243
+ }
1244
+ pushSetupLog(line) {
1245
+ this.setupLog.push(line);
1246
+ if (this.setupLog.length > _AgentRunner.MAX_SETUP_LOG_LINES) {
1247
+ this.setupLog.splice(0, this.setupLog.length - _AgentRunner.MAX_SETUP_LOG_LINES);
1248
+ }
1249
+ }
1250
+ async executeSetupConfig(config) {
1251
+ if (config.setupCommand) {
1252
+ this.pushSetupLog(`$ ${config.setupCommand}`);
1253
+ await runSetupCommand(config.setupCommand, this.config.workspaceDir, (stream, data) => {
1254
+ this.connection.sendEvent({ type: "setup_output", stream, data });
1255
+ for (const line of data.split("\n").filter(Boolean)) {
1256
+ this.pushSetupLog(`[${stream}] ${line}`);
1257
+ }
1258
+ });
1259
+ this.pushSetupLog("(exit 0)");
1260
+ }
1261
+ if (config.startCommand) {
1262
+ this.pushSetupLog(`$ ${config.startCommand} & (background)`);
1263
+ runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
1264
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
1265
+ });
1266
+ }
1267
+ }
1268
+ initRtk() {
1269
+ try {
1270
+ execSync3("rtk --version", { stdio: "ignore" });
1271
+ execSync3("rtk init --global --auto-patch", { stdio: "ignore" });
1272
+ } catch {
1273
+ }
1274
+ }
1275
+ injectHumanMessage(content) {
1276
+ const msg = {
1277
+ type: "user",
1278
+ session_id: "",
1279
+ message: { role: "user", content },
1280
+ parent_tool_use_id: null
1281
+ };
1282
+ if (this.inputResolver) {
1283
+ const resolve = this.inputResolver;
1284
+ this.inputResolver = null;
1285
+ resolve(msg);
1286
+ } else {
1287
+ this.pendingMessages.push(msg);
1288
+ }
1289
+ }
1290
+ waitForMessage() {
1291
+ return new Promise((resolve) => {
1292
+ const checkStopped = setInterval(() => {
1293
+ if (this.stopped) {
1294
+ clearInterval(checkStopped);
1295
+ this.inputResolver = null;
1296
+ resolve(null);
1297
+ }
1298
+ }, 1e3);
1299
+ this.inputResolver = (msg) => {
1300
+ clearInterval(checkStopped);
1301
+ resolve(msg);
1302
+ };
1303
+ });
1304
+ }
1305
+ async waitForUserContent() {
1306
+ if (this.pendingMessages.length > 0) {
1307
+ const next = this.pendingMessages.shift();
1308
+ return next?.message?.content ?? null;
1309
+ }
1310
+ const msg = await this.waitForMessage();
1311
+ if (!msg) return null;
1312
+ return msg.message.content;
1313
+ }
1314
+ async *createInputStream(initialPrompt) {
1315
+ const makeUserMessage = (content) => ({
1316
+ type: "user",
1317
+ session_id: "",
1318
+ message: { role: "user", content },
1319
+ parent_tool_use_id: null
1320
+ });
1321
+ yield makeUserMessage(initialPrompt);
1322
+ while (!this.stopped) {
1323
+ if (this.pendingMessages.length > 0) {
1324
+ const next = this.pendingMessages.shift();
1325
+ if (next) yield next;
1326
+ continue;
1327
+ }
1328
+ this.connection.emitStatus("waiting_for_input");
1329
+ await this.callbacks.onStatusChange("waiting_for_input");
1330
+ const msg = await this.waitForMessage();
1331
+ if (!msg) break;
1332
+ this.connection.emitStatus("running");
1333
+ await this.callbacks.onStatusChange("running");
1334
+ yield msg;
1335
+ }
1336
+ }
1337
+ /**
1338
+ * Snapshot current plan files so syncPlanFile can distinguish files created
1339
+ * by THIS session from ones created by a concurrent agent.
1340
+ */
1341
+ snapshotPlanFiles() {
1342
+ const plansDir = join3(homedir(), ".claude", "plans");
1343
+ this.planFileSnapshot.clear();
1344
+ try {
1345
+ for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
1346
+ try {
1347
+ const stat = statSync(join3(plansDir, file));
1348
+ this.planFileSnapshot.set(file, stat.mtimeMs);
1349
+ } catch {
1350
+ continue;
1351
+ }
1352
+ }
1353
+ } catch {
1354
+ }
1355
+ }
1356
+ syncPlanFile() {
1357
+ const plansDir = join3(homedir(), ".claude", "plans");
1358
+ let files;
1359
+ try {
1360
+ files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1361
+ } catch {
1362
+ return;
1363
+ }
1364
+ let newest = null;
1365
+ for (const file of files) {
1366
+ const fullPath = join3(plansDir, file);
1367
+ try {
1368
+ const stat = statSync(fullPath);
1369
+ const prevMtime = this.planFileSnapshot.get(file);
1370
+ const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
1371
+ if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
1372
+ newest = { path: fullPath, mtime: stat.mtimeMs };
1373
+ }
1374
+ } catch {
1375
+ continue;
1376
+ }
1377
+ }
1378
+ if (newest) {
1379
+ const content = readFileSync(newest.path, "utf-8").trim();
1380
+ if (content) {
1381
+ this.connection.updateTaskFields({ plan: content });
1382
+ }
1383
+ }
1384
+ }
1385
+ asQueryHost() {
1386
+ return {
1387
+ config: this.config,
1388
+ connection: this.connection,
1389
+ callbacks: this.callbacks,
1390
+ setupLog: this.setupLog,
1391
+ isStopped: () => this.stopped,
1392
+ createInputStream: (prompt) => this.createInputStream(prompt),
1393
+ snapshotPlanFiles: () => this.snapshotPlanFiles(),
1394
+ syncPlanFile: () => this.syncPlanFile()
1395
+ };
1396
+ }
1397
+ stop() {
1398
+ this.stopped = true;
1399
+ if (this.inputResolver) {
1400
+ this.inputResolver(null);
1401
+ this.inputResolver = null;
1402
+ }
1403
+ }
1404
+ };
1405
+
1406
+ export {
1407
+ ConveyorConnection,
1408
+ loadConveyorConfig,
1409
+ runSetupCommand,
1410
+ runStartCommand,
1411
+ ensureWorktree,
1412
+ removeWorktree,
1413
+ AgentRunner
1414
+ };
1415
+ //# sourceMappingURL=chunk-42BYQ6YY.js.map