@rallycry/conveyor-agent 3.8.0 → 4.0.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.
@@ -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;
@@ -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");
@@ -347,6 +356,66 @@ var ConveyorConnection = class _ConveyorConnection {
347
356
  );
348
357
  });
349
358
  }
359
+ updateTaskProperties(data) {
360
+ if (!this.socket) throw new Error("Not connected");
361
+ this.socket.emit("agentRunner:updateTaskProperties", data);
362
+ }
363
+ listIcons() {
364
+ const socket = this.socket;
365
+ if (!socket) throw new Error("Not connected");
366
+ return new Promise((resolve2, reject) => {
367
+ socket.emit("agentRunner:listIcons", {}, (response) => {
368
+ if (response.success && response.data) resolve2(response.data);
369
+ else reject(new Error(response.error ?? "Failed to list icons"));
370
+ });
371
+ });
372
+ }
373
+ generateTaskIcon(prompt, aspectRatio) {
374
+ const socket = this.socket;
375
+ if (!socket) throw new Error("Not connected");
376
+ return new Promise((resolve2, reject) => {
377
+ socket.emit(
378
+ "agentRunner:generateTaskIcon",
379
+ { prompt, aspectRatio },
380
+ (response) => {
381
+ if (response.success && response.data) resolve2(response.data);
382
+ else reject(new Error(response.error ?? "Failed to generate icon"));
383
+ }
384
+ );
385
+ });
386
+ }
387
+ getTaskProperties() {
388
+ const socket = this.socket;
389
+ if (!socket) throw new Error("Not connected");
390
+ return new Promise((resolve2, reject) => {
391
+ socket.emit(
392
+ "agentRunner:getTaskProperties",
393
+ {},
394
+ (response) => {
395
+ if (response.success && response.data) resolve2(response.data);
396
+ else reject(new Error(response.error ?? "Failed to get task properties"));
397
+ }
398
+ );
399
+ });
400
+ }
401
+ triggerIdentification() {
402
+ const socket = this.socket;
403
+ if (!socket) throw new Error("Not connected");
404
+ return new Promise((resolve2, reject) => {
405
+ socket.emit(
406
+ "agentRunner:triggerIdentification",
407
+ {},
408
+ (response) => {
409
+ if (response.success && response.data) resolve2(response.data);
410
+ else reject(new Error(response.error ?? "Identification failed"));
411
+ }
412
+ );
413
+ });
414
+ }
415
+ emitModeTransition(payload) {
416
+ if (!this.socket) return;
417
+ this.socket.emit("agentRunner:modeTransition", payload);
418
+ }
350
419
  disconnect() {
351
420
  this.flushEvents();
352
421
  this.socket?.disconnect();
@@ -799,6 +868,13 @@ async function processEvents(events, context, host) {
799
868
  const turnToolCalls = [];
800
869
  for await (const event of events) {
801
870
  if (host.isStopped()) break;
871
+ if (host.pendingModeRestart) {
872
+ host.pendingModeRestart = false;
873
+ if (isTyping) {
874
+ host.connection.sendTypingStop();
875
+ }
876
+ return { retriable: false, modeRestart: true };
877
+ }
802
878
  switch (event.type) {
803
879
  case "system": {
804
880
  const systemEvent = event;
@@ -1007,12 +1083,22 @@ ${context.plan}`);
1007
1083
  }
1008
1084
  return parts;
1009
1085
  }
1010
- function buildInstructions(mode, context, scenario) {
1086
+ function buildInstructions(mode, context, scenario, agentMode) {
1011
1087
  const parts = [`
1012
1088
  ## Instructions`];
1013
1089
  const isPm = mode === "pm";
1090
+ const isAutoMode = agentMode === "auto";
1014
1091
  if (scenario === "fresh") {
1015
- if (isPm && context.isParentTask) {
1092
+ if (isAutoMode && isPm) {
1093
+ parts.push(
1094
+ `You are operating autonomously. Begin planning immediately.`,
1095
+ `1. Explore the codebase to understand the architecture and relevant files`,
1096
+ `2. Draft a clear implementation plan and save it with update_task`,
1097
+ `3. Set story points (set_story_points), tags (set_task_tags), and title (set_task_title)`,
1098
+ `4. When the plan and all required properties are set, call ExitPlanMode to transition to building`,
1099
+ `Do NOT wait for team input \u2014 proceed autonomously.`
1100
+ );
1101
+ } else if (isPm && context.isParentTask) {
1016
1102
  parts.push(
1017
1103
  `You are the project manager for this task and its subtasks.`,
1018
1104
  `Use list_subtasks to review the current state of child tasks.`,
@@ -1091,27 +1177,155 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
1091
1177
  }
1092
1178
  return parts;
1093
1179
  }
1180
+ function buildPropertyInstructions(context) {
1181
+ const parts = [];
1182
+ parts.push(
1183
+ ``,
1184
+ `### Proactive Property Management`,
1185
+ `As you plan this task, proactively fill in task properties when you have enough context:`,
1186
+ `- Once you understand the scope, use set_story_points to assign a value`,
1187
+ `- Use set_task_tags to categorize the work`,
1188
+ `- For icons: FIRST call list_icons to check for existing matches. Use set_task_icon if one fits.`,
1189
+ ` Only call generate_task_icon if no existing icon is a good fit.`,
1190
+ `- Use set_task_title if the current title doesn't accurately reflect the plan`,
1191
+ ``,
1192
+ `Don't wait for the user to ask \u2014 fill these in naturally as the plan takes shape.`,
1193
+ `If the user adjusts the plan significantly, update the properties to match.`
1194
+ );
1195
+ if (context.storyPoints && context.storyPoints.length > 0) {
1196
+ parts.push(``, `Available story point tiers:`);
1197
+ for (const sp of context.storyPoints) {
1198
+ const desc = sp.description ? ` \u2014 ${sp.description}` : "";
1199
+ parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
1200
+ }
1201
+ }
1202
+ if (context.projectTags && context.projectTags.length > 0) {
1203
+ parts.push(``, `Available project tags:`);
1204
+ for (const tag of context.projectTags) {
1205
+ parts.push(`- ID: "${tag.id}", Name: "${tag.name}"`);
1206
+ }
1207
+ }
1208
+ return parts;
1209
+ }
1210
+ function buildModePrompt(agentMode, context) {
1211
+ switch (agentMode) {
1212
+ case "discovery": {
1213
+ const parts = [
1214
+ `
1215
+ ## Mode: Discovery`,
1216
+ `You are in Discovery mode \u2014 helping plan and scope this task.`,
1217
+ `- You have read-only codebase access (can read files, run git commands, search code)`,
1218
+ `- You have MCP tools: update_task (title, description, plan, tags, SP, icon)`,
1219
+ `- You can create and manage subtasks`,
1220
+ `- You cannot write code or edit files (except .claude/plans/)`,
1221
+ `- Goal: collaborate with the user to create a clear plan`,
1222
+ `- Proactively fill task properties (SP, tags, icon) as the plan takes shape`
1223
+ ];
1224
+ if (context) {
1225
+ parts.push(...buildPropertyInstructions(context));
1226
+ }
1227
+ return parts.join("\n");
1228
+ }
1229
+ case "building":
1230
+ return [
1231
+ `
1232
+ ## Mode: Building`,
1233
+ `You are in Building mode \u2014 executing the plan.`,
1234
+ `- You have full coding access (read, write, edit, bash, git)`,
1235
+ `- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
1236
+ `- If this is a leaf task (no children): execute the plan directly`,
1237
+ `- Goal: implement the plan, run tests, open a PR when done`
1238
+ ].join("\n");
1239
+ case "review":
1240
+ return [
1241
+ `
1242
+ ## Mode: Review`,
1243
+ `You are in Review mode \u2014 reviewing and coordinating.`,
1244
+ `- You have read-only access plus light edit capability (can suggest fixes, run tests, check linting)`,
1245
+ `- For parent tasks: you can manage children, review child PRs, fire next child builds`,
1246
+ `- You have Pack Runner coordination tools (list_subtasks, fire builds, approve PRs)`,
1247
+ `- Goal: ensure quality, provide feedback, coordinate progression`
1248
+ ].join("\n");
1249
+ case "auto": {
1250
+ const parts = [
1251
+ `
1252
+ ## Mode: Auto`,
1253
+ `You are in Auto mode \u2014 operating autonomously through planning \u2192 building \u2192 PR.`,
1254
+ ``,
1255
+ `### Phase 1: Discovery & Planning (current)`,
1256
+ `- You have read-only codebase access (can read files, run git commands, search code)`,
1257
+ `- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
1258
+ `- You have MCP tools for task properties: update_task, set_story_points, set_task_tags, set_task_icon, set_task_title`,
1259
+ ``,
1260
+ `### Required before transitioning:`,
1261
+ `Before calling ExitPlanMode, you MUST fill in ALL of these:`,
1262
+ `1. **Plan** \u2014 Save a clear implementation plan using update_task`,
1263
+ `2. **Story Points** \u2014 Assign via set_story_points`,
1264
+ `3. **Title** \u2014 Set an accurate title via set_task_title (if the current one is vague or "Untitled")`,
1265
+ ``,
1266
+ `### Transitioning to Building:`,
1267
+ `When your plan is complete and all required properties are set, call the **ExitPlanMode** tool.`,
1268
+ `- If any required properties are missing, ExitPlanMode will be denied with details on what's missing`,
1269
+ `- Once ExitPlanMode succeeds, the system will automatically restart your session in Building mode with the appropriate model`,
1270
+ `- You do NOT need to do anything after calling ExitPlanMode \u2014 the transition is handled for you`,
1271
+ ``,
1272
+ `### Autonomous Guidelines:`,
1273
+ `- Make decisions independently \u2014 do not ask the team for approval at each step`,
1274
+ `- Only escalate when genuinely blocked (ambiguous requirements, missing access, conflicting instructions)`,
1275
+ `- Be thorough in discovery: read relevant files, understand the codebase architecture, then plan`
1276
+ ];
1277
+ if (context) {
1278
+ parts.push(...buildPropertyInstructions(context));
1279
+ }
1280
+ return parts.join("\n");
1281
+ }
1282
+ default:
1283
+ return null;
1284
+ }
1285
+ }
1094
1286
  function buildPackRunnerSystemPrompt(context, config, setupLog) {
1095
1287
  const parts = [
1096
1288
  `You are an autonomous Pack Runner managing child tasks for the "${context.title}" project.`,
1097
1289
  `You are running locally with full access to the repository and task management tools.`,
1098
1290
  `Your job is to sequentially execute child tasks by firing cloud builds, reviewing their PRs, and merging them.`,
1099
1291
  ``,
1292
+ `## Child Task Status Lifecycle`,
1293
+ `- "Planning" \u2014 Not ready for execution. Skip it (or escalate if blocking).`,
1294
+ `- "Open" \u2014 Ready to execute. Use start_child_cloud_build to fire it.`,
1295
+ `- "InProgress" \u2014 Currently being worked on by a Task Runner. Wait \u2014 it will move to ReviewPR when done.`,
1296
+ `- "ReviewPR" \u2014 Task Runner finished and opened a PR. Review and merge it.`,
1297
+ `- "ReviewDev" \u2014 PR was merged to dev. This child is complete. Move on.`,
1298
+ `- "Complete" \u2014 Fully done. Move on.`,
1299
+ ``,
1100
1300
  `## Autonomous Loop`,
1101
- `Follow this loop continuously until all children are complete:`,
1301
+ `Follow this loop each time you are launched or relaunched:`,
1302
+ ``,
1102
1303
  `1. Call list_subtasks to see the current state of all child tasks.`,
1103
- `2. If any child is in "ReviewPR" status: review the PR (use get_task to read the child, check quality), then call approve_and_merge_pr to merge it. If CI/CD is failing, post guidance to the child's chat with post_to_chat, or notify the team if stuck.`,
1104
- `3. Find the next child in "Open" status (by ordinal order) and fire its cloud build with start_child_cloud_build.`,
1105
- `4. Report to chat which task you fired, then state you are going idle to wait. The system will relaunch you when the child completes.`,
1106
- `5. When ALL children are complete (no "Open" or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with update_task_status.`,
1304
+ ` The response includes PR info (githubPRNumber, githubPRUrl) and agent assignment (agentId).`,
1305
+ ``,
1306
+ `2. Evaluate each child by status (in ordinal order):`,
1307
+ ` - "ReviewPR": Review and merge its PR with approve_and_merge_pr.`,
1308
+ ` - If merge fails due to pending CI: post a status update to chat, state you are going idle, and the system will relaunch you to try again.`,
1309
+ ` - If merge fails due to failed CI: use get_task_cli(childTaskId) to check what went wrong. Escalate to the team in chat.`,
1310
+ ` - "InProgress": A Task Runner is actively working on this child. Do nothing \u2014 wait for it to finish.`,
1311
+ ` - "Open": This is the next child to execute. Fire it with start_child_cloud_build.`,
1312
+ ` - If it fails because the child is missing story points or an agent: notify the team in chat and go idle.`,
1313
+ ` - "ReviewDev" / "Complete": Already done. Skip.`,
1314
+ ` - "Planning": Not ready. If this is blocking progress, notify the team.`,
1315
+ ``,
1316
+ `3. After merging a PR: run \`git pull origin ${context.baseBranch}\` to get the merged changes before firing the next child.`,
1317
+ ``,
1318
+ `4. After firing a child build: report which task you fired to chat, then explicitly state you are going idle. The system will relaunch you when the child completes or changes status.`,
1319
+ ``,
1320
+ `5. When ALL children are in "ReviewDev" or "Complete" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with update_task_status("Complete").`,
1107
1321
  ``,
1108
1322
  `## Important Rules`,
1109
1323
  `- Process children ONE at a time, in ordinal order.`,
1110
- `- After firing a child build, explicitly state you are going idle. The system will disconnect you until the child completes.`,
1324
+ `- After firing a child build OR when waiting on CI, explicitly state you are going idle. The system will disconnect you and relaunch when there's a status change.`,
1111
1325
  `- Do NOT attempt to write code yourself. Your role is coordination only.`,
1112
- `- If a child task is missing story points or an assigned agent, notify the team in chat and wait.`,
1113
- `- If a child fails repeatedly, escalate to the team rather than retrying indefinitely.`,
1114
- `- You can use get_task and get_task_cli to inspect child task details and logs.`,
1326
+ `- If a child is stuck in "InProgress" for an unusually long time, use get_task_cli(childTaskId) to check its logs and escalate to the team if it appears stuck.`,
1327
+ `- You can use get_task(childTaskId) to get a child's full details including PR URL and branch.`,
1328
+ `- list_subtasks returns PR info (githubPRNumber, githubPRUrl) and agent assignment (agentId) for each child \u2014 use this to verify readiness before firing builds.`,
1115
1329
  `- You can use read_task_chat to check for team messages.`
1116
1330
  ];
1117
1331
  if (context.storyPoints && context.storyPoints.length > 0) {
@@ -1159,10 +1373,11 @@ function buildPackRunnerInstructions(context, scenario) {
1159
1373
  );
1160
1374
  } else if (scenario === "idle_relaunch") {
1161
1375
  parts.push(
1162
- `You have been relaunched \u2014 a child task may have completed.`,
1376
+ `You have been relaunched \u2014 a child task likely changed status (completed work, opened a PR, or was merged).`,
1163
1377
  `Call list_subtasks to check the current state of all children.`,
1164
- `If a child is in "ReviewPR" status, review and merge its PR.`,
1165
- `Then continue your autonomous loop with the next "Open" child.`
1378
+ `Look for children in "ReviewPR" status first \u2014 review and merge their PRs.`,
1379
+ `If a child you previously fired is now in "ReviewDev", it was merged. Pull latest with \`git pull origin ${context.baseBranch}\` and continue to the next "Open" child.`,
1380
+ `If no children need action (all InProgress or waiting), state you are going idle.`
1166
1381
  );
1167
1382
  } else {
1168
1383
  const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
@@ -1173,12 +1388,12 @@ function buildPackRunnerInstructions(context, scenario) {
1173
1388
  New messages since your last run:`,
1174
1389
  ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
1175
1390
  `
1176
- Review these messages, then continue your autonomous loop: call list_subtasks and proceed accordingly.`
1391
+ After addressing the feedback, resume your autonomous loop: call list_subtasks and proceed accordingly.`
1177
1392
  );
1178
1393
  }
1179
1394
  return parts;
1180
1395
  }
1181
- function buildInitialPrompt(mode, context, isAuto) {
1396
+ function buildInitialPrompt(mode, context, isAuto, agentMode) {
1182
1397
  const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
1183
1398
  if (!isPackRunner) {
1184
1399
  const sessionRelaunch = buildRelaunchWithSession(mode, context);
@@ -1186,12 +1401,12 @@ function buildInitialPrompt(mode, context, isAuto) {
1186
1401
  }
1187
1402
  const scenario = detectRelaunchScenario(context);
1188
1403
  const body = buildTaskBody(context);
1189
- const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario);
1404
+ const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
1190
1405
  return [...body, ...instructions].join("\n");
1191
1406
  }
1192
- function buildSystemPrompt(mode, context, config, setupLog, pmSubMode = "planning") {
1407
+ function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
1193
1408
  const isPm = mode === "pm";
1194
- const isPmActive = isPm && pmSubMode === "active";
1409
+ const isPmActive = isPm && agentMode === "building";
1195
1410
  const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
1196
1411
  if (isPackRunner) {
1197
1412
  return buildPackRunnerSystemPrompt(context, config, setupLog);
@@ -1317,6 +1532,10 @@ Your responses are sent directly to the task chat \u2014 the team sees everythin
1317
1532
  `Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
1318
1533
  );
1319
1534
  }
1535
+ const modePrompt = buildModePrompt(agentMode, context);
1536
+ if (modePrompt) {
1537
+ parts.push(modePrompt);
1538
+ }
1320
1539
  return parts.join("\n");
1321
1540
  }
1322
1541
 
@@ -1599,7 +1818,7 @@ function buildPmTools(connection, storyPoints, options) {
1599
1818
  ),
1600
1819
  tool2(
1601
1820
  "list_subtasks",
1602
- "List all subtasks under the current parent task",
1821
+ "List all subtasks under the current parent task. Returns status, PR info (githubPRNumber, githubPRUrl), agent assignment (agentId), and plan for each child.",
1603
1822
  {},
1604
1823
  async () => {
1605
1824
  try {
@@ -1634,7 +1853,7 @@ function buildPmTools(connection, storyPoints, options) {
1634
1853
  ),
1635
1854
  tool2(
1636
1855
  "approve_and_merge_pr",
1637
- "Approve and merge a child task's pull request. Only succeeds if all CI/CD checks are passing. The child task must be in ReviewPR status.",
1856
+ "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.",
1638
1857
  {
1639
1858
  childTaskId: z2.string().describe("The child task ID whose PR should be approved and merged")
1640
1859
  },
@@ -1684,6 +1903,117 @@ function buildTaskTools(connection) {
1684
1903
  ];
1685
1904
  }
1686
1905
 
1906
+ // src/tools/discovery-tools.ts
1907
+ import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
1908
+ import { z as z4 } from "zod";
1909
+ function buildDiscoveryTools(connection, context) {
1910
+ const spDescription = buildStoryPointDescription(context?.storyPoints);
1911
+ return [
1912
+ tool4(
1913
+ "set_story_points",
1914
+ "Set the story point estimate for this task. Use after understanding the scope of the work.",
1915
+ { value: z4.number().describe(spDescription) },
1916
+ async ({ value }) => {
1917
+ try {
1918
+ connection.updateTaskProperties({ storyPointValue: value });
1919
+ return textResult(`Story points set to ${value}`);
1920
+ } catch (error) {
1921
+ return textResult(
1922
+ `Failed to set story points: ${error instanceof Error ? error.message : "Unknown error"}`
1923
+ );
1924
+ }
1925
+ }
1926
+ ),
1927
+ tool4(
1928
+ "set_task_tags",
1929
+ "Assign tags to this task from the project's available tags. Use the tag IDs from the project tags list.",
1930
+ {
1931
+ tagIds: z4.array(z4.string()).describe("Array of tag IDs to assign")
1932
+ },
1933
+ async ({ tagIds }) => {
1934
+ try {
1935
+ connection.updateTaskProperties({ tagIds });
1936
+ return textResult(`Tags assigned: ${tagIds.length} tag(s)`);
1937
+ } catch (error) {
1938
+ return textResult(
1939
+ `Failed to set tags: ${error instanceof Error ? error.message : "Unknown error"}`
1940
+ );
1941
+ }
1942
+ }
1943
+ ),
1944
+ tool4(
1945
+ "set_task_title",
1946
+ "Update the task title to better reflect the planned work.",
1947
+ {
1948
+ title: z4.string().describe("The new task title")
1949
+ },
1950
+ async ({ title }) => {
1951
+ try {
1952
+ connection.updateTaskProperties({ title });
1953
+ return textResult(`Task title updated to: ${title}`);
1954
+ } catch (error) {
1955
+ return textResult(
1956
+ `Failed to update title: ${error instanceof Error ? error.message : "Unknown error"}`
1957
+ );
1958
+ }
1959
+ }
1960
+ ),
1961
+ tool4(
1962
+ "list_icons",
1963
+ "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.",
1964
+ {},
1965
+ async () => {
1966
+ try {
1967
+ const icons = await connection.listIcons();
1968
+ return textResult(JSON.stringify(icons, null, 2));
1969
+ } catch (error) {
1970
+ return textResult(
1971
+ `Failed to list icons: ${error instanceof Error ? error.message : "Unknown error"}`
1972
+ );
1973
+ }
1974
+ },
1975
+ { annotations: { readOnlyHint: true } }
1976
+ ),
1977
+ tool4(
1978
+ "set_task_icon",
1979
+ "Assign an existing icon to this task by its ID. Use list_icons first to find a matching icon.",
1980
+ {
1981
+ iconId: z4.string().describe("The icon ID to assign")
1982
+ },
1983
+ async ({ iconId }) => {
1984
+ try {
1985
+ connection.updateTaskProperties({ iconId });
1986
+ return textResult("Icon assigned to task.");
1987
+ } catch (error) {
1988
+ return textResult(
1989
+ `Failed to set icon: ${error instanceof Error ? error.message : "Unknown error"}`
1990
+ );
1991
+ }
1992
+ }
1993
+ ),
1994
+ tool4(
1995
+ "generate_task_icon",
1996
+ "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.",
1997
+ {
1998
+ prompt: z4.string().describe(
1999
+ "Description of the icon to generate (e.g. 'minimal flat gear and wrench icon')"
2000
+ ),
2001
+ aspectRatio: z4.enum(["auto", "portrait", "landscape", "square"]).optional().describe("Icon aspect ratio, defaults to square")
2002
+ },
2003
+ async ({ prompt, aspectRatio }) => {
2004
+ try {
2005
+ const result = await connection.generateTaskIcon(prompt, aspectRatio ?? "square");
2006
+ return textResult(`Icon generated and assigned: ${result.iconId}`);
2007
+ } catch (error) {
2008
+ return textResult(
2009
+ `Failed to generate icon: ${error instanceof Error ? error.message : "Unknown error"}`
2010
+ );
2011
+ }
2012
+ }
2013
+ )
2014
+ ];
2015
+ }
2016
+
1687
2017
  // src/tools/index.ts
1688
2018
  function textResult(text) {
1689
2019
  return { content: [{ type: "text", text }] };
@@ -1693,11 +2023,30 @@ function imageBlock(data, mimeType) {
1693
2023
  }
1694
2024
  function createConveyorMcpServer(connection, config, context) {
1695
2025
  const commonTools = buildCommonTools(connection, config);
1696
- const isPackRunner = config.mode === "pm" && !!config.isAuto && !!context?.isParentTask;
1697
- const modeTools = config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: isPackRunner }) : buildTaskTools(connection);
2026
+ const agentMode = context?.agentMode;
2027
+ let modeTools;
2028
+ switch (agentMode) {
2029
+ case "building":
2030
+ modeTools = buildTaskTools(connection);
2031
+ break;
2032
+ case "review":
2033
+ modeTools = buildPmTools(connection, context?.storyPoints, {
2034
+ includePackTools: !!context?.isParentTask
2035
+ });
2036
+ break;
2037
+ case "auto":
2038
+ case "discovery":
2039
+ case "help":
2040
+ modeTools = buildPmTools(connection, context?.storyPoints, { includePackTools: false });
2041
+ break;
2042
+ default:
2043
+ modeTools = config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: false }) : buildTaskTools(connection);
2044
+ break;
2045
+ }
2046
+ const discoveryTools = agentMode === "discovery" || agentMode === "auto" ? buildDiscoveryTools(connection, context) : [];
1698
2047
  return createSdkMcpServer({
1699
2048
  name: "conveyor",
1700
- tools: [...commonTools, ...modeTools]
2049
+ tools: [...commonTools, ...modeTools, ...discoveryTools]
1701
2050
  });
1702
2051
  }
1703
2052
 
@@ -1707,84 +2056,159 @@ var IMAGE_ERROR_PATTERN = /Could not process image/i;
1707
2056
  var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
1708
2057
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
1709
2058
  var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
2059
+ function handleDiscoveryToolAccess(toolName, input) {
2060
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2061
+ const filePath = String(input.file_path ?? input.path ?? "");
2062
+ if (filePath.includes(".claude/plans/")) {
2063
+ return { behavior: "allow", updatedInput: input };
2064
+ }
2065
+ return {
2066
+ behavior: "deny",
2067
+ message: "Discovery mode is read-only. File writes are restricted to plan files."
2068
+ };
2069
+ }
2070
+ return { behavior: "allow", updatedInput: input };
2071
+ }
2072
+ function handleBuildingToolAccess(toolName, input) {
2073
+ if (toolName === "Bash") {
2074
+ const cmd = String(input.command ?? "");
2075
+ if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2076
+ return {
2077
+ behavior: "deny",
2078
+ message: "Destructive operation blocked. Use safer alternatives."
2079
+ };
2080
+ }
2081
+ }
2082
+ return { behavior: "allow", updatedInput: input };
2083
+ }
2084
+ function handleReviewToolAccess(toolName, input, isParentTask) {
2085
+ if (isParentTask) {
2086
+ return handleBuildingToolAccess(toolName, input);
2087
+ }
2088
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2089
+ const filePath = String(input.file_path ?? input.path ?? "");
2090
+ if (filePath.includes(".claude/plans/")) {
2091
+ return { behavior: "allow", updatedInput: input };
2092
+ }
2093
+ return {
2094
+ behavior: "deny",
2095
+ message: "Review mode restricts file writes. Use bash to run tests and linting instead."
2096
+ };
2097
+ }
2098
+ if (toolName === "Bash") {
2099
+ const cmd = String(input.command ?? "");
2100
+ if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2101
+ return { behavior: "deny", message: "Destructive operation blocked in review mode." };
2102
+ }
2103
+ }
2104
+ return { behavior: "allow", updatedInput: input };
2105
+ }
2106
+ function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask) {
2107
+ if (hasExitedPlanMode) {
2108
+ return isParentTask ? handleReviewToolAccess(toolName, input, true) : handleBuildingToolAccess(toolName, input);
2109
+ }
2110
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
2111
+ const filePath = String(input.file_path ?? input.path ?? "");
2112
+ if (filePath.includes(".claude/plans/")) {
2113
+ return { behavior: "allow", updatedInput: input };
2114
+ }
2115
+ return {
2116
+ behavior: "deny",
2117
+ message: "You are in auto plan mode. File writes are restricted to plan files. Call ExitPlanMode when your plan is ready to start building."
2118
+ };
2119
+ }
2120
+ return { behavior: "allow", updatedInput: input };
2121
+ }
1710
2122
  function buildCanUseTool(host) {
1711
2123
  const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
1712
2124
  return async (toolName, input) => {
1713
- const isPackRunner = host.config.mode === "pm" && !!host.config.isAuto;
1714
- const isPmPlanning = host.config.mode === "pm" && host.activeMode === "planning";
1715
- const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
1716
- if (isPackRunner) {
1717
- if (toolName === "Bash") {
1718
- const cmd = String(input.command ?? "");
1719
- if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2125
+ if (toolName === "ExitPlanMode" && host.agentMode === "auto" && !host.hasExitedPlanMode) {
2126
+ try {
2127
+ const taskProps = await host.connection.getTaskProperties();
2128
+ const missingProps = [];
2129
+ if (!taskProps.plan?.trim()) missingProps.push("plan (save via update_task)");
2130
+ if (!taskProps.storyPointId) missingProps.push("story points (use set_story_points)");
2131
+ if (!taskProps.title || taskProps.title === "Untitled")
2132
+ missingProps.push("title (use set_task_title)");
2133
+ if (missingProps.length > 0) {
1720
2134
  return {
1721
2135
  behavior: "deny",
1722
- message: "Destructive operation blocked. Use safer alternatives."
2136
+ message: [
2137
+ "Cannot exit plan mode yet. Required task properties are missing:",
2138
+ ...missingProps.map((p) => `- ${p}`),
2139
+ "",
2140
+ "Fill these in using MCP tools, then try ExitPlanMode again."
2141
+ ].join("\n")
1723
2142
  };
1724
2143
  }
1725
- }
1726
- return { behavior: "allow", updatedInput: input };
1727
- }
1728
- if (isPmPlanning && PM_PLAN_FILE_TOOLS.has(toolName)) {
1729
- const filePath = String(input.file_path ?? input.path ?? "");
1730
- if (filePath.includes(".claude/plans/")) {
2144
+ await host.connection.triggerIdentification();
2145
+ host.hasExitedPlanMode = true;
2146
+ const newMode = host.isParentTask ? "review" : "building";
2147
+ host.pendingModeRestart = true;
2148
+ if (host.onModeTransition) {
2149
+ host.onModeTransition(newMode);
2150
+ }
1731
2151
  return { behavior: "allow", updatedInput: input };
2152
+ } catch (err) {
2153
+ return {
2154
+ behavior: "deny",
2155
+ message: `Identification failed: ${err instanceof Error ? err.message : String(err)}. Fix the issue and try again.`
2156
+ };
1732
2157
  }
1733
- return {
1734
- behavior: "deny",
1735
- message: "File write tools are only available for plan files in PM mode."
1736
- };
1737
2158
  }
1738
- if (isPmActive && toolName === "Bash") {
1739
- const cmd = String(input.command ?? "");
1740
- if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
2159
+ if (toolName === "AskUserQuestion") {
2160
+ const questions = input.questions;
2161
+ const requestId = randomUUID();
2162
+ host.connection.emitStatus("waiting_for_input");
2163
+ host.connection.sendEvent({
2164
+ type: "tool_use",
2165
+ tool: "AskUserQuestion",
2166
+ input: JSON.stringify(input)
2167
+ });
2168
+ const answerPromise = host.connection.askUserQuestion(requestId, questions);
2169
+ const timeoutPromise = new Promise((resolve2) => {
2170
+ setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
2171
+ });
2172
+ const answers = await Promise.race([answerPromise, timeoutPromise]);
2173
+ host.connection.emitStatus("running");
2174
+ if (!answers) {
1741
2175
  return {
1742
2176
  behavior: "deny",
1743
- message: "Destructive operation blocked in active mode. Use safer alternatives."
2177
+ message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
1744
2178
  };
1745
2179
  }
2180
+ return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
2181
+ }
2182
+ switch (host.agentMode) {
2183
+ case "discovery":
2184
+ return handleDiscoveryToolAccess(toolName, input);
2185
+ case "building":
2186
+ return handleBuildingToolAccess(toolName, input);
2187
+ case "review":
2188
+ return handleReviewToolAccess(toolName, input, host.isParentTask);
2189
+ case "auto":
2190
+ return handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
2191
+ default:
2192
+ return { behavior: "allow", updatedInput: input };
1746
2193
  }
1747
- if (toolName !== "AskUserQuestion") {
1748
- return { behavior: "allow", updatedInput: input };
1749
- }
1750
- const questions = input.questions;
1751
- const requestId = randomUUID();
1752
- host.connection.emitStatus("waiting_for_input");
1753
- host.connection.sendEvent({
1754
- type: "tool_use",
1755
- tool: "AskUserQuestion",
1756
- input: JSON.stringify(input)
1757
- });
1758
- const answerPromise = host.connection.askUserQuestion(requestId, questions);
1759
- const timeoutPromise = new Promise((resolve2) => {
1760
- setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
1761
- });
1762
- const answers = await Promise.race([answerPromise, timeoutPromise]);
1763
- host.connection.emitStatus("running");
1764
- if (!answers) {
1765
- return {
1766
- behavior: "deny",
1767
- message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
1768
- };
1769
- }
1770
- return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
1771
2194
  };
1772
2195
  }
1773
2196
  function buildQueryOptions(host, context) {
1774
2197
  const settings = context.agentSettings ?? host.config.agentSettings ?? {};
1775
- const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
1776
- const shouldSandbox = isPmActive && context.useSandbox !== false;
2198
+ const mode = host.agentMode;
2199
+ const isActiveMode = mode === "building" || mode === "review" || mode === "auto" && host.hasExitedPlanMode;
2200
+ const shouldSandbox = host.config.mode === "pm" && isActiveMode && context.useSandbox !== false;
1777
2201
  const systemPromptText = buildSystemPrompt(
1778
2202
  host.config.mode,
1779
2203
  context,
1780
2204
  host.config,
1781
2205
  host.setupLog,
1782
- isPmActive ? "active" : "planning"
2206
+ mode
1783
2207
  );
1784
2208
  const conveyorMcp = createConveyorMcpServer(host.connection, host.config, context);
1785
- const isPm = host.config.mode === "pm";
1786
- const pmDisallowedTools = isPm && !isPmActive ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
1787
- const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
2209
+ const isReadOnlyMode = mode === "discovery" || mode === "help" || mode === "auto" && !host.hasExitedPlanMode;
2210
+ const modeDisallowedTools = isReadOnlyMode ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
2211
+ const disallowedTools = [...settings.disallowedTools ?? [], ...modeDisallowedTools];
1788
2212
  const settingSources = settings.settingSources ?? ["user", "project"];
1789
2213
  const hooks = {
1790
2214
  PostToolUse: [
@@ -1897,10 +2321,10 @@ function buildMultimodalPrompt(textPrompt, context, skipImages = false) {
1897
2321
  }
1898
2322
  async function runSdkQuery(host, context, followUpContent) {
1899
2323
  if (host.isStopped()) return;
1900
- const isPm = host.config.mode === "pm";
1901
- const isPackRunner = isPm && !!host.config.isAuto;
1902
- const isPmPlanning = isPm && host.activeMode === "planning" && !isPackRunner;
1903
- if (isPmPlanning) {
2324
+ const mode = host.agentMode;
2325
+ const isPmMode = host.config.mode === "pm";
2326
+ const isDiscoveryLike = mode === "discovery" || mode === "help";
2327
+ if (isDiscoveryLike) {
1904
2328
  host.snapshotPlanFiles();
1905
2329
  }
1906
2330
  const options = buildQueryOptions(host, context);
@@ -1910,14 +2334,14 @@ async function runSdkQuery(host, context, followUpContent) {
1910
2334
  const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
1911
2335
  (b) => b.type === "image"
1912
2336
  );
1913
- const textPrompt = isPm ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto)}
2337
+ const textPrompt = isPmMode ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto, mode)}
1914
2338
 
1915
2339
  ---
1916
2340
 
1917
2341
  The team says:
1918
2342
  ${followUpText}` : followUpText;
1919
2343
  let prompt;
1920
- if (isPm) {
2344
+ if (isPmMode) {
1921
2345
  prompt = buildMultimodalPrompt(textPrompt, context);
1922
2346
  if (followUpImages.length > 0 && Array.isArray(prompt)) {
1923
2347
  prompt.push(...followUpImages);
@@ -1932,10 +2356,10 @@ ${followUpText}` : followUpText;
1932
2356
  options: { ...options, resume }
1933
2357
  });
1934
2358
  await runWithRetry(agentQuery, context, host, options);
1935
- } else if (isPmPlanning) {
2359
+ } else if (isDiscoveryLike) {
1936
2360
  return;
1937
2361
  } else {
1938
- const initialPrompt = buildInitialPrompt(host.config.mode, context, host.config.isAuto);
2362
+ const initialPrompt = buildInitialPrompt(host.config.mode, context, host.config.isAuto, mode);
1939
2363
  const prompt = buildMultimodalPrompt(initialPrompt, context);
1940
2364
  const agentQuery = query({
1941
2365
  prompt: host.createInputStream(prompt),
@@ -1943,7 +2367,7 @@ ${followUpText}` : followUpText;
1943
2367
  });
1944
2368
  await runWithRetry(agentQuery, context, host, options);
1945
2369
  }
1946
- if (isPmPlanning) {
2370
+ if (isDiscoveryLike) {
1947
2371
  host.syncPlanFile();
1948
2372
  }
1949
2373
  }
@@ -1958,7 +2382,7 @@ async function runWithRetry(initialQuery, context, host, options) {
1958
2382
  );
1959
2383
  }
1960
2384
  const retryPrompt = buildMultimodalPrompt(
1961
- buildInitialPrompt(host.config.mode, context, host.config.isAuto),
2385
+ buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
1962
2386
  context,
1963
2387
  lastErrorWasImage
1964
2388
  );
@@ -1968,7 +2392,12 @@ async function runWithRetry(initialQuery, context, host, options) {
1968
2392
  });
1969
2393
  })();
1970
2394
  try {
1971
- const { retriable, resultSummary } = await processEvents(agentQuery, context, host);
2395
+ const { retriable, resultSummary, modeRestart } = await processEvents(
2396
+ agentQuery,
2397
+ context,
2398
+ host
2399
+ );
2400
+ if (modeRestart) return;
1972
2401
  if (!retriable || host.isStopped()) return;
1973
2402
  lastErrorWasImage = IMAGE_ERROR_PATTERN.test(resultSummary ?? "");
1974
2403
  } catch (error) {
@@ -1978,7 +2407,7 @@ async function runWithRetry(initialQuery, context, host, options) {
1978
2407
  context.claudeSessionId = null;
1979
2408
  host.connection.storeSessionId("");
1980
2409
  const freshPrompt = buildMultimodalPrompt(
1981
- buildInitialPrompt(host.config.mode, context, host.config.isAuto),
2410
+ buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
1982
2411
  context
1983
2412
  );
1984
2413
  const freshQuery = query({
@@ -2164,7 +2593,13 @@ var AgentRunner = class _AgentRunner {
2164
2593
  planSync;
2165
2594
  costTracker = new CostTracker();
2166
2595
  worktreeActive = false;
2167
- activeMode = "planning";
2596
+ agentMode = null;
2597
+ hasExitedPlanMode = false;
2598
+ pendingModeRestart = false;
2599
+ sessionIds = /* @__PURE__ */ new Map();
2600
+ lastQueryModeRestart = false;
2601
+ deferredStartConfig = null;
2602
+ startCommandStarted = false;
2168
2603
  static MAX_SETUP_LOG_LINES = 50;
2169
2604
  constructor(config, callbacks) {
2170
2605
  this.config = config;
@@ -2175,6 +2610,18 @@ var AgentRunner = class _AgentRunner {
2175
2610
  get state() {
2176
2611
  return this._state;
2177
2612
  }
2613
+ /**
2614
+ * Resolve the effective AgentMode from explicit agentMode or legacy config flags.
2615
+ * This is the single axis of behavior for all execution path decisions.
2616
+ */
2617
+ get effectiveAgentMode() {
2618
+ if (this.agentMode) return this.agentMode;
2619
+ if (this.config.mode === "pm") {
2620
+ if (this.config.isAuto) return "auto";
2621
+ return "discovery";
2622
+ }
2623
+ return "building";
2624
+ }
2178
2625
  async setState(status) {
2179
2626
  this._state = status;
2180
2627
  this.connection.emitStatus(status);
@@ -2200,7 +2647,8 @@ var AgentRunner = class _AgentRunner {
2200
2647
  this.connection.onChatMessage(
2201
2648
  (message) => this.injectHumanMessage(message.content, message.files)
2202
2649
  );
2203
- this.connection.onModeChange((data) => this.handleModeChange(data.mode));
2650
+ this.connection.onModeChange((data) => this.handleModeChange(data.agentMode));
2651
+ this.connection.onRunStartCommand(() => this.runDeferredStartCommand());
2204
2652
  await this.setState("connected");
2205
2653
  this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
2206
2654
  this.startHeartbeat();
@@ -2239,6 +2687,9 @@ var AgentRunner = class _AgentRunner {
2239
2687
  return;
2240
2688
  }
2241
2689
  this.taskContext._runnerSessionId = randomUUID2();
2690
+ if (this.taskContext.agentMode) {
2691
+ this.agentMode = this.taskContext.agentMode;
2692
+ }
2242
2693
  this.logEffectiveSettings();
2243
2694
  if (process.env.CODESPACES === "true") {
2244
2695
  unshallowRepo(this.config.workspaceDir);
@@ -2275,18 +2726,30 @@ var AgentRunner = class _AgentRunner {
2275
2726
  } catch {
2276
2727
  }
2277
2728
  }
2278
- const isPm = this.config.mode === "pm";
2279
- const isPackRunner = isPm && !!this.config.isAuto && !!this.taskContext.isParentTask;
2280
- if (isPackRunner) {
2281
- await this.setState("running");
2282
- await this.runQuerySafe(this.taskContext);
2283
- if (!this.stopped) await this.setState("idle");
2284
- } else if (isPm) {
2285
- await this.setState("idle");
2286
- } else {
2287
- await this.setState("running");
2288
- await this.runQuerySafe(this.taskContext);
2289
- if (!this.stopped) await this.setState("idle");
2729
+ switch (this.effectiveAgentMode) {
2730
+ case "discovery":
2731
+ case "help":
2732
+ await this.setState("idle");
2733
+ break;
2734
+ case "building":
2735
+ await this.setState("running");
2736
+ await this.runQuerySafe(this.taskContext);
2737
+ if (!this.stopped) await this.setState("idle");
2738
+ break;
2739
+ case "review":
2740
+ if (this.taskContext.isParentTask) {
2741
+ await this.setState("running");
2742
+ await this.runQuerySafe(this.taskContext);
2743
+ if (!this.stopped) await this.setState("idle");
2744
+ } else {
2745
+ await this.setState("idle");
2746
+ }
2747
+ break;
2748
+ case "auto":
2749
+ await this.setState("running");
2750
+ await this.runQuerySafe(this.taskContext);
2751
+ if (!this.stopped) await this.setState("idle");
2752
+ break;
2290
2753
  }
2291
2754
  await this.runCoreLoop();
2292
2755
  this.stopHeartbeat();
@@ -2294,8 +2757,13 @@ var AgentRunner = class _AgentRunner {
2294
2757
  this.connection.disconnect();
2295
2758
  }
2296
2759
  async runQuerySafe(context, followUp) {
2760
+ this.lastQueryModeRestart = false;
2297
2761
  try {
2298
2762
  await runSdkQuery(this.asQueryHost(), context, followUp);
2763
+ if (this.pendingModeRestart) {
2764
+ this.lastQueryModeRestart = true;
2765
+ this.pendingModeRestart = false;
2766
+ }
2299
2767
  } catch (error) {
2300
2768
  const message = error instanceof Error ? error.message : "Unknown error";
2301
2769
  this.connection.sendEvent({ type: "error", message });
@@ -2306,6 +2774,11 @@ var AgentRunner = class _AgentRunner {
2306
2774
  async runCoreLoop() {
2307
2775
  if (!this.taskContext) return;
2308
2776
  while (!this.stopped) {
2777
+ if (this.lastQueryModeRestart) {
2778
+ this.lastQueryModeRestart = false;
2779
+ await this.handleAutoModeRestart();
2780
+ continue;
2781
+ }
2309
2782
  if (this._state === "idle") {
2310
2783
  const msg = await this.waitForUserContent();
2311
2784
  if (!msg) break;
@@ -2319,6 +2792,65 @@ var AgentRunner = class _AgentRunner {
2319
2792
  }
2320
2793
  }
2321
2794
  }
2795
+ /**
2796
+ * Handle auto mode transition after ExitPlanMode.
2797
+ * Saves the current session, switches model/mode, and restarts with a fresh query.
2798
+ */
2799
+ async handleAutoModeRestart() {
2800
+ if (!this.taskContext) return;
2801
+ const currentModel = this.taskContext.model;
2802
+ const currentSessionId = this.taskContext.claudeSessionId;
2803
+ if (currentSessionId && currentModel) {
2804
+ this.sessionIds.set(currentModel, currentSessionId);
2805
+ }
2806
+ const newMode = this.agentMode;
2807
+ const isParentTask = !!this.taskContext.isParentTask;
2808
+ const newModel = this.getModelForMode(newMode, isParentTask);
2809
+ const resumeSessionId = newModel ? this.sessionIds.get(newModel) : null;
2810
+ if (resumeSessionId) {
2811
+ this.taskContext.claudeSessionId = resumeSessionId;
2812
+ } else {
2813
+ this.taskContext.claudeSessionId = null;
2814
+ this.connection.storeSessionId("");
2815
+ }
2816
+ if (newModel) {
2817
+ this.taskContext.model = newModel;
2818
+ }
2819
+ this.taskContext.agentMode = newMode;
2820
+ this.connection.emitModeTransition({
2821
+ fromMode: "auto",
2822
+ toMode: newMode ?? "building"
2823
+ });
2824
+ this.connection.emitModeChanged(newMode);
2825
+ this.connection.postChatMessage(
2826
+ `Transitioning to **${newMode}** mode${newModel ? ` with ${newModel}` : ""}. Restarting session...`
2827
+ );
2828
+ try {
2829
+ const freshContext = await this.connection.fetchTaskContext();
2830
+ freshContext._runnerSessionId = this.taskContext._runnerSessionId;
2831
+ freshContext.claudeSessionId = this.taskContext.claudeSessionId;
2832
+ freshContext.agentMode = newMode;
2833
+ if (newModel) freshContext.model = newModel;
2834
+ this.taskContext = freshContext;
2835
+ } catch {
2836
+ }
2837
+ await this.setState("running");
2838
+ await this.runQuerySafe(this.taskContext);
2839
+ if (!this.stopped && this._state !== "error") {
2840
+ await this.setState("idle");
2841
+ }
2842
+ }
2843
+ /**
2844
+ * Get the appropriate model for a given mode.
2845
+ * Building uses the builder model (Sonnet), Discovery/Review use PM model (Opus).
2846
+ */
2847
+ getModelForMode(mode, isParentTask) {
2848
+ if (!this.taskContext) return null;
2849
+ if (mode === "building" && !isParentTask) {
2850
+ return this.taskContext.builderModel ?? this.taskContext.model;
2851
+ }
2852
+ return this.taskContext.pmModel ?? this.taskContext.model;
2853
+ }
2322
2854
  async runSetupSafe() {
2323
2855
  await this.setState("setup");
2324
2856
  const ports = await loadForwardPorts(this.config.workspaceDir);
@@ -2340,7 +2872,8 @@ var AgentRunner = class _AgentRunner {
2340
2872
  await this.executeSetupConfig(config);
2341
2873
  const setupEvent = {
2342
2874
  type: "setup_complete",
2343
- previewPort: config.previewPort ?? void 0
2875
+ previewPort: config.previewPort ?? void 0,
2876
+ startCommandDeferred: this.deferredStartConfig !== null ? true : void 0
2344
2877
  };
2345
2878
  this.connection.sendEvent(setupEvent);
2346
2879
  await this.callbacks.onEvent(setupEvent);
@@ -2392,12 +2925,33 @@ The agent cannot start until this is resolved.`
2392
2925
  this.pushSetupLog("(exit 0)");
2393
2926
  }
2394
2927
  if (config.startCommand) {
2395
- this.pushSetupLog(`$ ${config.startCommand} & (background)`);
2396
- runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
2397
- this.connection.sendEvent({ type: "start_command_output", stream, data });
2398
- });
2928
+ if (this.effectiveAgentMode === "auto") {
2929
+ this.deferredStartConfig = config;
2930
+ this.pushSetupLog(`[conveyor] startCommand deferred (auto mode)`);
2931
+ } else {
2932
+ this.pushSetupLog(`$ ${config.startCommand} & (background)`);
2933
+ runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
2934
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
2935
+ });
2936
+ }
2399
2937
  }
2400
2938
  }
2939
+ runDeferredStartCommand() {
2940
+ if (!this.deferredStartConfig?.startCommand || this.startCommandStarted) return;
2941
+ this.startCommandStarted = true;
2942
+ const config = this.deferredStartConfig;
2943
+ this.deferredStartConfig = null;
2944
+ this.pushSetupLog(`$ ${config.startCommand} & (background, deferred)`);
2945
+ this.connection.sendEvent({ type: "start_command_started" });
2946
+ runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
2947
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
2948
+ });
2949
+ const setupEvent = {
2950
+ type: "setup_complete",
2951
+ previewPort: config.previewPort ?? void 0
2952
+ };
2953
+ this.connection.sendEvent(setupEvent);
2954
+ }
2401
2955
  injectHumanMessage(content, files) {
2402
2956
  let messageContent;
2403
2957
  if (files?.length) {
@@ -2510,28 +3064,59 @@ ${f.content}
2510
3064
  }
2511
3065
  }
2512
3066
  asQueryHost() {
2513
- const getActiveMode = () => this.activeMode;
3067
+ const getEffectiveAgentMode = () => this.effectiveAgentMode;
3068
+ const getHasExitedPlanMode = () => this.hasExitedPlanMode;
3069
+ const setHasExitedPlanMode = (val) => {
3070
+ this.hasExitedPlanMode = val;
3071
+ };
3072
+ const getPendingModeRestart = () => this.pendingModeRestart;
3073
+ const setPendingModeRestart = (val) => {
3074
+ this.pendingModeRestart = val;
3075
+ };
3076
+ const getIsParentTask = () => !!this.taskContext?.isParentTask;
2514
3077
  return {
2515
3078
  config: this.config,
2516
3079
  connection: this.connection,
2517
3080
  callbacks: this.callbacks,
2518
3081
  setupLog: this.setupLog,
2519
3082
  costTracker: this.costTracker,
2520
- get activeMode() {
2521
- return getActiveMode();
3083
+ get agentMode() {
3084
+ return getEffectiveAgentMode();
2522
3085
  },
3086
+ get isParentTask() {
3087
+ return getIsParentTask();
3088
+ },
3089
+ get hasExitedPlanMode() {
3090
+ return getHasExitedPlanMode();
3091
+ },
3092
+ set hasExitedPlanMode(val) {
3093
+ setHasExitedPlanMode(val);
3094
+ },
3095
+ get pendingModeRestart() {
3096
+ return getPendingModeRestart();
3097
+ },
3098
+ set pendingModeRestart(val) {
3099
+ setPendingModeRestart(val);
3100
+ },
3101
+ sessionIds: this.sessionIds,
2523
3102
  isStopped: () => this.stopped,
2524
3103
  createInputStream: (prompt) => this.createInputStream(prompt),
2525
3104
  snapshotPlanFiles: () => this.planSync.snapshotPlanFiles(),
2526
- syncPlanFile: () => this.planSync.syncPlanFile()
3105
+ syncPlanFile: () => this.planSync.syncPlanFile(),
3106
+ onModeTransition: (newMode) => {
3107
+ this.agentMode = newMode;
3108
+ }
2527
3109
  };
2528
3110
  }
2529
- handleModeChange(mode) {
3111
+ handleModeChange(newAgentMode) {
2530
3112
  if (this.config.mode !== "pm") return;
2531
- this.activeMode = mode;
2532
- this.connection.emitModeChanged(mode);
3113
+ if (newAgentMode) {
3114
+ this.agentMode = newAgentMode;
3115
+ }
3116
+ const effectiveMode = this.effectiveAgentMode;
3117
+ this.connection.emitModeChanged(effectiveMode);
2533
3118
  this.connection.postChatMessage(
2534
- `Mode switched to **${mode}**${mode === "active" ? " \u2014 I now have direct coding access." : " \u2014 back to planning only."}`
3119
+ `Mode switched to **${effectiveMode}**${effectiveMode === "building" ? " \u2014 I now have direct coding access." : ""}`
2535
3120
  );
2536
3121
  }
2537
3122
  stop() {
@@ -2788,6 +3373,7 @@ var ProjectRunner = class {
2788
3373
  CONVEYOR_MODE: mode,
2789
3374
  CONVEYOR_WORKSPACE: workDir,
2790
3375
  CONVEYOR_USE_WORKTREE: "false",
3376
+ CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
2791
3377
  CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
2792
3378
  CONVEYOR_USE_SANDBOX: useSandbox === false ? "false" : "true"
2793
3379
  },
@@ -2967,4 +3553,4 @@ export {
2967
3553
  ProjectRunner,
2968
3554
  FileCache
2969
3555
  };
2970
- //# sourceMappingURL=chunk-GVE6FQ75.js.map
3556
+ //# sourceMappingURL=chunk-Y4TAVPZV.js.map