@rallycry/conveyor-agent 3.9.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ var ConveyorConnection = class _ConveyorConnection {
12
12
  chatMessageCallback = null;
13
13
  stopCallback = null;
14
14
  modeChangeCallback = null;
15
+ runStartCommandCallback = null;
15
16
  pendingQuestionResolvers = /* @__PURE__ */ new Map();
16
17
  constructor(config) {
17
18
  this.config = config;
@@ -61,6 +62,11 @@ var ConveyorConnection = class _ConveyorConnection {
61
62
  this.earlyModeChanges.push(data);
62
63
  }
63
64
  });
65
+ this.socket.on("agentRunner:runStartCommand", () => {
66
+ if (this.runStartCommandCallback) {
67
+ this.runStartCommandCallback();
68
+ }
69
+ });
64
70
  this.socket.on("connect", () => {
65
71
  if (!settled) {
66
72
  settled = true;
@@ -76,13 +82,13 @@ var ConveyorConnection = class _ConveyorConnection {
76
82
  });
77
83
  });
78
84
  }
79
- fetchChatMessages(limit) {
85
+ fetchChatMessages(limit, taskId) {
80
86
  const socket = this.socket;
81
87
  if (!socket) throw new Error("Not connected");
82
88
  return new Promise((resolve2, reject) => {
83
89
  socket.emit(
84
90
  "agentRunner:getChatMessages",
85
- { limit },
91
+ { limit, taskId },
86
92
  (response) => {
87
93
  if (response.success && response.data) {
88
94
  resolve2(response.data);
@@ -226,9 +232,12 @@ var ConveyorConnection = class _ConveyorConnection {
226
232
  }
227
233
  this.earlyModeChanges = [];
228
234
  }
229
- emitModeChanged(mode) {
235
+ onRunStartCommand(callback) {
236
+ this.runStartCommandCallback = callback;
237
+ }
238
+ emitModeChanged(agentMode) {
230
239
  if (!this.socket) return;
231
- this.socket.emit("agentRunner:modeChanged", { mode });
240
+ this.socket.emit("agentRunner:modeChanged", { agentMode });
232
241
  }
233
242
  trackSpending(params) {
234
243
  if (!this.socket) throw new Error("Not connected");
@@ -330,6 +339,48 @@ var ConveyorConnection = class _ConveyorConnection {
330
339
  );
331
340
  });
332
341
  }
342
+ postChildChatMessage(childTaskId, content) {
343
+ const socket = this.socket;
344
+ if (!socket) throw new Error("Not connected");
345
+ return new Promise((resolve2, reject) => {
346
+ socket.emit(
347
+ "agentRunner:postChildChatMessage",
348
+ { childTaskId, content },
349
+ (response) => {
350
+ if (response.success) resolve2();
351
+ else reject(new Error(response.error ?? "Failed to post to child chat"));
352
+ }
353
+ );
354
+ });
355
+ }
356
+ updateChildStatus(childTaskId, status) {
357
+ const socket = this.socket;
358
+ if (!socket) throw new Error("Not connected");
359
+ return new Promise((resolve2, reject) => {
360
+ socket.emit(
361
+ "agentRunner:updateChildStatus",
362
+ { childTaskId, status },
363
+ (response) => {
364
+ if (response.success) resolve2();
365
+ else reject(new Error(response.error ?? "Failed to update child status"));
366
+ }
367
+ );
368
+ });
369
+ }
370
+ stopChildBuild(childTaskId) {
371
+ const socket = this.socket;
372
+ if (!socket) throw new Error("Not connected");
373
+ return new Promise((resolve2, reject) => {
374
+ socket.emit(
375
+ "agentRunner:stopChildBuild",
376
+ { childTaskId },
377
+ (response) => {
378
+ if (response.success) resolve2();
379
+ else reject(new Error(response.error ?? "Failed to stop child build"));
380
+ }
381
+ );
382
+ });
383
+ }
333
384
  fetchTask(slugOrId) {
334
385
  const socket = this.socket;
335
386
  if (!socket) throw new Error("Not connected");
@@ -347,6 +398,66 @@ var ConveyorConnection = class _ConveyorConnection {
347
398
  );
348
399
  });
349
400
  }
401
+ updateTaskProperties(data) {
402
+ if (!this.socket) throw new Error("Not connected");
403
+ this.socket.emit("agentRunner:updateTaskProperties", data);
404
+ }
405
+ listIcons() {
406
+ const socket = this.socket;
407
+ if (!socket) throw new Error("Not connected");
408
+ return new Promise((resolve2, reject) => {
409
+ socket.emit("agentRunner:listIcons", {}, (response) => {
410
+ if (response.success && response.data) resolve2(response.data);
411
+ else reject(new Error(response.error ?? "Failed to list icons"));
412
+ });
413
+ });
414
+ }
415
+ generateTaskIcon(prompt, aspectRatio) {
416
+ const socket = this.socket;
417
+ if (!socket) throw new Error("Not connected");
418
+ return new Promise((resolve2, reject) => {
419
+ socket.emit(
420
+ "agentRunner:generateTaskIcon",
421
+ { prompt, aspectRatio },
422
+ (response) => {
423
+ if (response.success && response.data) resolve2(response.data);
424
+ else reject(new Error(response.error ?? "Failed to generate icon"));
425
+ }
426
+ );
427
+ });
428
+ }
429
+ getTaskProperties() {
430
+ const socket = this.socket;
431
+ if (!socket) throw new Error("Not connected");
432
+ return new Promise((resolve2, reject) => {
433
+ socket.emit(
434
+ "agentRunner:getTaskProperties",
435
+ {},
436
+ (response) => {
437
+ if (response.success && response.data) resolve2(response.data);
438
+ else reject(new Error(response.error ?? "Failed to get task properties"));
439
+ }
440
+ );
441
+ });
442
+ }
443
+ triggerIdentification() {
444
+ const socket = this.socket;
445
+ if (!socket) throw new Error("Not connected");
446
+ return new Promise((resolve2, reject) => {
447
+ socket.emit(
448
+ "agentRunner:triggerIdentification",
449
+ {},
450
+ (response) => {
451
+ if (response.success && response.data) resolve2(response.data);
452
+ else reject(new Error(response.error ?? "Identification failed"));
453
+ }
454
+ );
455
+ });
456
+ }
457
+ emitModeTransition(payload) {
458
+ if (!this.socket) return;
459
+ this.socket.emit("agentRunner:modeTransition", payload);
460
+ }
350
461
  disconnect() {
351
462
  this.flushEvents();
352
463
  this.socket?.disconnect();
@@ -799,6 +910,13 @@ async function processEvents(events, context, host) {
799
910
  const turnToolCalls = [];
800
911
  for await (const event of events) {
801
912
  if (host.isStopped()) break;
913
+ if (host.pendingModeRestart) {
914
+ host.pendingModeRestart = false;
915
+ if (isTyping) {
916
+ host.connection.sendTypingStop();
917
+ }
918
+ return { retriable: false, modeRestart: true };
919
+ }
802
920
  switch (event.type) {
803
921
  case "system": {
804
922
  const systemEvent = event;
@@ -1007,12 +1125,22 @@ ${context.plan}`);
1007
1125
  }
1008
1126
  return parts;
1009
1127
  }
1010
- function buildInstructions(mode, context, scenario) {
1128
+ function buildInstructions(mode, context, scenario, agentMode) {
1011
1129
  const parts = [`
1012
1130
  ## Instructions`];
1013
1131
  const isPm = mode === "pm";
1132
+ const isAutoMode = agentMode === "auto";
1014
1133
  if (scenario === "fresh") {
1015
- if (isPm && context.isParentTask) {
1134
+ if (isAutoMode && isPm) {
1135
+ parts.push(
1136
+ `You are operating autonomously. Begin planning immediately.`,
1137
+ `1. Explore the codebase to understand the architecture and relevant files`,
1138
+ `2. Draft a clear implementation plan and save it with update_task`,
1139
+ `3. Set story points (set_story_points), tags (set_task_tags), and title (set_task_title)`,
1140
+ `4. When the plan and all required properties are set, call ExitPlanMode to transition to building`,
1141
+ `Do NOT wait for team input \u2014 proceed autonomously.`
1142
+ );
1143
+ } else if (isPm && context.isParentTask) {
1016
1144
  parts.push(
1017
1145
  `You are the project manager for this task and its subtasks.`,
1018
1146
  `Use list_subtasks to review the current state of child tasks.`,
@@ -1091,6 +1219,112 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
1091
1219
  }
1092
1220
  return parts;
1093
1221
  }
1222
+ function buildPropertyInstructions(context) {
1223
+ const parts = [];
1224
+ parts.push(
1225
+ ``,
1226
+ `### Proactive Property Management`,
1227
+ `As you plan this task, proactively fill in task properties when you have enough context:`,
1228
+ `- Once you understand the scope, use set_story_points to assign a value`,
1229
+ `- Use set_task_tags to categorize the work`,
1230
+ `- For icons: FIRST call list_icons to check for existing matches. Use set_task_icon if one fits.`,
1231
+ ` Only call generate_task_icon if no existing icon is a good fit.`,
1232
+ `- Use set_task_title if the current title doesn't accurately reflect the plan`,
1233
+ ``,
1234
+ `Don't wait for the user to ask \u2014 fill these in naturally as the plan takes shape.`,
1235
+ `If the user adjusts the plan significantly, update the properties to match.`
1236
+ );
1237
+ if (context.storyPoints && context.storyPoints.length > 0) {
1238
+ parts.push(``, `Available story point tiers:`);
1239
+ for (const sp of context.storyPoints) {
1240
+ const desc = sp.description ? ` \u2014 ${sp.description}` : "";
1241
+ parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
1242
+ }
1243
+ }
1244
+ if (context.projectTags && context.projectTags.length > 0) {
1245
+ parts.push(``, `Available project tags:`);
1246
+ for (const tag of context.projectTags) {
1247
+ parts.push(`- ID: "${tag.id}", Name: "${tag.name}"`);
1248
+ }
1249
+ }
1250
+ return parts;
1251
+ }
1252
+ function buildModePrompt(agentMode, context) {
1253
+ switch (agentMode) {
1254
+ case "discovery": {
1255
+ const parts = [
1256
+ `
1257
+ ## Mode: Discovery`,
1258
+ `You are in Discovery mode \u2014 helping plan and scope this task.`,
1259
+ `- You have read-only codebase access (can read files, run git commands, search code)`,
1260
+ `- You have MCP tools: update_task (title, description, plan, tags, SP, icon)`,
1261
+ `- You can create and manage subtasks`,
1262
+ `- You cannot write code or edit files (except .claude/plans/)`,
1263
+ `- Goal: collaborate with the user to create a clear plan`,
1264
+ `- Proactively fill task properties (SP, tags, icon) as the plan takes shape`
1265
+ ];
1266
+ if (context) {
1267
+ parts.push(...buildPropertyInstructions(context));
1268
+ }
1269
+ return parts.join("\n");
1270
+ }
1271
+ case "building":
1272
+ return [
1273
+ `
1274
+ ## Mode: Building`,
1275
+ `You are in Building mode \u2014 executing the plan.`,
1276
+ `- You have full coding access (read, write, edit, bash, git)`,
1277
+ `- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
1278
+ `- If this is a leaf task (no children): execute the plan directly`,
1279
+ `- Goal: implement the plan, run tests, open a PR when done`
1280
+ ].join("\n");
1281
+ case "review":
1282
+ return [
1283
+ `
1284
+ ## Mode: Review`,
1285
+ `You are in Review mode \u2014 reviewing and coordinating.`,
1286
+ `- You have read-only access plus light edit capability (can suggest fixes, run tests, check linting)`,
1287
+ `- For parent tasks: you can manage children, review child PRs, fire next child builds`,
1288
+ `- You have Pack Runner coordination tools (list_subtasks, fire builds, approve PRs)`,
1289
+ `- Goal: ensure quality, provide feedback, coordinate progression`
1290
+ ].join("\n");
1291
+ case "auto": {
1292
+ const parts = [
1293
+ `
1294
+ ## Mode: Auto`,
1295
+ `You are in Auto mode \u2014 operating autonomously through planning \u2192 building \u2192 PR.`,
1296
+ ``,
1297
+ `### Phase 1: Discovery & Planning (current)`,
1298
+ `- You have read-only codebase access (can read files, run git commands, search code)`,
1299
+ `- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
1300
+ `- You have MCP tools for task properties: update_task, set_story_points, set_task_tags, set_task_icon, set_task_title`,
1301
+ ``,
1302
+ `### Required before transitioning:`,
1303
+ `Before calling ExitPlanMode, you MUST fill in ALL of these:`,
1304
+ `1. **Plan** \u2014 Save a clear implementation plan using update_task`,
1305
+ `2. **Story Points** \u2014 Assign via set_story_points`,
1306
+ `3. **Title** \u2014 Set an accurate title via set_task_title (if the current one is vague or "Untitled")`,
1307
+ ``,
1308
+ `### Transitioning to Building:`,
1309
+ `When your plan is complete and all required properties are set, call the **ExitPlanMode** tool.`,
1310
+ `- If any required properties are missing, ExitPlanMode will be denied with details on what's missing`,
1311
+ `- Once ExitPlanMode succeeds, the system will automatically restart your session in Building mode with the appropriate model`,
1312
+ `- You do NOT need to do anything after calling ExitPlanMode \u2014 the transition is handled for you`,
1313
+ ``,
1314
+ `### Autonomous Guidelines:`,
1315
+ `- Make decisions independently \u2014 do not ask the team for approval at each step`,
1316
+ `- Only escalate when genuinely blocked (ambiguous requirements, missing access, conflicting instructions)`,
1317
+ `- Be thorough in discovery: read relevant files, understand the codebase architecture, then plan`
1318
+ ];
1319
+ if (context) {
1320
+ parts.push(...buildPropertyInstructions(context));
1321
+ }
1322
+ return parts.join("\n");
1323
+ }
1324
+ default:
1325
+ return null;
1326
+ }
1327
+ }
1094
1328
  function buildPackRunnerSystemPrompt(context, config, setupLog) {
1095
1329
  const parts = [
1096
1330
  `You are an autonomous Pack Runner managing child tasks for the "${context.title}" project.`,
@@ -1201,7 +1435,7 @@ After addressing the feedback, resume your autonomous loop: call list_subtasks a
1201
1435
  }
1202
1436
  return parts;
1203
1437
  }
1204
- function buildInitialPrompt(mode, context, isAuto) {
1438
+ function buildInitialPrompt(mode, context, isAuto, agentMode) {
1205
1439
  const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
1206
1440
  if (!isPackRunner) {
1207
1441
  const sessionRelaunch = buildRelaunchWithSession(mode, context);
@@ -1209,12 +1443,12 @@ function buildInitialPrompt(mode, context, isAuto) {
1209
1443
  }
1210
1444
  const scenario = detectRelaunchScenario(context);
1211
1445
  const body = buildTaskBody(context);
1212
- const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario);
1446
+ const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
1213
1447
  return [...body, ...instructions].join("\n");
1214
1448
  }
1215
- function buildSystemPrompt(mode, context, config, setupLog, pmSubMode = "planning") {
1449
+ function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
1216
1450
  const isPm = mode === "pm";
1217
- const isPmActive = isPm && pmSubMode === "active";
1451
+ const isPmActive = isPm && agentMode === "building";
1218
1452
  const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
1219
1453
  if (isPackRunner) {
1220
1454
  return buildPackRunnerSystemPrompt(context, config, setupLog);
@@ -1340,6 +1574,10 @@ Your responses are sent directly to the task chat \u2014 the team sees everythin
1340
1574
  `Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
1341
1575
  );
1342
1576
  }
1577
+ const modePrompt = buildModePrompt(agentMode, context);
1578
+ if (modePrompt) {
1579
+ parts.push(modePrompt);
1580
+ }
1343
1581
  return parts.join("\n");
1344
1582
  }
1345
1583
 
@@ -1389,13 +1627,14 @@ function buildCommonTools(connection, config) {
1389
1627
  return [
1390
1628
  tool(
1391
1629
  "read_task_chat",
1392
- "Read recent messages from the task chat to see team feedback or instructions",
1630
+ "Read recent messages from a task chat. Omit task_id to read the current task's chat, or provide a child task ID to read a child's chat.",
1393
1631
  {
1394
- limit: z.number().optional().describe("Number of recent messages to fetch (default 20)")
1632
+ limit: z.number().optional().describe("Number of recent messages to fetch (default 20)"),
1633
+ task_id: z.string().optional().describe("Child task ID to read chat from. Omit to read the current task's chat.")
1395
1634
  },
1396
- async ({ limit }) => {
1635
+ async ({ limit, task_id }) => {
1397
1636
  try {
1398
- const messages = await connection.fetchChatMessages(limit);
1637
+ const messages = await connection.fetchChatMessages(limit, task_id);
1399
1638
  return textResult(JSON.stringify(messages, null, 2));
1400
1639
  } catch {
1401
1640
  return textResult(
@@ -1409,22 +1648,46 @@ function buildCommonTools(connection, config) {
1409
1648
  ),
1410
1649
  tool(
1411
1650
  "post_to_chat",
1412
- "Post a message to the task chat. Your normal replies already appear in chat \u2014 only use this for explicit out-of-band updates or posting to a different task's chat",
1413
- { message: z.string().describe("The message to post to the team") },
1414
- ({ message }) => {
1415
- connection.postChatMessage(message);
1416
- return Promise.resolve(textResult("Message posted to task chat."));
1651
+ "Post a message to a task chat. Your normal replies already appear in chat \u2014 only use this for explicit out-of-band updates or posting to a child task's chat.",
1652
+ {
1653
+ message: z.string().describe("The message to post to the team"),
1654
+ task_id: z.string().optional().describe("Child task ID to post to. Omit to post to the current task's chat.")
1655
+ },
1656
+ async ({ message, task_id }) => {
1657
+ try {
1658
+ if (task_id) {
1659
+ await connection.postChildChatMessage(task_id, message);
1660
+ return textResult(`Message posted to child task ${task_id} chat.`);
1661
+ }
1662
+ connection.postChatMessage(message);
1663
+ return textResult("Message posted to task chat.");
1664
+ } catch (error) {
1665
+ return textResult(
1666
+ `Failed to post message: ${error instanceof Error ? error.message : "Unknown error"}`
1667
+ );
1668
+ }
1417
1669
  }
1418
1670
  ),
1419
1671
  tool(
1420
1672
  "update_task_status",
1421
- "Update the task status on the Kanban board",
1673
+ "Update a task's status on the Kanban board. Omit task_id to update the current task, or provide a child task ID to update a child's status.",
1422
1674
  {
1423
- status: z.enum(["InProgress", "ReviewPR", "Complete"]).describe("The new status for the task")
1675
+ status: z.enum(["InProgress", "ReviewPR", "ReviewDev", "Complete"]).describe("The new status for the task"),
1676
+ task_id: z.string().optional().describe("Child task ID to update. Omit to update the current task.")
1424
1677
  },
1425
- ({ status }) => {
1426
- connection.updateStatus(status);
1427
- return Promise.resolve(textResult(`Task status updated to ${status}.`));
1678
+ async ({ status, task_id }) => {
1679
+ try {
1680
+ if (task_id) {
1681
+ await connection.updateChildStatus(task_id, status);
1682
+ return textResult(`Child task ${task_id} status updated to ${status}.`);
1683
+ }
1684
+ connection.updateStatus(status);
1685
+ return textResult(`Task status updated to ${status}.`);
1686
+ } catch (error) {
1687
+ return textResult(
1688
+ `Failed to update status: ${error instanceof Error ? error.message : "Unknown error"}`
1689
+ );
1690
+ }
1428
1691
  }
1429
1692
  ),
1430
1693
  tool(
@@ -1655,6 +1918,23 @@ function buildPmTools(connection, storyPoints, options) {
1655
1918
  }
1656
1919
  }
1657
1920
  ),
1921
+ tool2(
1922
+ "stop_child_build",
1923
+ "Stop a running cloud build for a child task. Sends a stop signal to the child agent.",
1924
+ {
1925
+ childTaskId: z2.string().describe("The child task ID whose build should be stopped")
1926
+ },
1927
+ async ({ childTaskId }) => {
1928
+ try {
1929
+ await connection.stopChildBuild(childTaskId);
1930
+ return textResult(`Stop signal sent to child task: ${childTaskId}`);
1931
+ } catch (error) {
1932
+ return textResult(
1933
+ `Failed to stop child build: ${error instanceof Error ? error.message : "Unknown error"}`
1934
+ );
1935
+ }
1936
+ }
1937
+ ),
1658
1938
  tool2(
1659
1939
  "approve_and_merge_pr",
1660
1940
  "Approve and merge a child task's pull request. Only succeeds if all CI/CD checks are passing. Returns an error if checks are pending (retry after waiting) or failed (investigate). The child task must be in ReviewPR status.",
@@ -1707,6 +1987,117 @@ function buildTaskTools(connection) {
1707
1987
  ];
1708
1988
  }
1709
1989
 
1990
+ // src/tools/discovery-tools.ts
1991
+ import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
1992
+ import { z as z4 } from "zod";
1993
+ function buildDiscoveryTools(connection, context) {
1994
+ const spDescription = buildStoryPointDescription(context?.storyPoints);
1995
+ return [
1996
+ tool4(
1997
+ "set_story_points",
1998
+ "Set the story point estimate for this task. Use after understanding the scope of the work.",
1999
+ { value: z4.number().describe(spDescription) },
2000
+ async ({ value }) => {
2001
+ try {
2002
+ connection.updateTaskProperties({ storyPointValue: value });
2003
+ return textResult(`Story points set to ${value}`);
2004
+ } catch (error) {
2005
+ return textResult(
2006
+ `Failed to set story points: ${error instanceof Error ? error.message : "Unknown error"}`
2007
+ );
2008
+ }
2009
+ }
2010
+ ),
2011
+ tool4(
2012
+ "set_task_tags",
2013
+ "Assign tags to this task from the project's available tags. Use the tag IDs from the project tags list.",
2014
+ {
2015
+ tagIds: z4.array(z4.string()).describe("Array of tag IDs to assign")
2016
+ },
2017
+ async ({ tagIds }) => {
2018
+ try {
2019
+ connection.updateTaskProperties({ tagIds });
2020
+ return textResult(`Tags assigned: ${tagIds.length} tag(s)`);
2021
+ } catch (error) {
2022
+ return textResult(
2023
+ `Failed to set tags: ${error instanceof Error ? error.message : "Unknown error"}`
2024
+ );
2025
+ }
2026
+ }
2027
+ ),
2028
+ tool4(
2029
+ "set_task_title",
2030
+ "Update the task title to better reflect the planned work.",
2031
+ {
2032
+ title: z4.string().describe("The new task title")
2033
+ },
2034
+ async ({ title }) => {
2035
+ try {
2036
+ connection.updateTaskProperties({ title });
2037
+ return textResult(`Task title updated to: ${title}`);
2038
+ } catch (error) {
2039
+ return textResult(
2040
+ `Failed to update title: ${error instanceof Error ? error.message : "Unknown error"}`
2041
+ );
2042
+ }
2043
+ }
2044
+ ),
2045
+ tool4(
2046
+ "list_icons",
2047
+ "List available icons (default library + user-created). Returns icon IDs, names, and whether they're defaults. Call this FIRST before set_task_icon to check for existing matches.",
2048
+ {},
2049
+ async () => {
2050
+ try {
2051
+ const icons = await connection.listIcons();
2052
+ return textResult(JSON.stringify(icons, null, 2));
2053
+ } catch (error) {
2054
+ return textResult(
2055
+ `Failed to list icons: ${error instanceof Error ? error.message : "Unknown error"}`
2056
+ );
2057
+ }
2058
+ },
2059
+ { annotations: { readOnlyHint: true } }
2060
+ ),
2061
+ tool4(
2062
+ "set_task_icon",
2063
+ "Assign an existing icon to this task by its ID. Use list_icons first to find a matching icon.",
2064
+ {
2065
+ iconId: z4.string().describe("The icon ID to assign")
2066
+ },
2067
+ async ({ iconId }) => {
2068
+ try {
2069
+ connection.updateTaskProperties({ iconId });
2070
+ return textResult("Icon assigned to task.");
2071
+ } catch (error) {
2072
+ return textResult(
2073
+ `Failed to set icon: ${error instanceof Error ? error.message : "Unknown error"}`
2074
+ );
2075
+ }
2076
+ }
2077
+ ),
2078
+ tool4(
2079
+ "generate_task_icon",
2080
+ "Generate a new SVG icon using AI and assign it to this task. Only use if no existing icon from list_icons is a good fit. Provide a concise visual description.",
2081
+ {
2082
+ prompt: z4.string().describe(
2083
+ "Description of the icon to generate (e.g. 'minimal flat gear and wrench icon')"
2084
+ ),
2085
+ aspectRatio: z4.enum(["auto", "portrait", "landscape", "square"]).optional().describe("Icon aspect ratio, defaults to square")
2086
+ },
2087
+ async ({ prompt, aspectRatio }) => {
2088
+ try {
2089
+ const result = await connection.generateTaskIcon(prompt, aspectRatio ?? "square");
2090
+ return textResult(`Icon generated and assigned: ${result.iconId}`);
2091
+ } catch (error) {
2092
+ return textResult(
2093
+ `Failed to generate icon: ${error instanceof Error ? error.message : "Unknown error"}`
2094
+ );
2095
+ }
2096
+ }
2097
+ )
2098
+ ];
2099
+ }
2100
+
1710
2101
  // src/tools/index.ts
1711
2102
  function textResult(text) {
1712
2103
  return { content: [{ type: "text", text }] };
@@ -1716,11 +2107,34 @@ function imageBlock(data, mimeType) {
1716
2107
  }
1717
2108
  function createConveyorMcpServer(connection, config, context) {
1718
2109
  const commonTools = buildCommonTools(connection, config);
1719
- const isPackRunner = config.mode === "pm" && !!config.isAuto && !!context?.isParentTask;
1720
- const modeTools = config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: isPackRunner }) : buildTaskTools(connection);
2110
+ const agentMode = context?.agentMode;
2111
+ let modeTools;
2112
+ switch (agentMode) {
2113
+ case "building":
2114
+ modeTools = buildTaskTools(connection);
2115
+ break;
2116
+ case "review":
2117
+ modeTools = buildPmTools(connection, context?.storyPoints, {
2118
+ includePackTools: !!context?.isParentTask
2119
+ });
2120
+ break;
2121
+ case "auto":
2122
+ modeTools = buildPmTools(connection, context?.storyPoints, {
2123
+ includePackTools: !!context?.isParentTask
2124
+ });
2125
+ break;
2126
+ case "discovery":
2127
+ case "help":
2128
+ modeTools = buildPmTools(connection, context?.storyPoints, { includePackTools: false });
2129
+ break;
2130
+ default:
2131
+ modeTools = config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: false }) : buildTaskTools(connection);
2132
+ break;
2133
+ }
2134
+ const discoveryTools = agentMode === "discovery" || agentMode === "auto" ? buildDiscoveryTools(connection, context) : [];
1721
2135
  return createSdkMcpServer({
1722
2136
  name: "conveyor",
1723
- tools: [...commonTools, ...modeTools]
2137
+ tools: [...commonTools, ...modeTools, ...discoveryTools]
1724
2138
  });
1725
2139
  }
1726
2140
 
@@ -1730,84 +2144,159 @@ var IMAGE_ERROR_PATTERN = /Could not process image/i;
1730
2144
  var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
1731
2145
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
1732
2146
  var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
2147
+ function handleDiscoveryToolAccess(toolName, input) {
2148
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2149
+ const filePath = String(input.file_path ?? input.path ?? "");
2150
+ if (filePath.includes(".claude/plans/")) {
2151
+ return { behavior: "allow", updatedInput: input };
2152
+ }
2153
+ return {
2154
+ behavior: "deny",
2155
+ message: "Discovery mode is read-only. File writes are restricted to plan files."
2156
+ };
2157
+ }
2158
+ return { behavior: "allow", updatedInput: input };
2159
+ }
2160
+ function handleBuildingToolAccess(toolName, input) {
2161
+ if (toolName === "Bash") {
2162
+ const cmd = String(input.command ?? "");
2163
+ if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2164
+ return {
2165
+ behavior: "deny",
2166
+ message: "Destructive operation blocked. Use safer alternatives."
2167
+ };
2168
+ }
2169
+ }
2170
+ return { behavior: "allow", updatedInput: input };
2171
+ }
2172
+ function handleReviewToolAccess(toolName, input, isParentTask) {
2173
+ if (isParentTask) {
2174
+ return handleBuildingToolAccess(toolName, input);
2175
+ }
2176
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2177
+ const filePath = String(input.file_path ?? input.path ?? "");
2178
+ if (filePath.includes(".claude/plans/")) {
2179
+ return { behavior: "allow", updatedInput: input };
2180
+ }
2181
+ return {
2182
+ behavior: "deny",
2183
+ message: "Review mode restricts file writes. Use bash to run tests and linting instead."
2184
+ };
2185
+ }
2186
+ if (toolName === "Bash") {
2187
+ const cmd = String(input.command ?? "");
2188
+ if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2189
+ return { behavior: "deny", message: "Destructive operation blocked in review mode." };
2190
+ }
2191
+ }
2192
+ return { behavior: "allow", updatedInput: input };
2193
+ }
2194
+ function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask) {
2195
+ if (hasExitedPlanMode) {
2196
+ return isParentTask ? handleReviewToolAccess(toolName, input, true) : handleBuildingToolAccess(toolName, input);
2197
+ }
2198
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2199
+ const filePath = String(input.file_path ?? input.path ?? "");
2200
+ if (filePath.includes(".claude/plans/")) {
2201
+ return { behavior: "allow", updatedInput: input };
2202
+ }
2203
+ return {
2204
+ behavior: "deny",
2205
+ message: "You are in auto plan mode. File writes are restricted to plan files. Call ExitPlanMode when your plan is ready to start building."
2206
+ };
2207
+ }
2208
+ return { behavior: "allow", updatedInput: input };
2209
+ }
1733
2210
  function buildCanUseTool(host) {
1734
2211
  const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
1735
2212
  return async (toolName, input) => {
1736
- const isPackRunner = host.config.mode === "pm" && !!host.config.isAuto;
1737
- const isPmPlanning = host.config.mode === "pm" && host.activeMode === "planning";
1738
- const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
1739
- if (isPackRunner) {
1740
- if (toolName === "Bash") {
1741
- const cmd = String(input.command ?? "");
1742
- if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2213
+ if (toolName === "ExitPlanMode" && host.agentMode === "auto" && !host.hasExitedPlanMode) {
2214
+ try {
2215
+ const taskProps = await host.connection.getTaskProperties();
2216
+ const missingProps = [];
2217
+ if (!taskProps.plan?.trim()) missingProps.push("plan (save via update_task)");
2218
+ if (!taskProps.storyPointId) missingProps.push("story points (use set_story_points)");
2219
+ if (!taskProps.title || taskProps.title === "Untitled")
2220
+ missingProps.push("title (use set_task_title)");
2221
+ if (missingProps.length > 0) {
1743
2222
  return {
1744
2223
  behavior: "deny",
1745
- message: "Destructive operation blocked. Use safer alternatives."
2224
+ message: [
2225
+ "Cannot exit plan mode yet. Required task properties are missing:",
2226
+ ...missingProps.map((p) => `- ${p}`),
2227
+ "",
2228
+ "Fill these in using MCP tools, then try ExitPlanMode again."
2229
+ ].join("\n")
1746
2230
  };
1747
2231
  }
1748
- }
1749
- return { behavior: "allow", updatedInput: input };
1750
- }
1751
- if (isPmPlanning && PM_PLAN_FILE_TOOLS.has(toolName)) {
1752
- const filePath = String(input.file_path ?? input.path ?? "");
1753
- if (filePath.includes(".claude/plans/")) {
2232
+ await host.connection.triggerIdentification();
2233
+ host.hasExitedPlanMode = true;
2234
+ const newMode = host.isParentTask ? "review" : "building";
2235
+ host.pendingModeRestart = true;
2236
+ if (host.onModeTransition) {
2237
+ host.onModeTransition(newMode);
2238
+ }
1754
2239
  return { behavior: "allow", updatedInput: input };
2240
+ } catch (err) {
2241
+ return {
2242
+ behavior: "deny",
2243
+ message: `Identification failed: ${err instanceof Error ? err.message : String(err)}. Fix the issue and try again.`
2244
+ };
1755
2245
  }
1756
- return {
1757
- behavior: "deny",
1758
- message: "File write tools are only available for plan files in PM mode."
1759
- };
1760
2246
  }
1761
- if (isPmActive && toolName === "Bash") {
1762
- const cmd = String(input.command ?? "");
1763
- if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2247
+ if (toolName === "AskUserQuestion") {
2248
+ const questions = input.questions;
2249
+ const requestId = randomUUID();
2250
+ host.connection.emitStatus("waiting_for_input");
2251
+ host.connection.sendEvent({
2252
+ type: "tool_use",
2253
+ tool: "AskUserQuestion",
2254
+ input: JSON.stringify(input)
2255
+ });
2256
+ const answerPromise = host.connection.askUserQuestion(requestId, questions);
2257
+ const timeoutPromise = new Promise((resolve2) => {
2258
+ setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
2259
+ });
2260
+ const answers = await Promise.race([answerPromise, timeoutPromise]);
2261
+ host.connection.emitStatus("running");
2262
+ if (!answers) {
1764
2263
  return {
1765
2264
  behavior: "deny",
1766
- message: "Destructive operation blocked in active mode. Use safer alternatives."
2265
+ message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
1767
2266
  };
1768
2267
  }
2268
+ return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
2269
+ }
2270
+ switch (host.agentMode) {
2271
+ case "discovery":
2272
+ return handleDiscoveryToolAccess(toolName, input);
2273
+ case "building":
2274
+ return handleBuildingToolAccess(toolName, input);
2275
+ case "review":
2276
+ return handleReviewToolAccess(toolName, input, host.isParentTask);
2277
+ case "auto":
2278
+ return handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
2279
+ default:
2280
+ return { behavior: "allow", updatedInput: input };
1769
2281
  }
1770
- if (toolName !== "AskUserQuestion") {
1771
- return { behavior: "allow", updatedInput: input };
1772
- }
1773
- const questions = input.questions;
1774
- const requestId = randomUUID();
1775
- host.connection.emitStatus("waiting_for_input");
1776
- host.connection.sendEvent({
1777
- type: "tool_use",
1778
- tool: "AskUserQuestion",
1779
- input: JSON.stringify(input)
1780
- });
1781
- const answerPromise = host.connection.askUserQuestion(requestId, questions);
1782
- const timeoutPromise = new Promise((resolve2) => {
1783
- setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
1784
- });
1785
- const answers = await Promise.race([answerPromise, timeoutPromise]);
1786
- host.connection.emitStatus("running");
1787
- if (!answers) {
1788
- return {
1789
- behavior: "deny",
1790
- message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
1791
- };
1792
- }
1793
- return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
1794
2282
  };
1795
2283
  }
1796
2284
  function buildQueryOptions(host, context) {
1797
2285
  const settings = context.agentSettings ?? host.config.agentSettings ?? {};
1798
- const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
1799
- const shouldSandbox = isPmActive && context.useSandbox !== false;
2286
+ const mode = host.agentMode;
2287
+ const isActiveMode = mode === "building" || mode === "review" || mode === "auto" && host.hasExitedPlanMode;
2288
+ const shouldSandbox = host.config.mode === "pm" && isActiveMode && context.useSandbox !== false;
1800
2289
  const systemPromptText = buildSystemPrompt(
1801
2290
  host.config.mode,
1802
2291
  context,
1803
2292
  host.config,
1804
2293
  host.setupLog,
1805
- isPmActive ? "active" : "planning"
2294
+ mode
1806
2295
  );
1807
2296
  const conveyorMcp = createConveyorMcpServer(host.connection, host.config, context);
1808
- const isPm = host.config.mode === "pm";
1809
- const pmDisallowedTools = isPm && !isPmActive ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
1810
- const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
2297
+ const isReadOnlyMode = mode === "discovery" || mode === "help" || mode === "auto" && !host.hasExitedPlanMode;
2298
+ const modeDisallowedTools = isReadOnlyMode ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
2299
+ const disallowedTools = [...settings.disallowedTools ?? [], ...modeDisallowedTools];
1811
2300
  const settingSources = settings.settingSources ?? ["user", "project"];
1812
2301
  const hooks = {
1813
2302
  PostToolUse: [
@@ -1920,10 +2409,10 @@ function buildMultimodalPrompt(textPrompt, context, skipImages = false) {
1920
2409
  }
1921
2410
  async function runSdkQuery(host, context, followUpContent) {
1922
2411
  if (host.isStopped()) return;
1923
- const isPm = host.config.mode === "pm";
1924
- const isPackRunner = isPm && !!host.config.isAuto;
1925
- const isPmPlanning = isPm && host.activeMode === "planning" && !isPackRunner;
1926
- if (isPmPlanning) {
2412
+ const mode = host.agentMode;
2413
+ const isPmMode = host.config.mode === "pm";
2414
+ const isDiscoveryLike = mode === "discovery" || mode === "help";
2415
+ if (isDiscoveryLike) {
1927
2416
  host.snapshotPlanFiles();
1928
2417
  }
1929
2418
  const options = buildQueryOptions(host, context);
@@ -1933,14 +2422,14 @@ async function runSdkQuery(host, context, followUpContent) {
1933
2422
  const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
1934
2423
  (b) => b.type === "image"
1935
2424
  );
1936
- const textPrompt = isPm ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto)}
2425
+ const textPrompt = isPmMode ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto, mode)}
1937
2426
 
1938
2427
  ---
1939
2428
 
1940
2429
  The team says:
1941
2430
  ${followUpText}` : followUpText;
1942
2431
  let prompt;
1943
- if (isPm) {
2432
+ if (isPmMode) {
1944
2433
  prompt = buildMultimodalPrompt(textPrompt, context);
1945
2434
  if (followUpImages.length > 0 && Array.isArray(prompt)) {
1946
2435
  prompt.push(...followUpImages);
@@ -1955,10 +2444,10 @@ ${followUpText}` : followUpText;
1955
2444
  options: { ...options, resume }
1956
2445
  });
1957
2446
  await runWithRetry(agentQuery, context, host, options);
1958
- } else if (isPmPlanning) {
2447
+ } else if (isDiscoveryLike) {
1959
2448
  return;
1960
2449
  } else {
1961
- const initialPrompt = buildInitialPrompt(host.config.mode, context, host.config.isAuto);
2450
+ const initialPrompt = buildInitialPrompt(host.config.mode, context, host.config.isAuto, mode);
1962
2451
  const prompt = buildMultimodalPrompt(initialPrompt, context);
1963
2452
  const agentQuery = query({
1964
2453
  prompt: host.createInputStream(prompt),
@@ -1966,7 +2455,7 @@ ${followUpText}` : followUpText;
1966
2455
  });
1967
2456
  await runWithRetry(agentQuery, context, host, options);
1968
2457
  }
1969
- if (isPmPlanning) {
2458
+ if (isDiscoveryLike) {
1970
2459
  host.syncPlanFile();
1971
2460
  }
1972
2461
  }
@@ -1981,7 +2470,7 @@ async function runWithRetry(initialQuery, context, host, options) {
1981
2470
  );
1982
2471
  }
1983
2472
  const retryPrompt = buildMultimodalPrompt(
1984
- buildInitialPrompt(host.config.mode, context, host.config.isAuto),
2473
+ buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
1985
2474
  context,
1986
2475
  lastErrorWasImage
1987
2476
  );
@@ -1991,7 +2480,12 @@ async function runWithRetry(initialQuery, context, host, options) {
1991
2480
  });
1992
2481
  })();
1993
2482
  try {
1994
- const { retriable, resultSummary } = await processEvents(agentQuery, context, host);
2483
+ const { retriable, resultSummary, modeRestart } = await processEvents(
2484
+ agentQuery,
2485
+ context,
2486
+ host
2487
+ );
2488
+ if (modeRestart) return;
1995
2489
  if (!retriable || host.isStopped()) return;
1996
2490
  lastErrorWasImage = IMAGE_ERROR_PATTERN.test(resultSummary ?? "");
1997
2491
  } catch (error) {
@@ -2001,7 +2495,7 @@ async function runWithRetry(initialQuery, context, host, options) {
2001
2495
  context.claudeSessionId = null;
2002
2496
  host.connection.storeSessionId("");
2003
2497
  const freshPrompt = buildMultimodalPrompt(
2004
- buildInitialPrompt(host.config.mode, context, host.config.isAuto),
2498
+ buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2005
2499
  context
2006
2500
  );
2007
2501
  const freshQuery = query({
@@ -2187,7 +2681,15 @@ var AgentRunner = class _AgentRunner {
2187
2681
  planSync;
2188
2682
  costTracker = new CostTracker();
2189
2683
  worktreeActive = false;
2190
- activeMode = "planning";
2684
+ agentMode = null;
2685
+ hasExitedPlanMode = false;
2686
+ pendingModeRestart = false;
2687
+ sessionIds = /* @__PURE__ */ new Map();
2688
+ lastQueryModeRestart = false;
2689
+ deferredStartConfig = null;
2690
+ startCommandStarted = false;
2691
+ idleTimer = null;
2692
+ idleCheckInterval = null;
2191
2693
  static MAX_SETUP_LOG_LINES = 50;
2192
2694
  constructor(config, callbacks) {
2193
2695
  this.config = config;
@@ -2198,6 +2700,18 @@ var AgentRunner = class _AgentRunner {
2198
2700
  get state() {
2199
2701
  return this._state;
2200
2702
  }
2703
+ /**
2704
+ * Resolve the effective AgentMode from explicit agentMode or legacy config flags.
2705
+ * This is the single axis of behavior for all execution path decisions.
2706
+ */
2707
+ get effectiveAgentMode() {
2708
+ if (this.agentMode) return this.agentMode;
2709
+ if (this.config.mode === "pm") {
2710
+ if (this.config.isAuto) return "auto";
2711
+ return "discovery";
2712
+ }
2713
+ return "building";
2714
+ }
2201
2715
  async setState(status) {
2202
2716
  this._state = status;
2203
2717
  this.connection.emitStatus(status);
@@ -2216,6 +2730,16 @@ var AgentRunner = class _AgentRunner {
2216
2730
  this.heartbeatTimer = null;
2217
2731
  }
2218
2732
  }
2733
+ clearIdleTimers() {
2734
+ if (this.idleTimer) {
2735
+ clearTimeout(this.idleTimer);
2736
+ this.idleTimer = null;
2737
+ }
2738
+ if (this.idleCheckInterval) {
2739
+ clearInterval(this.idleCheckInterval);
2740
+ this.idleCheckInterval = null;
2741
+ }
2742
+ }
2219
2743
  async start() {
2220
2744
  await this.setState("connecting");
2221
2745
  await this.connection.connect();
@@ -2223,7 +2747,8 @@ var AgentRunner = class _AgentRunner {
2223
2747
  this.connection.onChatMessage(
2224
2748
  (message) => this.injectHumanMessage(message.content, message.files)
2225
2749
  );
2226
- this.connection.onModeChange((data) => this.handleModeChange(data.mode));
2750
+ this.connection.onModeChange((data) => this.handleModeChange(data.agentMode));
2751
+ this.connection.onRunStartCommand(() => this.runDeferredStartCommand());
2227
2752
  await this.setState("connected");
2228
2753
  this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
2229
2754
  this.startHeartbeat();
@@ -2262,6 +2787,9 @@ var AgentRunner = class _AgentRunner {
2262
2787
  return;
2263
2788
  }
2264
2789
  this.taskContext._runnerSessionId = randomUUID2();
2790
+ if (this.taskContext.agentMode) {
2791
+ this.agentMode = this.taskContext.agentMode;
2792
+ }
2265
2793
  this.logEffectiveSettings();
2266
2794
  if (process.env.CODESPACES === "true") {
2267
2795
  unshallowRepo(this.config.workspaceDir);
@@ -2298,18 +2826,30 @@ var AgentRunner = class _AgentRunner {
2298
2826
  } catch {
2299
2827
  }
2300
2828
  }
2301
- const isPm = this.config.mode === "pm";
2302
- const isPackRunner = isPm && !!this.config.isAuto && !!this.taskContext.isParentTask;
2303
- if (isPackRunner) {
2304
- await this.setState("running");
2305
- await this.runQuerySafe(this.taskContext);
2306
- if (!this.stopped) await this.setState("idle");
2307
- } else if (isPm) {
2308
- await this.setState("idle");
2309
- } else {
2310
- await this.setState("running");
2311
- await this.runQuerySafe(this.taskContext);
2312
- if (!this.stopped) await this.setState("idle");
2829
+ switch (this.effectiveAgentMode) {
2830
+ case "discovery":
2831
+ case "help":
2832
+ await this.setState("idle");
2833
+ break;
2834
+ case "building":
2835
+ await this.setState("running");
2836
+ await this.runQuerySafe(this.taskContext);
2837
+ if (!this.stopped) await this.setState("idle");
2838
+ break;
2839
+ case "review":
2840
+ if (this.taskContext.isParentTask) {
2841
+ await this.setState("running");
2842
+ await this.runQuerySafe(this.taskContext);
2843
+ if (!this.stopped) await this.setState("idle");
2844
+ } else {
2845
+ await this.setState("idle");
2846
+ }
2847
+ break;
2848
+ case "auto":
2849
+ await this.setState("running");
2850
+ await this.runQuerySafe(this.taskContext);
2851
+ if (!this.stopped) await this.setState("idle");
2852
+ break;
2313
2853
  }
2314
2854
  await this.runCoreLoop();
2315
2855
  this.stopHeartbeat();
@@ -2317,8 +2857,13 @@ var AgentRunner = class _AgentRunner {
2317
2857
  this.connection.disconnect();
2318
2858
  }
2319
2859
  async runQuerySafe(context, followUp) {
2860
+ this.lastQueryModeRestart = false;
2320
2861
  try {
2321
2862
  await runSdkQuery(this.asQueryHost(), context, followUp);
2863
+ if (this.pendingModeRestart) {
2864
+ this.lastQueryModeRestart = true;
2865
+ this.pendingModeRestart = false;
2866
+ }
2322
2867
  } catch (error) {
2323
2868
  const message = error instanceof Error ? error.message : "Unknown error";
2324
2869
  this.connection.sendEvent({ type: "error", message });
@@ -2329,6 +2874,11 @@ var AgentRunner = class _AgentRunner {
2329
2874
  async runCoreLoop() {
2330
2875
  if (!this.taskContext) return;
2331
2876
  while (!this.stopped) {
2877
+ if (this.lastQueryModeRestart) {
2878
+ this.lastQueryModeRestart = false;
2879
+ await this.handleAutoModeRestart();
2880
+ continue;
2881
+ }
2332
2882
  if (this._state === "idle") {
2333
2883
  const msg = await this.waitForUserContent();
2334
2884
  if (!msg) break;
@@ -2342,6 +2892,65 @@ var AgentRunner = class _AgentRunner {
2342
2892
  }
2343
2893
  }
2344
2894
  }
2895
+ /**
2896
+ * Handle auto mode transition after ExitPlanMode.
2897
+ * Saves the current session, switches model/mode, and restarts with a fresh query.
2898
+ */
2899
+ async handleAutoModeRestart() {
2900
+ if (!this.taskContext) return;
2901
+ const currentModel = this.taskContext.model;
2902
+ const currentSessionId = this.taskContext.claudeSessionId;
2903
+ if (currentSessionId && currentModel) {
2904
+ this.sessionIds.set(currentModel, currentSessionId);
2905
+ }
2906
+ const newMode = this.agentMode;
2907
+ const isParentTask = !!this.taskContext.isParentTask;
2908
+ const newModel = this.getModelForMode(newMode, isParentTask);
2909
+ const resumeSessionId = newModel ? this.sessionIds.get(newModel) : null;
2910
+ if (resumeSessionId) {
2911
+ this.taskContext.claudeSessionId = resumeSessionId;
2912
+ } else {
2913
+ this.taskContext.claudeSessionId = null;
2914
+ this.connection.storeSessionId("");
2915
+ }
2916
+ if (newModel) {
2917
+ this.taskContext.model = newModel;
2918
+ }
2919
+ this.taskContext.agentMode = newMode;
2920
+ this.connection.emitModeTransition({
2921
+ fromMode: "auto",
2922
+ toMode: newMode ?? "building"
2923
+ });
2924
+ this.connection.emitModeChanged(newMode);
2925
+ this.connection.postChatMessage(
2926
+ `Transitioning to **${newMode}** mode${newModel ? ` with ${newModel}` : ""}. Restarting session...`
2927
+ );
2928
+ try {
2929
+ const freshContext = await this.connection.fetchTaskContext();
2930
+ freshContext._runnerSessionId = this.taskContext._runnerSessionId;
2931
+ freshContext.claudeSessionId = this.taskContext.claudeSessionId;
2932
+ freshContext.agentMode = newMode;
2933
+ if (newModel) freshContext.model = newModel;
2934
+ this.taskContext = freshContext;
2935
+ } catch {
2936
+ }
2937
+ await this.setState("running");
2938
+ await this.runQuerySafe(this.taskContext);
2939
+ if (!this.stopped && this._state !== "error") {
2940
+ await this.setState("idle");
2941
+ }
2942
+ }
2943
+ /**
2944
+ * Get the appropriate model for a given mode.
2945
+ * Building uses the builder model (Sonnet), Discovery/Review use PM model (Opus).
2946
+ */
2947
+ getModelForMode(mode, isParentTask) {
2948
+ if (!this.taskContext) return null;
2949
+ if (mode === "building" && !isParentTask) {
2950
+ return this.taskContext.builderModel ?? this.taskContext.model;
2951
+ }
2952
+ return this.taskContext.pmModel ?? this.taskContext.model;
2953
+ }
2345
2954
  async runSetupSafe() {
2346
2955
  await this.setState("setup");
2347
2956
  const ports = await loadForwardPorts(this.config.workspaceDir);
@@ -2363,7 +2972,8 @@ var AgentRunner = class _AgentRunner {
2363
2972
  await this.executeSetupConfig(config);
2364
2973
  const setupEvent = {
2365
2974
  type: "setup_complete",
2366
- previewPort: config.previewPort ?? void 0
2975
+ previewPort: config.previewPort ?? void 0,
2976
+ startCommandDeferred: this.deferredStartConfig !== null ? true : void 0
2367
2977
  };
2368
2978
  this.connection.sendEvent(setupEvent);
2369
2979
  await this.callbacks.onEvent(setupEvent);
@@ -2415,12 +3025,33 @@ The agent cannot start until this is resolved.`
2415
3025
  this.pushSetupLog("(exit 0)");
2416
3026
  }
2417
3027
  if (config.startCommand) {
2418
- this.pushSetupLog(`$ ${config.startCommand} & (background)`);
2419
- runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
2420
- this.connection.sendEvent({ type: "start_command_output", stream, data });
2421
- });
3028
+ if (this.effectiveAgentMode === "auto") {
3029
+ this.deferredStartConfig = config;
3030
+ this.pushSetupLog(`[conveyor] startCommand deferred (auto mode)`);
3031
+ } else {
3032
+ this.pushSetupLog(`$ ${config.startCommand} & (background)`);
3033
+ runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
3034
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
3035
+ });
3036
+ }
2422
3037
  }
2423
3038
  }
3039
+ runDeferredStartCommand() {
3040
+ if (!this.deferredStartConfig?.startCommand || this.startCommandStarted) return;
3041
+ this.startCommandStarted = true;
3042
+ const config = this.deferredStartConfig;
3043
+ this.deferredStartConfig = null;
3044
+ this.pushSetupLog(`$ ${config.startCommand} & (background, deferred)`);
3045
+ this.connection.sendEvent({ type: "start_command_started" });
3046
+ runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
3047
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
3048
+ });
3049
+ const setupEvent = {
3050
+ type: "setup_complete",
3051
+ previewPort: config.previewPort ?? void 0
3052
+ };
3053
+ this.connection.sendEvent(setupEvent);
3054
+ }
2424
3055
  injectHumanMessage(content, files) {
2425
3056
  let messageContent;
2426
3057
  if (files?.length) {
@@ -2469,17 +3100,17 @@ ${f.content}
2469
3100
  }
2470
3101
  }
2471
3102
  waitForMessage() {
3103
+ this.clearIdleTimers();
2472
3104
  return new Promise((resolve2) => {
2473
- const checkStopped = setInterval(() => {
3105
+ this.idleCheckInterval = setInterval(() => {
2474
3106
  if (this.stopped) {
2475
- clearInterval(checkStopped);
2476
- clearTimeout(idleTimer);
3107
+ this.clearIdleTimers();
2477
3108
  this.inputResolver = null;
2478
3109
  resolve2(null);
2479
3110
  }
2480
3111
  }, 1e3);
2481
- const idleTimer = setTimeout(() => {
2482
- clearInterval(checkStopped);
3112
+ this.idleTimer = setTimeout(() => {
3113
+ this.clearIdleTimers();
2483
3114
  this.inputResolver = null;
2484
3115
  console.log(
2485
3116
  `[conveyor-agent] Idle for ${IDLE_TIMEOUT_MS / 6e4} minutes \u2014 shutting down.`
@@ -2490,8 +3121,7 @@ ${f.content}
2490
3121
  resolve2(null);
2491
3122
  }, IDLE_TIMEOUT_MS);
2492
3123
  this.inputResolver = (msg) => {
2493
- clearInterval(checkStopped);
2494
- clearTimeout(idleTimer);
3124
+ this.clearIdleTimers();
2495
3125
  resolve2(msg);
2496
3126
  };
2497
3127
  });
@@ -2517,48 +3147,84 @@ ${f.content}
2517
3147
  parent_tool_use_id: null
2518
3148
  });
2519
3149
  yield makeUserMessage(initialPrompt);
2520
- while (!this.stopped) {
2521
- if (this.pendingMessages.length > 0) {
2522
- const next = this.pendingMessages.shift();
2523
- if (next) yield next;
2524
- continue;
3150
+ try {
3151
+ while (!this.stopped) {
3152
+ if (this.pendingMessages.length > 0) {
3153
+ const next = this.pendingMessages.shift();
3154
+ if (next) yield next;
3155
+ continue;
3156
+ }
3157
+ this.connection.emitStatus("waiting_for_input");
3158
+ await this.callbacks.onStatusChange("waiting_for_input");
3159
+ const msg = await this.waitForMessage();
3160
+ if (!msg) break;
3161
+ this.connection.emitStatus("running");
3162
+ await this.callbacks.onStatusChange("running");
3163
+ yield msg;
2525
3164
  }
2526
- this.connection.emitStatus("waiting_for_input");
2527
- await this.callbacks.onStatusChange("waiting_for_input");
2528
- const msg = await this.waitForMessage();
2529
- if (!msg) break;
2530
- this.connection.emitStatus("running");
2531
- await this.callbacks.onStatusChange("running");
2532
- yield msg;
3165
+ } finally {
3166
+ this.clearIdleTimers();
2533
3167
  }
2534
3168
  }
2535
3169
  asQueryHost() {
2536
- const getActiveMode = () => this.activeMode;
3170
+ const getEffectiveAgentMode = () => this.effectiveAgentMode;
3171
+ const getHasExitedPlanMode = () => this.hasExitedPlanMode;
3172
+ const setHasExitedPlanMode = (val) => {
3173
+ this.hasExitedPlanMode = val;
3174
+ };
3175
+ const getPendingModeRestart = () => this.pendingModeRestart;
3176
+ const setPendingModeRestart = (val) => {
3177
+ this.pendingModeRestart = val;
3178
+ };
3179
+ const getIsParentTask = () => !!this.taskContext?.isParentTask;
2537
3180
  return {
2538
3181
  config: this.config,
2539
3182
  connection: this.connection,
2540
3183
  callbacks: this.callbacks,
2541
3184
  setupLog: this.setupLog,
2542
3185
  costTracker: this.costTracker,
2543
- get activeMode() {
2544
- return getActiveMode();
3186
+ get agentMode() {
3187
+ return getEffectiveAgentMode();
3188
+ },
3189
+ get isParentTask() {
3190
+ return getIsParentTask();
3191
+ },
3192
+ get hasExitedPlanMode() {
3193
+ return getHasExitedPlanMode();
3194
+ },
3195
+ set hasExitedPlanMode(val) {
3196
+ setHasExitedPlanMode(val);
2545
3197
  },
3198
+ get pendingModeRestart() {
3199
+ return getPendingModeRestart();
3200
+ },
3201
+ set pendingModeRestart(val) {
3202
+ setPendingModeRestart(val);
3203
+ },
3204
+ sessionIds: this.sessionIds,
2546
3205
  isStopped: () => this.stopped,
2547
3206
  createInputStream: (prompt) => this.createInputStream(prompt),
2548
3207
  snapshotPlanFiles: () => this.planSync.snapshotPlanFiles(),
2549
- syncPlanFile: () => this.planSync.syncPlanFile()
3208
+ syncPlanFile: () => this.planSync.syncPlanFile(),
3209
+ onModeTransition: (newMode) => {
3210
+ this.agentMode = newMode;
3211
+ }
2550
3212
  };
2551
3213
  }
2552
- handleModeChange(mode) {
3214
+ handleModeChange(newAgentMode) {
2553
3215
  if (this.config.mode !== "pm") return;
2554
- this.activeMode = mode;
2555
- this.connection.emitModeChanged(mode);
3216
+ if (newAgentMode) {
3217
+ this.agentMode = newAgentMode;
3218
+ }
3219
+ const effectiveMode = this.effectiveAgentMode;
3220
+ this.connection.emitModeChanged(effectiveMode);
2556
3221
  this.connection.postChatMessage(
2557
- `Mode switched to **${mode}**${mode === "active" ? " \u2014 I now have direct coding access." : " \u2014 back to planning only."}`
3222
+ `Mode switched to **${effectiveMode}**${effectiveMode === "building" ? " \u2014 I now have direct coding access." : ""}`
2558
3223
  );
2559
3224
  }
2560
3225
  stop() {
2561
3226
  this.stopped = true;
3227
+ this.clearIdleTimers();
2562
3228
  if (this.inputResolver) {
2563
3229
  this.inputResolver(null);
2564
3230
  this.inputResolver = null;
@@ -2811,6 +3477,7 @@ var ProjectRunner = class {
2811
3477
  CONVEYOR_MODE: mode,
2812
3478
  CONVEYOR_WORKSPACE: workDir,
2813
3479
  CONVEYOR_USE_WORKTREE: "false",
3480
+ CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
2814
3481
  CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
2815
3482
  CONVEYOR_USE_SANDBOX: useSandbox === false ? "false" : "true"
2816
3483
  },
@@ -2990,4 +3657,4 @@ export {
2990
3657
  ProjectRunner,
2991
3658
  FileCache
2992
3659
  };
2993
- //# sourceMappingURL=chunk-6Z262BNH.js.map
3660
+ //# sourceMappingURL=chunk-Q2LN2YBW.js.map