@rallycry/conveyor-agent 7.0.4 → 7.0.6

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.
@@ -9,12 +9,16 @@ import {
9
9
  // src/connection/agent-connection.ts
10
10
  import { io } from "socket.io-client";
11
11
  var EVENT_BATCH_MS = 500;
12
- var AgentConnection = class {
12
+ var AgentConnection = class _AgentConnection {
13
13
  socket = null;
14
14
  config;
15
15
  eventBuffer = [];
16
16
  flushTimer = null;
17
17
  lastEmittedStatus = null;
18
+ // Dedup: suppress near-identical messages within a short window
19
+ recentMessages = [];
20
+ static DEDUP_WINDOW_MS = 3e4;
21
+ static DEDUP_SIMILARITY_THRESHOLD = 0.7;
18
22
  // Early-buffering: events that arrive before callbacks are registered
19
23
  earlyMessages = [];
20
24
  earlyStop = false;
@@ -237,6 +241,7 @@ var AgentConnection = class {
237
241
  // ── Convenience methods (thin wrappers around call / emit) ─────────
238
242
  emitStatus(status) {
239
243
  this.lastEmittedStatus = status;
244
+ this.flushEvents();
240
245
  void this.call("reportAgentStatus", {
241
246
  sessionId: this.config.sessionId,
242
247
  status
@@ -245,12 +250,42 @@ var AgentConnection = class {
245
250
  }
246
251
  postChatMessage(content) {
247
252
  if (!this.socket) return;
253
+ if (this.isDuplicateMessage(content)) {
254
+ process.stderr.write(`[dedup] Suppressed near-duplicate message
255
+ `);
256
+ return;
257
+ }
248
258
  void this.call("postAgentMessage", {
249
259
  sessionId: this.config.sessionId,
250
260
  content
251
261
  }).catch(() => {
252
262
  });
253
263
  }
264
+ isDuplicateMessage(content) {
265
+ const now = Date.now();
266
+ this.recentMessages = this.recentMessages.filter(
267
+ (m) => now - m.timestamp < _AgentConnection.DEDUP_WINDOW_MS
268
+ );
269
+ const words = new Set(
270
+ content.toLowerCase().split(/\s+/).filter((w) => w.length >= 3)
271
+ );
272
+ if (words.size === 0) return false;
273
+ for (const recent of this.recentMessages) {
274
+ let intersection = 0;
275
+ for (const w of words) {
276
+ if (recent.words.has(w)) intersection++;
277
+ }
278
+ const union = (/* @__PURE__ */ new Set([...words, ...recent.words])).size;
279
+ if (union > 0 && intersection / union > _AgentConnection.DEDUP_SIMILARITY_THRESHOLD) {
280
+ return true;
281
+ }
282
+ }
283
+ this.recentMessages.push({ words, timestamp: now });
284
+ if (this.recentMessages.length > 3) {
285
+ this.recentMessages.shift();
286
+ }
287
+ return false;
288
+ }
254
289
  sendHeartbeat() {
255
290
  if (!this.socket) return;
256
291
  const statusMap = {
@@ -593,6 +628,34 @@ var Lifecycle = class {
593
628
 
594
629
  // src/runner/git-utils.ts
595
630
  import { execSync } from "child_process";
631
+ function syncWithBaseBranch(cwd, baseBranch) {
632
+ if (!baseBranch) return true;
633
+ try {
634
+ execSync(`git fetch origin ${baseBranch}`, { cwd, stdio: "ignore", timeout: 6e4 });
635
+ } catch {
636
+ process.stderr.write(
637
+ `[conveyor-agent] Warning: git fetch origin ${baseBranch} failed, continuing with current base
638
+ `
639
+ );
640
+ return false;
641
+ }
642
+ try {
643
+ execSync(`git merge origin/${baseBranch} --no-edit`, { cwd, stdio: "ignore", timeout: 3e4 });
644
+ } catch {
645
+ process.stderr.write(
646
+ `[conveyor-agent] Warning: merge origin/${baseBranch} failed, aborting merge and continuing
647
+ `
648
+ );
649
+ try {
650
+ execSync("git merge --abort", { cwd, stdio: "ignore" });
651
+ } catch {
652
+ }
653
+ return false;
654
+ }
655
+ process.stderr.write(`[conveyor-agent] Synced with latest origin/${baseBranch}
656
+ `);
657
+ return true;
658
+ }
596
659
  function hasUncommittedChanges(cwd) {
597
660
  const status = execSync("git status --porcelain", {
598
661
  cwd,
@@ -668,10 +731,12 @@ function tryPush(cwd, branch) {
668
731
  }
669
732
  function isAuthError(cwd) {
670
733
  try {
671
- execSync("git push --dry-run 2>&1", { cwd, stdio: ["ignore", "pipe", "pipe"] });
734
+ execSync("git push --dry-run", { cwd, stdio: ["ignore", "pipe", "pipe"] });
672
735
  return false;
673
736
  } catch (err) {
674
- const msg = err instanceof Error ? err.stderr?.toString() ?? err.message : "";
737
+ const stderr = err.stderr?.toString() ?? "";
738
+ const stdout = err.stdout?.toString() ?? "";
739
+ const msg = stderr || stdout || (err instanceof Error ? err.message : "");
675
740
  return /authentication|authorization|403|401|token/i.test(msg);
676
741
  }
677
742
  }
@@ -833,7 +898,6 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
833
898
  `- "Open" \u2014 Ready to execute (if dependencies are met). Use start_child_cloud_build to fire it.`,
834
899
  `- "InProgress" \u2014 Currently being worked on by a Task Runner. Wait \u2014 it will move to ReviewPR when done.`,
835
900
  `- "ReviewPR" \u2014 Task Runner finished and opened a PR. Review and merge it.`,
836
- `- "Hold" \u2014 PR exists but is on hold for team review. Do not merge \u2014 skip and move on.`,
837
901
  `- "ReviewDev" \u2014 PR was merged to dev. This child is complete. Move on.`,
838
902
  `- "Complete" \u2014 Fully done. Move on.`,
839
903
  ``,
@@ -850,7 +914,6 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
850
914
  ` - "InProgress": A Task Runner is actively working. Do nothing \u2014 wait.`,
851
915
  ` - "Open" + allDependenciesMet=true: Ready to fire. Use start_child_cloud_build.`,
852
916
  ` - "Open" + allDependenciesMet=false: Blocked \u2014 skip for now. Will be unblocked when deps complete.`,
853
- ` - "Hold": On hold \u2014 team must review before merge. Skip.`,
854
917
  ` - "ReviewDev" / "Complete": Already done. Skip.`,
855
918
  ` - "Planning": Not ready. If blocking progress, notify team.`,
856
919
  ``,
@@ -860,7 +923,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
860
923
  ``,
861
924
  `5. After firing all ready tasks: report which tasks you fired to chat, then state you are going idle.`,
862
925
  ``,
863
- `6. When ALL children are in "ReviewDev", "Complete", or "Hold" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with force_update_task_status("Complete").`,
926
+ `6. 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 force_update_task_status("Complete").`,
864
927
  ``,
865
928
  `## Important Rules`,
866
929
  `- When dependencies are set on children, use them to determine execution order. Fire all ready tasks in parallel.`,
@@ -1158,6 +1221,15 @@ function buildDiscoveryPrompt(context) {
1158
1221
  `- Goal: collaborate with the user to create a clear plan`,
1159
1222
  `- Proactively fill task properties (SP, tags, icon) as the plan takes shape`,
1160
1223
  ``,
1224
+ `### Planning Checklist (complete ALL before calling ExitPlanMode)`,
1225
+ `Your PRIMARY goal is to create a thorough plan. Complete these steps in order:`,
1226
+ `1. Read the task description and chat history \u2014 respond to what's been discussed`,
1227
+ `2. Explore the codebase to understand relevant files and patterns`,
1228
+ `3. Save a detailed plan via \`update_task\``,
1229
+ `4. Set story points, tags, and title via \`update_task_properties\``,
1230
+ `5. Discuss the plan with the team if they're engaged, incorporate feedback`,
1231
+ `6. THEN call ExitPlanMode \u2014 it is the LAST step, not the first`,
1232
+ ``,
1161
1233
  `### Self-Identification Tools`,
1162
1234
  `Use these MCP tools to set your own task properties:`,
1163
1235
  `- \`update_task\` \u2014 save your plan and description`,
@@ -1169,9 +1241,16 @@ function buildDiscoveryPrompt(context) {
1169
1241
  `- Add matching tags using \`update_task_properties\` \u2014 this links relevant documentation and rules that help you plan more effectively`,
1170
1242
  `- Tags accelerate discovery by surfacing domain-specific context automatically`,
1171
1243
  ``,
1172
- `### Self-Update vs Subtasks`,
1173
- `- If the work fits in a single task (1-3 SP), update YOUR OWN plan and properties \u2014 do not create subtasks`,
1174
- `- Only create subtasks when the work genuinely requires multiple independent pieces (e.g., Pack-tier work, 8+ SP)`,
1244
+ ...context?.isParentTask ? [
1245
+ `### Parent Task Coordination`,
1246
+ `You are a parent task with child tasks. Focus on breaking work into child tasks with detailed plans, not planning implementation for yourself.`,
1247
+ `- Use \`list_subtasks\` to review existing children. Create or update child tasks using \`create_subtask\` / \`update_subtask\`.`,
1248
+ `- Each child task should be a self-contained unit of work with a clear plan.`
1249
+ ] : [
1250
+ `### Self-Update vs Subtasks`,
1251
+ `- If the work fits in a single task (1-3 SP), update YOUR OWN plan and properties \u2014 do not create subtasks`,
1252
+ `- Only create subtasks when the work genuinely requires multiple independent pieces (e.g., Pack-tier work, 8+ SP)`
1253
+ ],
1175
1254
  ``,
1176
1255
  `### Subtask Plan Requirements`,
1177
1256
  `When creating subtasks, each MUST include a detailed \`plan\` field:`,
@@ -1181,11 +1260,12 @@ function buildDiscoveryPrompt(context) {
1181
1260
  `- Include testing requirements and acceptance criteria`,
1182
1261
  `- Set \`storyPointValue\` based on estimated complexity`,
1183
1262
  ``,
1184
- `### Finishing Planning`,
1185
- `Once your plan is complete and all required properties are set, call the **ExitPlanMode** tool.`,
1263
+ `### Completing Planning`,
1264
+ `Once ALL checklist items above are done, call the **ExitPlanMode** tool.`,
1186
1265
  `- Required before ExitPlanMode will succeed: **plan** (via update_task), **story points** (via update_task_properties), **title** (via update_task_properties)`,
1187
1266
  `- ExitPlanMode validates these properties and marks planning as complete`,
1188
- `- It does NOT start building \u2014 the team controls when to switch to Build mode`
1267
+ `- It does NOT start building \u2014 the team controls when to switch to Build mode`,
1268
+ `- Do NOT call ExitPlanMode until you have thoroughly explored the codebase and saved a detailed plan`
1189
1269
  ];
1190
1270
  if (context) parts.push(...buildPropertyInstructions(context));
1191
1271
  return parts.join("\n");
@@ -1220,6 +1300,14 @@ function buildAutoPrompt(context) {
1220
1300
  `- Include testing requirements and acceptance criteria`,
1221
1301
  `- Set \`storyPointValue\` based on estimated complexity`,
1222
1302
  ``,
1303
+ ...context?.isParentTask ? [
1304
+ ``,
1305
+ `### Parent Task Guidance`,
1306
+ `You are a parent task. Your plan should define child tasks with detailed plans, not direct implementation steps.`,
1307
+ `After ExitPlanMode, you'll transition to Review mode to coordinate child task execution.`,
1308
+ `Child task status lifecycle: Open \u2192 InProgress \u2192 ReviewPR \u2192 ReviewDev \u2192 Complete.`
1309
+ ] : [],
1310
+ ``,
1223
1311
  `### Autonomous Guidelines:`,
1224
1312
  `- Make decisions independently \u2014 do not ask the team for approval at each step`,
1225
1313
  `- Only escalate when genuinely blocked (ambiguous requirements, missing access, conflicting instructions)`,
@@ -1239,8 +1327,14 @@ function buildModePrompt(agentMode, context) {
1239
1327
  `You are in Building mode \u2014 executing the plan.`,
1240
1328
  `- You have full coding access (read, write, edit, bash, git)`,
1241
1329
  `- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
1242
- `- If this is a leaf task (no children): execute the plan directly`,
1243
- `- Goal: implement the plan, run tests, open a PR when done`
1330
+ ...context?.isParentTask ? [
1331
+ `- You are a parent task. Use \`list_subtasks\`, \`start_child_cloud_build\`, and subtask management tools to coordinate children.`,
1332
+ `- Do NOT implement code directly \u2014 fire child builds and review their work.`,
1333
+ `- Goal: coordinate child task execution and ensure all children complete successfully`
1334
+ ] : [
1335
+ `- If this is a leaf task (no children): execute the plan directly`,
1336
+ `- Goal: implement the plan, run tests, open a PR when done`
1337
+ ]
1244
1338
  ];
1245
1339
  if (process.env.CLAUDESPACE_NAME) {
1246
1340
  parts.push(
@@ -1257,15 +1351,7 @@ function buildModePrompt(agentMode, context) {
1257
1351
  return parts.join("\n");
1258
1352
  }
1259
1353
  case "review":
1260
- return [
1261
- `
1262
- ## Mode: Review`,
1263
- `You are in Review mode \u2014 reviewing and coordinating.`,
1264
- `- You have read-only access plus light edit capability (can suggest fixes, run tests, check linting)`,
1265
- `- For parent tasks: you can manage children, review child PRs, fire next child builds`,
1266
- `- You have Pack Runner coordination tools (list_subtasks, fire builds, approve PRs)`,
1267
- `- Goal: ensure quality, provide feedback, coordinate progression`
1268
- ].join("\n");
1354
+ return buildReviewPrompt(context);
1269
1355
  case "auto":
1270
1356
  return buildAutoPrompt(context);
1271
1357
  case "code-review":
@@ -1274,6 +1360,64 @@ function buildModePrompt(agentMode, context) {
1274
1360
  return null;
1275
1361
  }
1276
1362
  }
1363
+ function buildReviewPrompt(context) {
1364
+ const parts = [
1365
+ `
1366
+ ## Mode: Review`,
1367
+ `You are in Review mode \u2014 reviewing and coordinating.`,
1368
+ `- You have read-only access plus light edit capability (can suggest fixes, run tests, check linting)`,
1369
+ ``
1370
+ ];
1371
+ if (context?.isParentTask) {
1372
+ parts.push(
1373
+ `### Parent Task Review`,
1374
+ `You are reviewing and coordinating child tasks.`,
1375
+ `- Use \`list_subtasks\` to see current child task state and progress.`,
1376
+ `- For children in ReviewPR status: review their code quality and merge with \`approve_and_merge_pr\`.`,
1377
+ `- For children with failing CI: check with \`get_task_cli(childTaskId)\` and escalate if stuck.`,
1378
+ `- Fire next child builds with \`start_child_cloud_build\` when ready.`,
1379
+ `- Create follow-up tasks for issues discovered during review.`,
1380
+ ``,
1381
+ `### Coordination Workflow`,
1382
+ `1. Check child task statuses with \`list_subtasks\``,
1383
+ `2. Review completed children \u2014 check PRs, run tests if needed`,
1384
+ `3. Approve and merge passing PRs`,
1385
+ `4. Fire builds for children that are ready`,
1386
+ `5. Create follow-up tasks for anything out of scope`
1387
+ );
1388
+ } else {
1389
+ parts.push(
1390
+ `### Leaf Task Review`,
1391
+ `You are reviewing your own work before completion.`,
1392
+ `- Run tests and check linting to verify the PR is ready.`,
1393
+ `- If the PR is already open, review the diff for correctness.`,
1394
+ `- You can get hands dirty \u2014 if a fix is small, make it directly.`,
1395
+ `- If follow-up work is needed, use \`create_follow_up_task\`.`
1396
+ );
1397
+ }
1398
+ parts.push(
1399
+ ``,
1400
+ `### General Review Guidelines`,
1401
+ `- For larger issues, create a follow-up task rather than fixing directly.`,
1402
+ `- Focus on correctness, pattern consistency, and test coverage.`,
1403
+ `- Be concise in feedback \u2014 actionable specifics over general observations.`,
1404
+ `- Goal: ensure quality, provide feedback, coordinate progression.`
1405
+ );
1406
+ if (process.env.CLAUDESPACE_NAME) {
1407
+ parts.push(
1408
+ ``,
1409
+ `### Resource Management`,
1410
+ `Your pod starts with minimal resources (0.25 CPU / 1 Gi). You MUST call \`scale_up_resources\``,
1411
+ `BEFORE running any of these operations \u2014 they WILL fail or OOM at baseline resources:`,
1412
+ `- **light** (1 CPU / 4 Gi) \u2014 bun/npm/yarn install, pip install, basic dev servers, light builds`,
1413
+ `- **standard** (2 CPU / 8 Gi) \u2014 full dev servers, test suites, typecheck, lint`,
1414
+ `- **heavy** (4 CPU / 16 Gi) \u2014 E2E/browser automation, large parallel builds`,
1415
+ `Scaling is one-way (up only) and capped by project limits.`,
1416
+ `CRITICAL: Always scale to at least "light" before running any package install command.`
1417
+ );
1418
+ }
1419
+ return parts.join("\n");
1420
+ }
1277
1421
  function buildCodeReviewPrompt() {
1278
1422
  return [
1279
1423
  `
@@ -1504,6 +1648,7 @@ Your responses are sent directly to the task chat \u2014 the team sees everythin
1504
1648
  }
1505
1649
 
1506
1650
  // src/execution/prompt-builder.ts
1651
+ var CHAT_HISTORY_LIMIT = 40;
1507
1652
  function formatFileSize(bytes) {
1508
1653
  if (bytes === void 0) return "";
1509
1654
  if (bytes < 1024) return `${bytes}B`;
@@ -1649,7 +1794,7 @@ function formatTaskFile(file) {
1649
1794
  return [];
1650
1795
  }
1651
1796
  function formatChatHistory(chatHistory) {
1652
- const relevant = chatHistory.slice(-20);
1797
+ const relevant = chatHistory.slice(-CHAT_HISTORY_LIMIT);
1653
1798
  const parts = [`
1654
1799
  ## Recent Chat Context`];
1655
1800
  for (const msg of relevant) {
@@ -1676,6 +1821,12 @@ async function resolveTaskTagContext(context) {
1676
1821
  async function buildTaskBody(context) {
1677
1822
  const parts = [];
1678
1823
  parts.push(`# Task: ${context.title}`);
1824
+ if (context.projectName) {
1825
+ const descLine = context.projectDescription ? `
1826
+ ${context.projectDescription}` : "";
1827
+ parts.push(`
1828
+ ## Project: ${context.projectName}${descLine}`);
1829
+ }
1679
1830
  if (context.description) {
1680
1831
  parts.push(`
1681
1832
  ## Description
@@ -1755,7 +1906,8 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
1755
1906
  if (isPm && context.isParentTask) {
1756
1907
  return [
1757
1908
  `You are the project manager for this task and its subtasks.`,
1758
- `Use list_subtasks to review the current state of child tasks.`,
1909
+ `Review existing subtasks via \`list_subtasks\` and the chat history before taking action.`,
1910
+ `Read the task description and chat history carefully \u2014 the team may have already provided context and requirements. Respond to what's been discussed rather than starting from scratch.`,
1759
1911
  `The task details are provided above. Wait for the team to provide instructions before taking action.`,
1760
1912
  `When you finish planning, save the plan with update_task and end your turn. Your reply will be visible to the team in chat.`
1761
1913
  ];
@@ -1763,6 +1915,7 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
1763
1915
  if (isPm) {
1764
1916
  return [
1765
1917
  `You are the project manager for this task.`,
1918
+ `Read the task description and chat history carefully \u2014 the team may have already provided context and requirements. Respond to what's been discussed rather than starting from scratch.`,
1766
1919
  `The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`,
1767
1920
  `When you finish planning, save the plan with update_task and end your turn. Your reply summarizing the plan will be visible in chat. A separate task agent will execute the plan after review.`
1768
1921
  ];
@@ -2016,7 +2169,11 @@ function buildGetTaskCliTool(connection) {
2016
2169
  taskId: task_id,
2017
2170
  source
2018
2171
  });
2019
- const formatted = result.map((entry) => `[${entry.time}] [${entry.event.type}] ${formatCliEvent(entry.event)}`).join("\n");
2172
+ const formatted = result.map((entry) => {
2173
+ const type = entry.type;
2174
+ const time = entry.timestamp;
2175
+ return `[${time}] [${type}] ${formatCliEvent(entry)}`;
2176
+ }).join("\n");
2020
2177
  return textResult(formatted || "No CLI logs found.");
2021
2178
  } catch (error) {
2022
2179
  return textResult(
@@ -2264,7 +2421,7 @@ function buildForceUpdateTaskStatusTool(connection) {
2264
2421
  "force_update_task_status",
2265
2422
  "EMERGENCY ONLY: Force-override a task's Kanban status. Status transitions happen automatically (building sets InProgress, PR creation sets ReviewPR, merge sets ReviewDev). Only use this if an automatic transition failed or a task is stuck in the wrong status. Omit task_id to update the current task, or provide a child task ID.",
2266
2423
  {
2267
- status: z.enum(["InProgress", "ReviewPR", "Hold", "ReviewDev", "Complete"]).describe("The new status for the task"),
2424
+ status: z.enum(["InProgress", "ReviewPR", "ReviewDev", "Complete"]).describe("The new status for the task"),
2268
2425
  task_id: z.string().optional().describe("Child task ID to update. Omit to update the current task.")
2269
2426
  },
2270
2427
  async ({ status, task_id }) => {
@@ -2770,6 +2927,45 @@ function buildPmTools(connection, options) {
2770
2927
  // src/tools/discovery-tools.ts
2771
2928
  import { z as z3 } from "zod";
2772
2929
  var SP_DESCRIPTION2 = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)";
2930
+ function buildSearchIconsTool(connection) {
2931
+ return defineTool(
2932
+ "search_icons",
2933
+ "Search for icons by keyword. Searches both the Conveyor database (existing icons) and FontAwesome library (~2200 icons). Returns matching icons with IDs and preview info. Use this to find an icon before generating one.",
2934
+ {
2935
+ query: z3.string().describe("Search query for icon name or keyword (e.g. 'bug', 'gear', 'star')")
2936
+ },
2937
+ async ({ query }) => {
2938
+ try {
2939
+ const [dbIcons, faIcons] = await Promise.all([
2940
+ connection.call("listIcons", { sessionId: connection.sessionId }),
2941
+ connection.call("searchFaIcons", { sessionId: connection.sessionId, query })
2942
+ ]);
2943
+ const q = query.toLowerCase();
2944
+ const matchingDbIcons = dbIcons.filter((icon) => icon.name.toLowerCase().includes(q));
2945
+ const dbFaIds = new Set(matchingDbIcons.map((i) => i.name));
2946
+ const uniqueFaIcons = faIcons.filter((fa) => !dbFaIds.has(fa.fontAwesomeId));
2947
+ const results = {
2948
+ existingIcons: matchingDbIcons.map((i) => ({
2949
+ id: i.id,
2950
+ name: i.name,
2951
+ source: "database"
2952
+ })),
2953
+ fontAwesomeIcons: uniqueFaIcons.map((fa) => ({
2954
+ fontAwesomeId: fa.fontAwesomeId,
2955
+ label: fa.label,
2956
+ styles: fa.styles,
2957
+ source: "fontawesome"
2958
+ }))
2959
+ };
2960
+ return textResult(JSON.stringify(results, null, 2));
2961
+ } catch (error) {
2962
+ const message = error instanceof Error ? error.message : "Unknown error";
2963
+ return textResult(`Failed to search icons: ${message}`);
2964
+ }
2965
+ },
2966
+ { annotations: { readOnlyHint: true } }
2967
+ );
2968
+ }
2773
2969
  function buildIconTools(connection) {
2774
2970
  return [
2775
2971
  defineTool(
@@ -2792,10 +2988,10 @@ function buildIconTools(connection) {
2792
2988
  ),
2793
2989
  defineTool(
2794
2990
  "generate_task_icon",
2795
- "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.",
2991
+ "Find a FontAwesome icon matching the description and assign it to this task. Falls back to a placeholder if no match is found. Provide a concise keyword or description.",
2796
2992
  {
2797
2993
  prompt: z3.string().describe(
2798
- "Description of the icon to generate (e.g. 'minimal flat gear and wrench icon')"
2994
+ "Keyword or description to search FontAwesome icons (e.g. 'bug', 'gear', 'rocket')"
2799
2995
  ),
2800
2996
  aspectRatio: z3.enum(["auto", "portrait", "landscape", "square"]).optional().describe("Icon aspect ratio, defaults to square")
2801
2997
  },
@@ -2850,6 +3046,7 @@ function buildDiscoveryTools(connection) {
2850
3046
  }
2851
3047
  }
2852
3048
  ),
3049
+ buildSearchIconsTool(connection),
2853
3050
  ...buildIconTools(connection)
2854
3051
  ];
2855
3052
  }
@@ -4222,6 +4419,7 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
4222
4419
  }
4223
4420
 
4224
4421
  // src/execution/event-handlers.ts
4422
+ var logger = createServiceLogger("event-handlers");
4225
4423
  function safeVoid(promise, context) {
4226
4424
  if (promise && typeof promise.catch === "function") {
4227
4425
  promise.catch((err) => {
@@ -4399,6 +4597,7 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
4399
4597
  }
4400
4598
  function handleRateLimitEvent(event, host) {
4401
4599
  const { rate_limit_info } = event;
4600
+ logger.info("Rate limit event received", { rate_limit_info });
4402
4601
  const status = rate_limit_info.status;
4403
4602
  const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
4404
4603
  if (utilization !== void 0 && rate_limit_info.rateLimitType) {
@@ -4616,6 +4815,16 @@ async function processEvents(events, context, host) {
4616
4815
  };
4617
4816
  }
4618
4817
 
4818
+ // src/execution/task-property-utils.ts
4819
+ function collectMissingProps(taskProps) {
4820
+ const missing = [];
4821
+ if (!taskProps.plan?.trim()) missing.push("plan (save via update_task)");
4822
+ if (!taskProps.storyPointId) missing.push("story points (use update_task_properties)");
4823
+ if (!taskProps.title || taskProps.title === "Untitled")
4824
+ missing.push("title (use update_task_properties)");
4825
+ return missing;
4826
+ }
4827
+
4619
4828
  // src/execution/tool-access.ts
4620
4829
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
4621
4830
  var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
@@ -4701,18 +4910,11 @@ function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask)
4701
4910
  }
4702
4911
  return { behavior: "allow", updatedInput: input };
4703
4912
  }
4704
- function collectMissingProps(taskProps) {
4705
- const missing = [];
4706
- if (!taskProps.plan?.trim()) missing.push("plan (save via update_task)");
4707
- if (!taskProps.storyPointId) missing.push("story points (use update_task_properties)");
4708
- if (!taskProps.title || taskProps.title === "Untitled")
4709
- missing.push("title (use update_task_properties)");
4710
- return missing;
4711
- }
4712
4913
  async function handleExitPlanMode(host, input) {
4713
4914
  if (host.hasExitedPlanMode) {
4714
4915
  return { behavior: "allow", updatedInput: input };
4715
4916
  }
4917
+ host.exitPlanAttempts++;
4716
4918
  try {
4717
4919
  host.syncPlanFile();
4718
4920
  const taskProps = await host.connection.getTaskProperties();
@@ -4735,21 +4937,27 @@ async function handleExitPlanMode(host, input) {
4735
4937
  }
4736
4938
  }
4737
4939
  if (missingProps.length > 0) {
4738
- return {
4739
- behavior: "deny",
4740
- message: [
4741
- "Cannot exit plan mode yet. Required task properties are missing:",
4742
- ...missingProps.map((p) => `- ${p}`),
4743
- "",
4744
- "Fill these in using MCP tools, then try ExitPlanMode again."
4745
- ].join("\n")
4746
- };
4940
+ if (host.exitPlanAttempts <= 1) {
4941
+ return {
4942
+ behavior: "deny",
4943
+ message: [
4944
+ "Cannot exit plan mode yet. Required task properties are missing:",
4945
+ ...missingProps.map((p) => `- ${p}`),
4946
+ "",
4947
+ "Fill these in using MCP tools, then try ExitPlanMode again."
4948
+ ].join("\n")
4949
+ };
4950
+ }
4951
+ host.connection.postChatMessage(
4952
+ `\u26A0\uFE0F ExitPlanMode allowed with missing properties: ${missingProps.join(", ")}. Consider backfilling these later.`
4953
+ );
4747
4954
  }
4748
4955
  if (host.agentMode === "discovery") {
4749
4956
  host.hasExitedPlanMode = true;
4750
4957
  host.connection.postChatMessage(
4751
4958
  "Plan complete. The task stays in Planning \u2014 identify it or switch to Build mode when ready."
4752
4959
  );
4960
+ host.requestStop();
4753
4961
  return { behavior: "allow", updatedInput: input };
4754
4962
  }
4755
4963
  await host.connection.triggerIdentification();
@@ -4848,7 +5056,7 @@ function buildCanUseTool(host) {
4848
5056
  }
4849
5057
 
4850
5058
  // src/execution/query-executor.ts
4851
- var logger = createServiceLogger("QueryExecutor");
5059
+ var logger2 = createServiceLogger("QueryExecutor");
4852
5060
  var API_ERROR_PATTERN3 = /API Error: [45]\d\d/;
4853
5061
  var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
4854
5062
  var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
@@ -4868,6 +5076,18 @@ function buildHooks(host) {
4868
5076
  isError: false
4869
5077
  });
4870
5078
  host.pendingToolOutputs.push(output);
5079
+ if (input.tool_name === "mcp__conveyor__create_pull_request") {
5080
+ try {
5081
+ const props = await host.connection.getTaskProperties();
5082
+ const missing = collectMissingProps(props);
5083
+ if (missing.length > 0) {
5084
+ host.connection.postChatMessage(
5085
+ `PR created! Please backfill missing task properties: ${missing.join(", ")}`
5086
+ );
5087
+ }
5088
+ } catch {
5089
+ }
5090
+ }
4871
5091
  }
4872
5092
  return await Promise.resolve({ continue: true });
4873
5093
  }
@@ -4929,7 +5149,7 @@ function buildQueryOptions(host, context) {
4929
5149
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
4930
5150
  enableFileCheckpointing: settings.enableFileCheckpointing,
4931
5151
  stderr: (data) => {
4932
- logger.warn("Claude Code stderr", { data: data.trimEnd() });
5152
+ logger2.warn("Claude Code stderr", { data: data.trimEnd() });
4933
5153
  }
4934
5154
  };
4935
5155
  }
@@ -5239,7 +5459,7 @@ var CostTracker = class {
5239
5459
  };
5240
5460
 
5241
5461
  // src/runner/query-bridge.ts
5242
- var logger2 = createServiceLogger("QueryBridge");
5462
+ var logger3 = createServiceLogger("QueryBridge");
5243
5463
  var QueryBridge = class {
5244
5464
  constructor(connection, mode, runnerConfig, callbacks, workspaceDir) {
5245
5465
  this.connection = connection;
@@ -5250,6 +5470,10 @@ var QueryBridge = class {
5250
5470
  this.costTracker = new CostTracker();
5251
5471
  this.planSync = new PlanSync(workspaceDir, connection);
5252
5472
  }
5473
+ connection;
5474
+ mode;
5475
+ runnerConfig;
5476
+ callbacks;
5253
5477
  harness;
5254
5478
  costTracker;
5255
5479
  planSync;
@@ -5290,8 +5514,13 @@ var QueryBridge = class {
5290
5514
  await runSdkQuery(host, context, followUpContent);
5291
5515
  } catch (err) {
5292
5516
  const msg = err instanceof Error ? err.message : String(err);
5293
- logger2.error("Query execution failed", { error: msg });
5294
- this.connection.sendEvent({ type: "error", message: msg });
5517
+ const isAbort = this._stopped || /abort/i.test(msg);
5518
+ if (isAbort) {
5519
+ logger3.info("Query stopped by user", { error: msg });
5520
+ } else {
5521
+ logger3.error("Query execution failed", { error: msg });
5522
+ this.connection.sendEvent({ type: "error", message: msg });
5523
+ }
5295
5524
  } finally {
5296
5525
  this.mode.pendingModeRestart = false;
5297
5526
  }
@@ -5321,6 +5550,7 @@ var QueryBridge = class {
5321
5550
  set hasExitedPlanMode(val) {
5322
5551
  bridge.mode.hasExitedPlanMode = val;
5323
5552
  },
5553
+ exitPlanAttempts: 0,
5324
5554
  get pendingModeRestart() {
5325
5555
  return bridge.mode.pendingModeRestart;
5326
5556
  },
@@ -5337,6 +5567,7 @@ var QueryBridge = class {
5337
5567
  return bridge._abortController;
5338
5568
  },
5339
5569
  isStopped: () => bridge._stopped,
5570
+ requestStop: () => bridge.stop(),
5340
5571
  createInputStream: (prompt) => bridge.createInputStream(prompt),
5341
5572
  snapshotPlanFiles: () => bridge.planSync.snapshotPlanFiles(),
5342
5573
  syncPlanFile: () => bridge.planSync.syncPlanFile(),
@@ -5381,9 +5612,11 @@ var SessionRunner = class _SessionRunner {
5381
5612
  this.lifecycle = new Lifecycle(lifecycleConfig, {
5382
5613
  onHeartbeat: () => this.connection.sendHeartbeat(),
5383
5614
  onIdleTimeout: () => {
5615
+ process.stderr.write("[conveyor-agent] Idle timeout reached, entering sleep\n");
5384
5616
  this.connection.emitStatus("sleeping");
5385
5617
  },
5386
5618
  onSleep: () => {
5619
+ process.stderr.write("[conveyor-agent] Sleep mode active\n");
5387
5620
  this.connection.postChatMessage("Agent sleeping \u2014 send a message or click Resume to wake.");
5388
5621
  },
5389
5622
  onSleepGraceExpired: () => {
@@ -5395,6 +5628,7 @@ var SessionRunner = class _SessionRunner {
5395
5628
  }
5396
5629
  },
5397
5630
  onWake: () => {
5631
+ process.stderr.write("[conveyor-agent] Woken from sleep\n");
5398
5632
  this.lifecycle.cancelSleepShutdown();
5399
5633
  }
5400
5634
  });
@@ -5409,7 +5643,7 @@ var SessionRunner = class _SessionRunner {
5409
5643
  return this.stopped;
5410
5644
  }
5411
5645
  // ── Main lifecycle ─────────────────────────────────────────────────
5412
- // oxlint-disable-next-line max-lines-per-function -- lifecycle orchestration is inherently sequential
5646
+ // oxlint-disable-next-line max-lines-per-function, complexity -- lifecycle orchestration is inherently sequential
5413
5647
  async start() {
5414
5648
  await this.setState("connecting");
5415
5649
  await this.connection.connect();
@@ -5447,6 +5681,9 @@ var SessionRunner = class _SessionRunner {
5447
5681
  await this.shutdown("error");
5448
5682
  return;
5449
5683
  }
5684
+ if (this.fullContext?.baseBranch) {
5685
+ syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
5686
+ }
5450
5687
  this.mode.resolveInitialMode(this.taskContext);
5451
5688
  this.queryBridge = this.createQueryBridge();
5452
5689
  this.logInitialization();
@@ -5496,8 +5733,11 @@ var SessionRunner = class _SessionRunner {
5496
5733
  content: msg.content,
5497
5734
  userId: msg.userId
5498
5735
  });
5499
- if (this.fullContext && this.queryBridge) {
5500
- await this.queryBridge.execute(this.fullContext, msg.content);
5736
+ await this.executeQuery(msg.content);
5737
+ if (this.stopped) break;
5738
+ if (this.interrupted) {
5739
+ this.interrupted = false;
5740
+ continue;
5501
5741
  }
5502
5742
  if (!this.stopped && this.pendingMessages.length === 0) {
5503
5743
  await this.maybeSendPRNudge();
@@ -5518,7 +5758,7 @@ var SessionRunner = class _SessionRunner {
5518
5758
  if (effectiveMode === "code-review") {
5519
5759
  await this.setState("running");
5520
5760
  await this.callbacks.onEvent({ type: "execute_mode", mode: effectiveMode });
5521
- await this.queryBridge?.execute(this.fullContext);
5761
+ await this.executeQuery();
5522
5762
  this.stopped = true;
5523
5763
  return;
5524
5764
  }
@@ -5526,7 +5766,7 @@ var SessionRunner = class _SessionRunner {
5526
5766
  if (shouldRun) {
5527
5767
  await this.setState("running");
5528
5768
  await this.callbacks.onEvent({ type: "execute_mode", mode: effectiveMode });
5529
- await this.queryBridge?.execute(this.fullContext);
5769
+ await this.executeQuery();
5530
5770
  if (!this.stopped) await this.setState("idle");
5531
5771
  } else {
5532
5772
  await this.setState("idle");
@@ -5555,6 +5795,20 @@ var SessionRunner = class _SessionRunner {
5555
5795
  }
5556
5796
  }
5557
5797
  }
5798
+ // ── Query execution with abort handling ────────────────────────────
5799
+ /** Run queryBridge.execute, swallowing abort errors from stop/softStop. */
5800
+ async executeQuery(followUpContent) {
5801
+ if (!this.fullContext || !this.queryBridge) return;
5802
+ try {
5803
+ await this.queryBridge.execute(this.fullContext, followUpContent);
5804
+ } catch (err) {
5805
+ if (this.interrupted || this.stopped) {
5806
+ process.stderr.write("[conveyor-agent] Query aborted by stop/softStop signal\n");
5807
+ return;
5808
+ }
5809
+ throw err;
5810
+ }
5811
+ }
5558
5812
  // ── Stop / soft-stop ───────────────────────────────────────────────
5559
5813
  stop() {
5560
5814
  this.stopped = true;
@@ -5624,9 +5878,8 @@ var SessionRunner = class _SessionRunner {
5624
5878
  this.connection.postChatMessage(chatMsg);
5625
5879
  await this.setState("running");
5626
5880
  await this.callbacks.onEvent({ type: "pr_nudge", prompt });
5627
- if (this.fullContext && this.queryBridge) {
5628
- await this.queryBridge.execute(this.fullContext, prompt);
5629
- }
5881
+ await this.executeQuery(prompt);
5882
+ if (this.interrupted || this.stopped) return;
5630
5883
  await this.refreshTaskContext();
5631
5884
  }
5632
5885
  }
@@ -5663,6 +5916,8 @@ var SessionRunner = class _SessionRunner {
5663
5916
  model: ctx.model,
5664
5917
  githubBranch: ctx.githubBranch ?? "",
5665
5918
  baseBranch: ctx.baseBranch ?? "",
5919
+ projectName: ctx.projectName ?? null,
5920
+ projectDescription: ctx.projectDescription ?? null,
5666
5921
  githubPRUrl: ctx.githubPRUrl,
5667
5922
  claudeSessionId: ctx.claudeSessionId ?? null,
5668
5923
  isParentTask: !!ctx.parentTaskId,
@@ -5696,6 +5951,10 @@ var SessionRunner = class _SessionRunner {
5696
5951
  );
5697
5952
  bridge.isParentTask = this.fullContext?.isParentTask ?? false;
5698
5953
  bridge.onModeTransition = (newMode) => {
5954
+ const oldMode = this.mode.effectiveMode;
5955
+ process.stderr.write(`[conveyor-agent] Mode transition: ${oldMode} \u2192 ${newMode}
5956
+ `);
5957
+ this.connection.sendEvent({ type: "mode_transition", from: oldMode, to: newMode });
5699
5958
  this.mode.pendingModeRestart = true;
5700
5959
  this.connection.emitModeChanged(newMode);
5701
5960
  this.softStop();
@@ -5731,6 +5990,9 @@ var SessionRunner = class _SessionRunner {
5731
5990
  await this.callbacks.onStatusChange(status);
5732
5991
  }
5733
5992
  async shutdown(finalState) {
5993
+ process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
5994
+ `);
5995
+ this.connection.sendEvent({ type: "shutdown", reason: finalState });
5734
5996
  this.lifecycle.destroy();
5735
5997
  await this.setState(finalState);
5736
5998
  this.connection.disconnect();
@@ -5738,21 +6000,29 @@ var SessionRunner = class _SessionRunner {
5738
6000
  logInitialization() {
5739
6001
  const context = {
5740
6002
  mode: this.mode.effectiveMode,
5741
- model: this.taskContext?.model,
5742
6003
  runnerMode: this.config.runnerMode ?? "task",
5743
6004
  sessionId: this.sessionId,
6005
+ // Task context
5744
6006
  isParentTask: this.fullContext?.isParentTask ?? false,
5745
- status: this.taskContext?.status
6007
+ status: this.taskContext?.status,
6008
+ taskTitle: this.fullContext?.title,
6009
+ hasExistingPR: !!this.fullContext?.githubPRUrl,
6010
+ hasExistingSession: !!this.fullContext?.claudeSessionId,
6011
+ chatHistoryLength: this.fullContext?.chatHistory?.length ?? 0,
6012
+ tagIds: this.fullContext?.taskTagIds ?? [],
6013
+ // Agent config
6014
+ model: this.taskContext?.model,
6015
+ isAuto: this.config.isAuto ?? false
5746
6016
  };
5747
6017
  process.stderr.write(`[conveyor-agent] Initialized: ${JSON.stringify(context)}
5748
6018
  `);
5749
- this.connection.sendEvent({ type: "initialization", ...context });
6019
+ this.connection.sendEvent({ type: "session_manifest", ...context });
5750
6020
  }
5751
6021
  };
5752
6022
 
5753
6023
  // src/connection/project-connection.ts
5754
6024
  import { io as io2 } from "socket.io-client";
5755
- var logger3 = createServiceLogger("ProjectConnection");
6025
+ var logger4 = createServiceLogger("ProjectConnection");
5756
6026
  var EVENT_BATCH_MS2 = 500;
5757
6027
  var ProjectConnection = class {
5758
6028
  socket = null;
@@ -5795,7 +6065,7 @@ var ProjectConnection = class {
5795
6065
  let settled = false;
5796
6066
  let attempts = 0;
5797
6067
  const maxInitialAttempts = 30;
5798
- logger3.info("Connecting", { apiUrl: this.config.apiUrl, projectId: this.config.projectId });
6068
+ logger4.info("Connecting", { apiUrl: this.config.apiUrl, projectId: this.config.projectId });
5799
6069
  this.socket = io2(this.config.apiUrl, {
5800
6070
  auth: {
5801
6071
  projectToken: this.config.projectToken,
@@ -5815,7 +6085,7 @@ var ProjectConnection = class {
5815
6085
  settled = true;
5816
6086
  resolve2();
5817
6087
  }
5818
- logger3.info("Connected to API");
6088
+ logger4.info("Connected to API");
5819
6089
  });
5820
6090
  this.socket.on("connect_error", (error) => {
5821
6091
  attempts++;
@@ -5827,10 +6097,10 @@ var ProjectConnection = class {
5827
6097
  }
5828
6098
  });
5829
6099
  this.socket.on("disconnect", (reason) => {
5830
- logger3.warn("Disconnected from API", { reason });
6100
+ logger4.warn("Disconnected from API", { reason });
5831
6101
  });
5832
6102
  this.socket.io.on("reconnect", () => {
5833
- logger3.info("Reconnected to API");
6103
+ logger4.info("Reconnected to API");
5834
6104
  for (const cb of this.reconnectCallbacks) cb();
5835
6105
  });
5836
6106
  });
@@ -5977,12 +6247,14 @@ function runStartCommand(cmd, cwd, onOutput) {
5977
6247
 
5978
6248
  // src/runner/commit-watcher.ts
5979
6249
  import { execSync as execSync3 } from "child_process";
5980
- var logger4 = createServiceLogger("CommitWatcher");
6250
+ var logger5 = createServiceLogger("CommitWatcher");
5981
6251
  var CommitWatcher = class {
5982
6252
  constructor(config, callbacks) {
5983
6253
  this.config = config;
5984
6254
  this.callbacks = callbacks;
5985
6255
  }
6256
+ config;
6257
+ callbacks;
5986
6258
  interval = null;
5987
6259
  lastKnownRemoteSha = null;
5988
6260
  branch = null;
@@ -5993,7 +6265,7 @@ var CommitWatcher = class {
5993
6265
  this.branch = branch;
5994
6266
  this.lastKnownRemoteSha = this.getLocalHeadSha();
5995
6267
  this.interval = setInterval(() => void this.poll(), this.config.pollIntervalMs);
5996
- logger4.info("Commit watcher started", {
6268
+ logger5.info("Commit watcher started", {
5997
6269
  branch,
5998
6270
  baseSha: this.lastKnownRemoteSha?.slice(0, 8)
5999
6271
  });
@@ -6058,7 +6330,7 @@ var CommitWatcher = class {
6058
6330
  }
6059
6331
  this.lastKnownRemoteSha = remoteSha;
6060
6332
  this.isSyncing = true;
6061
- logger4.info("New commits detected", {
6333
+ logger5.info("New commits detected", {
6062
6334
  branch: this.branch,
6063
6335
  commitCount,
6064
6336
  sha: remoteSha.slice(0, 8)
@@ -6074,7 +6346,7 @@ var CommitWatcher = class {
6074
6346
  });
6075
6347
  } catch (err) {
6076
6348
  const msg = err instanceof Error ? err.message : String(err);
6077
- logger4.error("Error handling new commits", { error: msg });
6349
+ logger5.error("Error handling new commits", { error: msg });
6078
6350
  } finally {
6079
6351
  this.isSyncing = false;
6080
6352
  }
@@ -6311,7 +6583,7 @@ function buildProjectTools(connection) {
6311
6583
  }
6312
6584
 
6313
6585
  // src/runner/project-chat-handler.ts
6314
- var logger5 = createServiceLogger("ProjectChat");
6586
+ var logger6 = createServiceLogger("ProjectChat");
6315
6587
  var FALLBACK_MODEL = "claude-sonnet-4-20250514";
6316
6588
  function buildSystemPrompt2(projectDir, agentCtx) {
6317
6589
  const parts = [];
@@ -6358,7 +6630,7 @@ async function fetchContext(connection, chatId) {
6358
6630
  projectId: connection.projectId
6359
6631
  });
6360
6632
  } catch {
6361
- logger5.warn("Could not fetch agent context, using defaults");
6633
+ logger6.warn("Could not fetch agent context, using defaults");
6362
6634
  }
6363
6635
  let chatHistory = [];
6364
6636
  try {
@@ -6368,7 +6640,7 @@ async function fetchContext(connection, chatId) {
6368
6640
  chatId
6369
6641
  });
6370
6642
  } catch {
6371
- logger5.warn("Could not fetch chat history, proceeding without it");
6643
+ logger6.warn("Could not fetch chat history, proceeding without it");
6372
6644
  }
6373
6645
  return { agentCtx, chatHistory };
6374
6646
  }
@@ -6486,7 +6758,7 @@ async function handleProjectChatMessage(message, connection, projectDir, session
6486
6758
  return await runChatQuery(message, connection, projectDir, sessionId);
6487
6759
  } catch (error) {
6488
6760
  const msg = error instanceof Error ? error.message : String(error);
6489
- logger5.error("Failed to handle message", { error: msg });
6761
+ logger6.error("Failed to handle message", { error: msg });
6490
6762
  try {
6491
6763
  await connection.call("postProjectAgentMessage", {
6492
6764
  projectId: connection.projectId,
@@ -6502,7 +6774,7 @@ async function handleProjectChatMessage(message, connection, projectDir, session
6502
6774
  }
6503
6775
 
6504
6776
  // src/runner/project-runner.ts
6505
- var logger6 = createServiceLogger("ProjectRunner");
6777
+ var logger7 = createServiceLogger("ProjectRunner");
6506
6778
  var __filename = fileURLToPath(import.meta.url);
6507
6779
  var __dirname = path.dirname(__filename);
6508
6780
  var HEARTBEAT_INTERVAL_MS = 3e4;
@@ -6521,7 +6793,7 @@ function setupWorkDir(projectDir, assignment) {
6521
6793
  }
6522
6794
  if (branch && branch !== devBranch) {
6523
6795
  if (hasUncommittedChanges(workDir)) {
6524
- logger6.warn("Uncommitted changes, skipping checkout", { taskId: shortId, branch });
6796
+ logger7.warn("Uncommitted changes, skipping checkout", { taskId: shortId, branch });
6525
6797
  } else {
6526
6798
  try {
6527
6799
  execSync5(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
@@ -6529,7 +6801,7 @@ function setupWorkDir(projectDir, assignment) {
6529
6801
  try {
6530
6802
  execSync5(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
6531
6803
  } catch {
6532
- logger6.warn("Could not checkout branch", { taskId: shortId, branch });
6804
+ logger7.warn("Could not checkout branch", { taskId: shortId, branch });
6533
6805
  }
6534
6806
  }
6535
6807
  }
@@ -6545,7 +6817,7 @@ function spawnChildAgent(assignment, workDir) {
6545
6817
  const effectiveAgentMode = agentMode ?? (isAuto ? "auto" : "");
6546
6818
  const effectiveApiUrl = apiUrl || process.env.CONVEYOR_API_URL || "";
6547
6819
  if (!effectiveApiUrl) {
6548
- logger6.error("No API URL available for child agent", { taskId: taskId.slice(0, 8) });
6820
+ logger7.error("No API URL available for child agent", { taskId: taskId.slice(0, 8) });
6549
6821
  }
6550
6822
  const child = fork(cliPath, [], {
6551
6823
  env: {
@@ -6573,12 +6845,12 @@ function spawnChildAgent(assignment, workDir) {
6573
6845
  const shortId = taskId.slice(0, 8);
6574
6846
  child.stdout?.on("data", (data) => {
6575
6847
  for (const line of data.toString().trimEnd().split("\n")) {
6576
- logger6.info(line, { taskId: shortId });
6848
+ logger7.info(line, { taskId: shortId });
6577
6849
  }
6578
6850
  });
6579
6851
  child.stderr?.on("data", (data) => {
6580
6852
  for (const line of data.toString().trimEnd().split("\n")) {
6581
- logger6.info(line, { taskId: shortId });
6853
+ logger7.info(line, { taskId: shortId });
6582
6854
  }
6583
6855
  });
6584
6856
  return child;
@@ -6626,7 +6898,7 @@ var ProjectRunner = class {
6626
6898
  capabilities: ["task", "pm", "code-review", "audit"]
6627
6899
  });
6628
6900
  this.branchSwitchCommand = registration.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
6629
- logger6.info("Registered as project agent", { agentName: registration.agentName });
6901
+ logger7.info("Registered as project agent", { agentName: registration.agentName });
6630
6902
  try {
6631
6903
  await this.executeSetupCommand();
6632
6904
  this.executeStartCommand();
@@ -6637,7 +6909,7 @@ var ProjectRunner = class {
6637
6909
  });
6638
6910
  } catch (error) {
6639
6911
  const msg = error instanceof Error ? error.message : String(error);
6640
- logger6.error("Environment setup failed", { error: msg });
6912
+ logger7.error("Environment setup failed", { error: msg });
6641
6913
  this.setupComplete = false;
6642
6914
  }
6643
6915
  this.wireEventHandlers();
@@ -6649,7 +6921,7 @@ var ProjectRunner = class {
6649
6921
  if (currentBranch) {
6650
6922
  this.commitWatcher.start(currentBranch);
6651
6923
  }
6652
- logger6.info("Connected, waiting for task assignments");
6924
+ logger7.info("Connected, waiting for task assignments");
6653
6925
  await new Promise((resolve2) => {
6654
6926
  this.resolveLifecycle = resolve2;
6655
6927
  });
@@ -6657,7 +6929,7 @@ var ProjectRunner = class {
6657
6929
  async stop() {
6658
6930
  if (this.stopping) return;
6659
6931
  this.stopping = true;
6660
- logger6.info("Shutting down");
6932
+ logger7.info("Shutting down");
6661
6933
  this.commitWatcher.stop();
6662
6934
  await this.killStartCommand();
6663
6935
  if (this.heartbeatTimer) {
@@ -6688,7 +6960,7 @@ var ProjectRunner = class {
6688
6960
  } catch {
6689
6961
  }
6690
6962
  this.connection.disconnect();
6691
- logger6.info("Shutdown complete");
6963
+ logger7.info("Shutdown complete");
6692
6964
  if (this.resolveLifecycle) {
6693
6965
  this.resolveLifecycle();
6694
6966
  this.resolveLifecycle = null;
@@ -6724,10 +6996,10 @@ var ProjectRunner = class {
6724
6996
  capabilities: ["task", "pm", "code-review", "audit"]
6725
6997
  });
6726
6998
  this.connection.emitStatus(this.activeAgents.size > 0 ? "busy" : "idle");
6727
- logger6.info("Re-registered after reconnect");
6999
+ logger7.info("Re-registered after reconnect");
6728
7000
  } catch (error) {
6729
7001
  const msg = error instanceof Error ? error.message : String(error);
6730
- logger6.error("Failed to re-register after reconnect", { error: msg });
7002
+ logger7.error("Failed to re-register after reconnect", { error: msg });
6731
7003
  }
6732
7004
  }
6733
7005
  // ── Tag audit ──────────────────────────────────────────────────────────
@@ -6738,7 +7010,7 @@ var ProjectRunner = class {
6738
7010
  await handleTagAudit(request, this.connection, this.projectDir);
6739
7011
  } catch (error) {
6740
7012
  const msg = error instanceof Error ? error.message : String(error);
6741
- logger6.error("Tag audit failed", { error: msg, requestId: request.requestId });
7013
+ logger7.error("Tag audit failed", { error: msg, requestId: request.requestId });
6742
7014
  try {
6743
7015
  await this.connection.call("reportTagAuditResult", {
6744
7016
  projectId: this.connection.projectId,
@@ -6757,15 +7029,15 @@ var ProjectRunner = class {
6757
7029
  async killAgent(agent, taskId) {
6758
7030
  const shortId = taskId.slice(0, 8);
6759
7031
  if (agent.process.exitCode !== null) {
6760
- logger6.info("Agent process already exited", { taskId: shortId });
7032
+ logger7.info("Agent process already exited", { taskId: shortId });
6761
7033
  return;
6762
7034
  }
6763
- logger6.info("Killing agent process", { taskId: shortId });
7035
+ logger7.info("Killing agent process", { taskId: shortId });
6764
7036
  agent.process.kill("SIGTERM");
6765
7037
  await new Promise((resolve2) => {
6766
7038
  const timer = setTimeout(() => {
6767
7039
  if (agent.process.exitCode === null) {
6768
- logger6.warn("Agent did not exit after SIGTERM, sending SIGKILL", { taskId: shortId });
7040
+ logger7.warn("Agent did not exit after SIGTERM, sending SIGKILL", { taskId: shortId });
6769
7041
  agent.process.kill("SIGKILL");
6770
7042
  }
6771
7043
  resolve2();
@@ -6784,15 +7056,15 @@ var ProjectRunner = class {
6784
7056
  const existing = this.activeAgents.get(agentKey);
6785
7057
  if (existing) {
6786
7058
  if (existing.process.exitCode === null) {
6787
- logger6.info("Re-assignment received, killing existing agent", { taskId: shortId });
7059
+ logger7.info("Re-assignment received, killing existing agent", { taskId: shortId });
6788
7060
  await this.killAgent(existing, taskId);
6789
7061
  } else {
6790
- logger6.info("Stale agent entry (process already exited), cleaning up", { taskId: shortId });
7062
+ logger7.info("Stale agent entry (process already exited), cleaning up", { taskId: shortId });
6791
7063
  }
6792
7064
  this.activeAgents.delete(agentKey);
6793
7065
  }
6794
7066
  if (this.activeAgents.size >= MAX_CONCURRENT) {
6795
- logger6.warn("Max concurrent agents reached", { maxConcurrent: MAX_CONCURRENT });
7067
+ logger7.warn("Max concurrent agents reached", { maxConcurrent: MAX_CONCURRENT });
6796
7068
  this.connection.emitTaskStopped(taskId, "max_concurrent_reached");
6797
7069
  return;
6798
7070
  }
@@ -6800,9 +7072,12 @@ var ProjectRunner = class {
6800
7072
  try {
6801
7073
  execSync5("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
6802
7074
  } catch {
6803
- logger6.warn("Git fetch failed", { taskId: shortId });
7075
+ logger7.warn("Git fetch failed", { taskId: shortId });
6804
7076
  }
6805
7077
  const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
7078
+ if (assignment.devBranch) {
7079
+ syncWithBaseBranch(workDir, assignment.devBranch);
7080
+ }
6806
7081
  const child = spawnChildAgent(assignment, workDir);
6807
7082
  this.activeAgents.set(agentKey, {
6808
7083
  process: child,
@@ -6811,12 +7086,12 @@ var ProjectRunner = class {
6811
7086
  usesWorktree
6812
7087
  });
6813
7088
  this.connection.emitTaskStarted(taskId);
6814
- logger6.info("Started task", { taskId: shortId, mode, workDir });
7089
+ logger7.info("Started task", { taskId: shortId, mode, workDir });
6815
7090
  child.on("exit", (code) => {
6816
7091
  this.activeAgents.delete(agentKey);
6817
7092
  const reason = code === 0 ? "completed" : `exited with code ${code}`;
6818
7093
  this.connection.emitTaskStopped(taskId, reason);
6819
- logger6.info("Task exited", { taskId: shortId, reason });
7094
+ logger7.info("Task exited", { taskId: shortId, reason });
6820
7095
  if (code === 0 && usesWorktree) {
6821
7096
  try {
6822
7097
  removeWorktree(this.projectDir, taskId);
@@ -6826,7 +7101,7 @@ var ProjectRunner = class {
6826
7101
  });
6827
7102
  } catch (error) {
6828
7103
  const msg = error instanceof Error ? error.message : "Unknown";
6829
- logger6.error("Failed to start task", { taskId: shortId, error: msg });
7104
+ logger7.error("Failed to start task", { taskId: shortId, error: msg });
6830
7105
  this.connection.emitTaskStopped(taskId, `start_failed: ${msg}`);
6831
7106
  }
6832
7107
  }
@@ -6834,7 +7109,7 @@ var ProjectRunner = class {
6834
7109
  const agentKey = this.activeAgents.has(taskId) ? taskId : `${taskId}:code-review`;
6835
7110
  const agent = this.activeAgents.get(agentKey);
6836
7111
  if (!agent) return;
6837
- logger6.info("Stopping task", { taskId: taskId.slice(0, 8) });
7112
+ logger7.info("Stopping task", { taskId: taskId.slice(0, 8) });
6838
7113
  void this.killAgent(agent, taskId).then(() => {
6839
7114
  if (agent.usesWorktree) {
6840
7115
  try {
@@ -6851,30 +7126,30 @@ var ProjectRunner = class {
6851
7126
  const currentBranch = this.getCurrentBranch();
6852
7127
  if (currentBranch === workspaceBranch) return;
6853
7128
  if (hasUncommittedChanges(this.projectDir)) {
6854
- logger6.warn("Uncommitted changes, skipping workspace branch checkout");
7129
+ logger7.warn("Uncommitted changes, skipping workspace branch checkout");
6855
7130
  return;
6856
7131
  }
6857
7132
  try {
6858
7133
  execSync5(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
6859
7134
  execSync5(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
6860
- logger6.info("Checked out workspace branch", { workspaceBranch });
7135
+ logger7.info("Checked out workspace branch", { workspaceBranch });
6861
7136
  } catch {
6862
- logger6.warn("Failed to checkout workspace branch", { workspaceBranch });
7137
+ logger7.warn("Failed to checkout workspace branch", { workspaceBranch });
6863
7138
  }
6864
7139
  }
6865
7140
  async executeSetupCommand() {
6866
7141
  const cmd = process.env.CONVEYOR_SETUP_COMMAND;
6867
7142
  if (!cmd) return;
6868
- logger6.info("Running setup command", { command: cmd });
7143
+ logger7.info("Running setup command", { command: cmd });
6869
7144
  try {
6870
7145
  await runSetupCommand(cmd, this.projectDir, (stream, data) => {
6871
7146
  this.connection.sendEvent({ type: "setup_output", stream, data });
6872
7147
  (stream === "stderr" ? process.stderr : process.stdout).write(data);
6873
7148
  });
6874
- logger6.info("Setup command completed");
7149
+ logger7.info("Setup command completed");
6875
7150
  } catch (error) {
6876
7151
  const msg = error instanceof Error ? error.message : "Setup command failed";
6877
- logger6.error("Setup command failed", { error: msg });
7152
+ logger7.error("Setup command failed", { error: msg });
6878
7153
  this.connection.sendEvent({ type: "setup_error", message: msg });
6879
7154
  throw error;
6880
7155
  }
@@ -6882,7 +7157,7 @@ var ProjectRunner = class {
6882
7157
  executeStartCommand() {
6883
7158
  const cmd = process.env.CONVEYOR_START_COMMAND;
6884
7159
  if (!cmd) return;
6885
- logger6.info("Running start command", { command: cmd });
7160
+ logger7.info("Running start command", { command: cmd });
6886
7161
  const child = runStartCommand(cmd, this.projectDir, (stream, data) => {
6887
7162
  this.connection.sendEvent({ type: "start_command_output", stream, data });
6888
7163
  (stream === "stderr" ? process.stderr : process.stdout).write(data);
@@ -6892,13 +7167,13 @@ var ProjectRunner = class {
6892
7167
  child.on("exit", (code, signal) => {
6893
7168
  this.startCommandRunning = false;
6894
7169
  this.startCommandChild = null;
6895
- logger6.info("Start command exited", { code, signal });
7170
+ logger7.info("Start command exited", { code, signal });
6896
7171
  this.connection.sendEvent({ type: "start_command_exited", code, signal });
6897
7172
  });
6898
7173
  child.on("error", (err) => {
6899
7174
  this.startCommandRunning = false;
6900
7175
  this.startCommandChild = null;
6901
- logger6.error("Start command error", { error: err.message });
7176
+ logger7.error("Start command error", { error: err.message });
6902
7177
  });
6903
7178
  }
6904
7179
  async killStartCommand() {
@@ -6941,7 +7216,7 @@ var ProjectRunner = class {
6941
7216
  try {
6942
7217
  execSync5("git fetch origin", { cwd: this.projectDir, stdio: "pipe" });
6943
7218
  } catch {
6944
- logger6.warn("Git fetch failed during branch switch");
7219
+ logger7.warn("Git fetch failed during branch switch");
6945
7220
  }
6946
7221
  detachWorktreeBranch(this.projectDir, data.branch);
6947
7222
  try {
@@ -6954,7 +7229,7 @@ var ProjectRunner = class {
6954
7229
  try {
6955
7230
  execSync5(`git pull origin ${data.branch}`, { cwd: this.projectDir, stdio: "pipe" });
6956
7231
  } catch {
6957
- logger6.warn("Git pull failed during branch switch");
7232
+ logger7.warn("Git pull failed during branch switch");
6958
7233
  }
6959
7234
  if (data.syncAfter !== false) {
6960
7235
  await this.handleSyncEnvironment();
@@ -6963,7 +7238,7 @@ var ProjectRunner = class {
6963
7238
  callback({ ok: true });
6964
7239
  } catch (err) {
6965
7240
  const msg = err instanceof Error ? err.message : "Branch switch failed";
6966
- logger6.error("Branch switch failed", { error: msg });
7241
+ logger7.error("Branch switch failed", { error: msg });
6967
7242
  callback({ ok: false, error: msg });
6968
7243
  }
6969
7244
  }
@@ -6978,14 +7253,14 @@ var ProjectRunner = class {
6978
7253
  });
6979
7254
  } catch (err) {
6980
7255
  const msg = err instanceof Error ? err.message : "Sync command failed";
6981
- logger6.error("Branch switch sync command failed", { error: msg });
7256
+ logger7.error("Branch switch sync command failed", { error: msg });
6982
7257
  }
6983
7258
  }
6984
7259
  this.executeStartCommand();
6985
7260
  callback?.({ ok: true });
6986
7261
  } catch (err) {
6987
7262
  const msg = err instanceof Error ? err.message : "Sync failed";
6988
- logger6.error("Environment sync failed", { error: msg });
7263
+ logger7.error("Environment sync failed", { error: msg });
6989
7264
  callback?.({ ok: false, error: msg });
6990
7265
  }
6991
7266
  }
@@ -7010,7 +7285,7 @@ var ProjectRunner = class {
7010
7285
  setupComplete: this.setupComplete,
7011
7286
  startCommandRunning: this.startCommandRunning
7012
7287
  });
7013
- logger6.info("Commit sync complete", { steps: stepsRun.join(", ") });
7288
+ logger7.info("Commit sync complete", { steps: stepsRun.join(", ") });
7014
7289
  }
7015
7290
  async smartSync(previousSha, newSha, branch) {
7016
7291
  if (hasUncommittedChanges(this.projectDir)) {
@@ -7029,7 +7304,7 @@ var ProjectRunner = class {
7029
7304
  });
7030
7305
  } catch (err) {
7031
7306
  const msg = err instanceof Error ? err.message : "Pull failed";
7032
- logger6.error("Git pull failed during commit sync", { error: msg });
7307
+ logger7.error("Git pull failed during commit sync", { error: msg });
7033
7308
  this.executeStartCommand();
7034
7309
  return ["error:pull"];
7035
7310
  }
@@ -7056,7 +7331,7 @@ var ProjectRunner = class {
7056
7331
  stepsRun.push("branchSwitchCommand");
7057
7332
  } catch (err) {
7058
7333
  const msg = err instanceof Error ? err.message : "Sync command failed";
7059
- logger6.error("Branch switch command failed", { error: msg });
7334
+ logger7.error("Branch switch command failed", { error: msg });
7060
7335
  }
7061
7336
  } else if (!cmd) {
7062
7337
  this.runIndividualSyncSteps(needsInstall, needsPrisma, stepsRun);
@@ -7069,7 +7344,7 @@ var ProjectRunner = class {
7069
7344
  stepsRun.push("install");
7070
7345
  } catch (err) {
7071
7346
  const msg = err instanceof Error ? err.message : "Install failed";
7072
- logger6.error("bun install failed", { error: msg });
7347
+ logger7.error("bun install failed", { error: msg });
7073
7348
  }
7074
7349
  }
7075
7350
  if (needsPrisma) {
@@ -7083,7 +7358,7 @@ var ProjectRunner = class {
7083
7358
  stepsRun.push("prisma");
7084
7359
  } catch (err) {
7085
7360
  const msg = err instanceof Error ? err.message : "Prisma sync failed";
7086
- logger6.error("Prisma sync failed", { error: msg });
7361
+ logger7.error("Prisma sync failed", { error: msg });
7087
7362
  }
7088
7363
  }
7089
7364
  }
@@ -7099,6 +7374,33 @@ var ProjectRunner = class {
7099
7374
  }
7100
7375
  };
7101
7376
 
7377
+ // src/setup/config.ts
7378
+ import { readFile as readFile2 } from "fs/promises";
7379
+ import { join as join3 } from "path";
7380
+ var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
7381
+ async function loadForwardPorts(workspaceDir) {
7382
+ try {
7383
+ const raw = await readFile2(join3(workspaceDir, DEVCONTAINER_PATH), "utf-8");
7384
+ const parsed = JSON.parse(raw);
7385
+ return parsed.forwardPorts ?? [];
7386
+ } catch {
7387
+ return [];
7388
+ }
7389
+ }
7390
+ function loadConveyorConfig() {
7391
+ const envSetup = process.env.CONVEYOR_SETUP_COMMAND;
7392
+ const envStart = process.env.CONVEYOR_START_COMMAND;
7393
+ const envPort = process.env.CONVEYOR_PREVIEW_PORT;
7394
+ if (envSetup || envStart) {
7395
+ return {
7396
+ setupCommand: envSetup,
7397
+ startCommand: envStart,
7398
+ previewPort: envPort ? Number(envPort) : void 0
7399
+ };
7400
+ }
7401
+ return null;
7402
+ }
7403
+
7102
7404
  export {
7103
7405
  AgentConnection,
7104
7406
  ModeController,
@@ -7121,6 +7423,8 @@ export {
7121
7423
  ensureWorktree,
7122
7424
  detachWorktreeBranch,
7123
7425
  removeWorktree,
7124
- ProjectRunner
7426
+ ProjectRunner,
7427
+ loadForwardPorts,
7428
+ loadConveyorConfig
7125
7429
  };
7126
- //# sourceMappingURL=chunk-K2TJHPMA.js.map
7430
+ //# sourceMappingURL=chunk-3H2FXJFD.js.map