@rallycry/conveyor-agent 6.0.7 → 6.0.8

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.
@@ -487,6 +487,10 @@ var ConveyorConnection = class _ConveyorConnection {
487
487
  if (!this.socket) return;
488
488
  this.socket.emit("agentRunner:debugReproduceRequested", { hypothesis });
489
489
  }
490
+ emitCodeReviewResult(content, approved) {
491
+ if (!this.socket) throw new Error("Not connected");
492
+ this.socket.emit("agentRunner:codeReviewResult", { content, approved });
493
+ }
490
494
  searchIncidents(status, source) {
491
495
  return searchIncidents(this.socket, status, source);
492
496
  }
@@ -1487,7 +1491,13 @@ var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
1487
1491
  function stopTypingIfNeeded(host, isTyping) {
1488
1492
  if (isTyping) host.connection.sendTypingStop();
1489
1493
  }
1490
- function flushPendingToolCalls(host, turnToolCalls) {
1494
+ function shouldBreakEventLoop(host, state) {
1495
+ if (host.isStopped()) return true;
1496
+ if (state.loopDetected) return true;
1497
+ return false;
1498
+ }
1499
+ function flushPendingToolCalls(host, state) {
1500
+ const { turnToolCalls } = state;
1491
1501
  if (turnToolCalls.length === 0) return;
1492
1502
  for (let i = 0; i < turnToolCalls.length; i++) {
1493
1503
  if (i < host.pendingToolOutputs.length) {
@@ -1495,6 +1505,12 @@ function flushPendingToolCalls(host, turnToolCalls) {
1495
1505
  }
1496
1506
  }
1497
1507
  host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
1508
+ if (host.loopDetector.recordTurn(turnToolCalls)) {
1509
+ state.loopDetected = true;
1510
+ host.connection.postChatMessage(
1511
+ "Loop detected: the agent has been repeating the same tool pattern. Intervening to change strategy..."
1512
+ );
1513
+ }
1498
1514
  turnToolCalls.length = 0;
1499
1515
  host.pendingToolOutputs.length = 0;
1500
1516
  }
@@ -1552,12 +1568,13 @@ async function processEvents(events, context, host) {
1552
1568
  rateLimitResetsAt: void 0,
1553
1569
  staleSession: void 0,
1554
1570
  authError: void 0,
1571
+ loopDetected: false,
1555
1572
  lastAssistantUsage: void 0,
1556
1573
  turnToolCalls: []
1557
1574
  };
1558
1575
  for await (const event of events) {
1559
- if (host.isStopped()) break;
1560
- flushPendingToolCalls(host, state.turnToolCalls);
1576
+ flushPendingToolCalls(host, state);
1577
+ if (shouldBreakEventLoop(host, state)) break;
1561
1578
  const now = Date.now();
1562
1579
  if (now - lastStatusEmit >= STATUS_REEMIT_INTERVAL_MS) {
1563
1580
  host.connection.emitStatus("running");
@@ -1587,14 +1604,15 @@ async function processEvents(events, context, host) {
1587
1604
  break;
1588
1605
  }
1589
1606
  }
1590
- flushPendingToolCalls(host, state.turnToolCalls);
1607
+ flushPendingToolCalls(host, state);
1591
1608
  stopTypingIfNeeded(host, state.isTyping);
1592
1609
  return {
1593
1610
  retriable: state.retriable || state.sawApiError,
1594
1611
  resultSummary: state.resultSummary,
1595
1612
  rateLimitResetsAt: state.rateLimitResetsAt,
1596
1613
  ...state.staleSession && { staleSession: state.staleSession },
1597
- ...state.authError && { authError: state.authError }
1614
+ ...state.authError && { authError: state.authError },
1615
+ ...state.loopDetected && { loopDetected: state.loopDetected }
1598
1616
  };
1599
1617
  }
1600
1618
 
@@ -2035,10 +2053,52 @@ function buildModePrompt(agentMode, context) {
2035
2053
  ].join("\n");
2036
2054
  case "auto":
2037
2055
  return buildAutoPrompt(context);
2056
+ case "code-review":
2057
+ return buildCodeReviewPrompt();
2038
2058
  default:
2039
2059
  return null;
2040
2060
  }
2041
2061
  }
2062
+ function buildCodeReviewPrompt() {
2063
+ return [
2064
+ `
2065
+ ## Mode: Code Review`,
2066
+ `You are an automated code reviewer. A PR has passed all CI checks and you are performing a final code quality review before merge.`,
2067
+ ``,
2068
+ `## Review Process`,
2069
+ `1. Run \`git diff <devBranch>..HEAD\` to see all changes in this PR`,
2070
+ `2. Read the task plan to understand the intended changes`,
2071
+ `3. Explore the surrounding codebase to verify pattern consistency`,
2072
+ `4. Review against the criteria below`,
2073
+ ``,
2074
+ `### Review Criteria`,
2075
+ `- **Correctness**: Does the code do what the plan says? Logic errors, off-by-one, race conditions?`,
2076
+ `- **Pattern Consistency**: Does the code follow existing patterns in the codebase? Check nearby files.`,
2077
+ `- **Security**: No hardcoded secrets, no injection vulnerabilities, proper input validation at boundaries.`,
2078
+ `- **Performance**: No unnecessary loops, no N+1 queries, no blocking in async contexts.`,
2079
+ `- **Error Handling**: Appropriate error handling at system boundaries. No swallowed errors.`,
2080
+ `- **Test Coverage**: Are new code paths tested? Edge cases covered?`,
2081
+ `- **TypeScript Best Practices**: Proper typing (no unnecessary \`any\`), correct React patterns, proper async/await.`,
2082
+ `- **Naming & Readability**: Clear names, no misleading comments, self-documenting code.`,
2083
+ ``,
2084
+ `## Output \u2014 You MUST do exactly ONE of:`,
2085
+ ``,
2086
+ `### If code passes review:`,
2087
+ `Use the \`approve_code_review\` tool with a brief summary of what looks good.`,
2088
+ ``,
2089
+ `### If changes are needed:`,
2090
+ `Use the \`request_code_changes\` tool with specific issues:`,
2091
+ `- Reference specific files and line numbers`,
2092
+ `- Explain what's wrong and suggest fixes`,
2093
+ `- Focus on substantive issues, not style nitpicks (linting handles that)`,
2094
+ ``,
2095
+ `## Rules`,
2096
+ `- You are READ-ONLY. Do NOT modify any files.`,
2097
+ `- Do NOT re-review things CI already validates (formatting, lint rules).`,
2098
+ `- Be concise \u2014 the task agent needs actionable feedback, not essays.`,
2099
+ `- Max 5-7 issues per review. Prioritize the most important ones.`
2100
+ ].join("\n");
2101
+ }
2042
2102
 
2043
2103
  // src/execution/system-prompt.ts
2044
2104
  function formatProjectAgentLine(pa) {
@@ -2410,6 +2470,18 @@ ${context.plan}`);
2410
2470
  }
2411
2471
  return parts;
2412
2472
  }
2473
+ function buildCodeReviewInstructions(context) {
2474
+ const parts = [
2475
+ `You are performing an automated code review for this task.`,
2476
+ `The PR branch is "${context.githubBranch}"${context.baseBranch ? ` based on "${context.baseBranch}"` : ""}.`,
2477
+ `Begin your code review by running \`git diff ${context.baseBranch ?? "dev"}..HEAD\` to see all changes.`,
2478
+ ``,
2479
+ `CRITICAL: You are in Code Review mode. You are READ-ONLY \u2014 do NOT modify any files.`,
2480
+ `After reviewing, you MUST call exactly one of: \`approve_code_review\` or \`request_code_changes\`.`,
2481
+ `Do NOT go idle, ask for confirmation, or wait for instructions \u2014 complete the review and exit.`
2482
+ ];
2483
+ return parts;
2484
+ }
2413
2485
  function buildFreshInstructions(isPm, isAutoMode, context, agentMode) {
2414
2486
  if (isPm && agentMode === "building") {
2415
2487
  return [
@@ -2507,6 +2579,10 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
2507
2579
  function buildInstructions(mode, context, scenario, agentMode, isAuto) {
2508
2580
  const parts = [`
2509
2581
  ## Instructions`];
2582
+ if (agentMode === "code-review") {
2583
+ parts.push(...buildCodeReviewInstructions(context));
2584
+ return parts;
2585
+ }
2510
2586
  const isPm = mode === "pm";
2511
2587
  if (scenario === "fresh") {
2512
2588
  parts.push(...buildFreshInstructions(isPm, agentMode === "auto", context, agentMode));
@@ -2558,7 +2634,7 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
2558
2634
  }
2559
2635
  async function buildInitialPrompt(mode, context, isAuto, agentMode) {
2560
2636
  const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
2561
- if (!isPackRunner) {
2637
+ if (!isPackRunner && agentMode !== "code-review") {
2562
2638
  const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
2563
2639
  if (sessionRelaunch) return sessionRelaunch;
2564
2640
  }
@@ -4365,6 +4441,73 @@ function buildDebugTools(manager) {
4365
4441
  ];
4366
4442
  }
4367
4443
 
4444
+ // src/tools/code-review-tools.ts
4445
+ import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
4446
+ import { z as z7 } from "zod";
4447
+ function buildCodeReviewTools(connection) {
4448
+ return [
4449
+ tool7(
4450
+ "approve_code_review",
4451
+ "Approve the code review. Use this when the code passes all review criteria and is ready to merge.",
4452
+ {
4453
+ summary: z7.string().describe("Brief summary of what was reviewed and why it looks good")
4454
+ },
4455
+ // eslint-disable-next-line require-await -- SDK tool() API requires async handler
4456
+ async ({ summary }) => {
4457
+ connection.emitCodeReviewResult(
4458
+ `**Code Review: Approved** :white_check_mark:
4459
+
4460
+ ${summary}`,
4461
+ true
4462
+ );
4463
+ connection.sendEvent({
4464
+ type: "code_review_complete",
4465
+ result: "approved",
4466
+ summary
4467
+ });
4468
+ return textResult("Code review approved. Exiting.");
4469
+ }
4470
+ ),
4471
+ tool7(
4472
+ "request_code_changes",
4473
+ "Request changes during code review. Use this when substantive issues are found that need to be fixed before merge.",
4474
+ {
4475
+ issues: z7.array(
4476
+ z7.object({
4477
+ file: z7.string().describe("File path where the issue was found"),
4478
+ line: z7.number().optional().describe("Line number (if applicable)"),
4479
+ severity: z7.enum(["critical", "major", "minor"]).describe("Issue severity"),
4480
+ description: z7.string().describe("What is wrong and how to fix it")
4481
+ })
4482
+ ).describe("List of issues found during review"),
4483
+ summary: z7.string().describe("Brief overall summary of the review findings")
4484
+ },
4485
+ // eslint-disable-next-line require-await -- SDK tool() API requires async handler
4486
+ async ({ issues, summary }) => {
4487
+ const issueLines = issues.map((issue) => {
4488
+ const loc = issue.line ? `:${issue.line}` : "";
4489
+ return `- **[${issue.severity}]** \`${issue.file}${loc}\`: ${issue.description}`;
4490
+ }).join("\n");
4491
+ connection.emitCodeReviewResult(
4492
+ `**Code Review: Changes Requested** :warning:
4493
+
4494
+ ${summary}
4495
+
4496
+ ${issueLines}`,
4497
+ false
4498
+ );
4499
+ connection.sendEvent({
4500
+ type: "code_review_complete",
4501
+ result: "changes_requested",
4502
+ summary,
4503
+ issues
4504
+ });
4505
+ return textResult("Code review complete \u2014 changes requested. Exiting.");
4506
+ }
4507
+ )
4508
+ ];
4509
+ }
4510
+
4368
4511
  // src/tools/index.ts
4369
4512
  function textResult(text) {
4370
4513
  return { content: [{ type: "text", text }] };
@@ -4395,8 +4538,22 @@ function getModeTools(agentMode, connection, config, context) {
4395
4538
  }
4396
4539
  }
4397
4540
  function createConveyorMcpServer(connection, config, context, agentMode, debugManager) {
4398
- const commonTools = buildCommonTools(connection, config);
4399
4541
  const effectiveMode = agentMode ?? context?.agentMode ?? void 0;
4542
+ if (effectiveMode === "code-review") {
4543
+ return createSdkMcpServer({
4544
+ name: "conveyor",
4545
+ tools: [
4546
+ buildReadTaskChatTool(connection),
4547
+ buildGetTaskPlanTool(connection, config),
4548
+ buildGetTaskTool(connection),
4549
+ buildGetTaskCliTool(connection),
4550
+ buildListTaskFilesTool(connection),
4551
+ buildGetTaskFileTool(connection),
4552
+ ...buildCodeReviewTools(connection)
4553
+ ]
4554
+ });
4555
+ }
4556
+ const commonTools = buildCommonTools(connection, config);
4400
4557
  const modeTools = getModeTools(effectiveMode, connection, config, context);
4401
4558
  const discoveryTools = effectiveMode === "discovery" || effectiveMode === "auto" ? buildDiscoveryTools(connection, context) : [];
4402
4559
  const debugTools = debugManager && effectiveMode === "building" ? buildDebugTools(debugManager) : [];
@@ -4411,6 +4568,7 @@ function createConveyorMcpServer(connection, config, context, agentMode, debugMa
4411
4568
  import { randomUUID } from "crypto";
4412
4569
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
4413
4570
  var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
4571
+ var CODE_REVIEW_WRITE_CMD_PATTERN = /git\s+push|git\s+commit|git\s+add|rm\s+|mv\s+|cp\s+|mkdir\s+|touch\s+|chmod\s+|chown\s+/;
4414
4572
  function isPlanFile(input) {
4415
4573
  const filePath = String(input.file_path ?? input.path ?? "");
4416
4574
  return filePath.includes(".claude/plans/");
@@ -4460,6 +4618,24 @@ function handleReviewToolAccess(toolName, input, isParentTask) {
4460
4618
  }
4461
4619
  return { behavior: "allow", updatedInput: input };
4462
4620
  }
4621
+ function handleCodeReviewToolAccess(toolName, input) {
4622
+ if (PM_PLAN_FILE_TOOLS.has(toolName)) {
4623
+ return {
4624
+ behavior: "deny",
4625
+ message: "Code review mode is fully read-only. File writes are not permitted."
4626
+ };
4627
+ }
4628
+ if (toolName === "Bash") {
4629
+ const cmd = String(input.command ?? "");
4630
+ if (DESTRUCTIVE_CMD_PATTERN.test(cmd) || CODE_REVIEW_WRITE_CMD_PATTERN.test(cmd)) {
4631
+ return {
4632
+ behavior: "deny",
4633
+ message: "Code review mode is read-only. Write operations and destructive commands are blocked."
4634
+ };
4635
+ }
4636
+ }
4637
+ return { behavior: "allow", updatedInput: input };
4638
+ }
4463
4639
  function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask) {
4464
4640
  if (hasExitedPlanMode) {
4465
4641
  return isParentTask ? handleReviewToolAccess(toolName, input, true) : handleBuildingToolAccess(toolName, input);
@@ -4564,6 +4740,9 @@ function buildCanUseTool(host) {
4564
4740
  case "auto":
4565
4741
  result = handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
4566
4742
  break;
4743
+ case "code-review":
4744
+ result = handleCodeReviewToolAccess(toolName, input);
4745
+ break;
4567
4746
  default:
4568
4747
  result = { behavior: "allow", updatedInput: input };
4569
4748
  }
@@ -4611,10 +4790,13 @@ function buildHooks(host) {
4611
4790
  };
4612
4791
  }
4613
4792
  function isReadOnlyMode(mode, hasExitedPlanMode) {
4614
- return mode === "discovery" || mode === "help" || mode === "auto" && !hasExitedPlanMode;
4793
+ return mode === "discovery" || mode === "help" || mode === "code-review" || mode === "auto" && !hasExitedPlanMode;
4615
4794
  }
4616
4795
  function buildDisallowedTools(settings, mode, hasExitedPlanMode) {
4617
4796
  const modeDisallowed = isReadOnlyMode(mode, hasExitedPlanMode) ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
4797
+ if (mode === "code-review") {
4798
+ modeDisallowed.push("ExitPlanMode", "EnterPlanMode");
4799
+ }
4618
4800
  const configured = settings.disallowedTools ?? [];
4619
4801
  const combined = [...configured, ...modeDisallowed];
4620
4802
  return combined.length > 0 ? combined : void 0;
@@ -4647,11 +4829,11 @@ function buildQueryOptions(host, context) {
4647
4829
  tools: { type: "preset", preset: "claude_code" },
4648
4830
  mcpServers: { conveyor: createConveyorMcpServer(host.connection, host.config, context, mode) },
4649
4831
  hooks: buildHooks(host),
4650
- maxTurns: settings.maxTurns,
4832
+ maxTurns: mode === "code-review" ? Math.min(settings.maxTurns ?? 15, 15) : settings.maxTurns,
4651
4833
  effort: settings.effort,
4652
4834
  thinking: settings.thinking,
4653
4835
  betas: settings.betas,
4654
- maxBudgetUsd: settings.maxBudgetUsd ?? 50,
4836
+ maxBudgetUsd: mode === "code-review" ? Math.min(settings.maxBudgetUsd ?? 10, 10) : settings.maxBudgetUsd ?? 50,
4655
4837
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
4656
4838
  enableFileCheckpointing: settings.enableFileCheckpointing,
4657
4839
  stderr: (data) => {
@@ -4891,6 +5073,17 @@ function handleProcessResult(result, context, host, options) {
4891
5073
  if (result.authError) {
4892
5074
  return { action: "return_promise", promise: handleAuthError(context, host, options) };
4893
5075
  }
5076
+ if (result.loopDetected) {
5077
+ host.loopDetector.reset();
5078
+ return {
5079
+ action: "return_promise",
5080
+ promise: runSdkQuery(
5081
+ host,
5082
+ context,
5083
+ "You've been repeating the same tool pattern for several consecutive turns. Step back, analyze why your current approach is failing, and try a fundamentally different strategy. Do NOT repeat the same sequence of actions."
5084
+ )
5085
+ };
5086
+ }
4894
5087
  if (!result.retriable) return { action: "return" };
4895
5088
  return {
4896
5089
  action: "continue",
@@ -4960,6 +5153,38 @@ var CostTracker = class {
4960
5153
  }
4961
5154
  };
4962
5155
 
5156
+ // src/execution/loop-detector.ts
5157
+ var DEFAULT_MAX_CONSECUTIVE_REPEATS = 3;
5158
+ var LoopDetector = class _LoopDetector {
5159
+ fingerprints = [];
5160
+ maxConsecutiveRepeats;
5161
+ constructor(maxConsecutiveRepeats = DEFAULT_MAX_CONSECUTIVE_REPEATS) {
5162
+ this.maxConsecutiveRepeats = maxConsecutiveRepeats;
5163
+ }
5164
+ /** Build a fingerprint from a turn's tool calls: sorted unique tool names joined by comma. */
5165
+ static fingerprint(toolCalls) {
5166
+ const names = [...new Set(toolCalls.map((tc) => tc.tool))].sort();
5167
+ return names.join(",");
5168
+ }
5169
+ /** Record a completed turn and return whether a loop is detected. */
5170
+ recordTurn(toolCalls) {
5171
+ if (toolCalls.length === 0) return false;
5172
+ const fp = _LoopDetector.fingerprint(toolCalls);
5173
+ this.fingerprints.push(fp);
5174
+ if (this.fingerprints.length < this.maxConsecutiveRepeats) return false;
5175
+ const recent = this.fingerprints.slice(-this.maxConsecutiveRepeats);
5176
+ return recent.every((f) => f === fp);
5177
+ }
5178
+ /** Reset state (e.g. after a successful intervention breaks the loop). */
5179
+ reset() {
5180
+ this.fingerprints.length = 0;
5181
+ }
5182
+ /** Number of recorded turns. */
5183
+ get turnCount() {
5184
+ return this.fingerprints.length;
5185
+ }
5186
+ };
5187
+
4963
5188
  // src/runner/plan-sync.ts
4964
5189
  import { readdirSync, statSync, readFileSync } from "fs";
4965
5190
  import { join as join3 } from "path";
@@ -5311,6 +5536,7 @@ function buildQueryHost(deps) {
5311
5536
  callbacks: deps.callbacks,
5312
5537
  setupLog: deps.setupLog,
5313
5538
  costTracker: deps.costTracker,
5539
+ loopDetector: deps.loopDetector,
5314
5540
  get agentMode() {
5315
5541
  return deps.getEffectiveAgentMode();
5316
5542
  },
@@ -5358,10 +5584,12 @@ var AgentRunner = class {
5358
5584
  taskContext = null;
5359
5585
  planSync;
5360
5586
  costTracker = new CostTracker();
5587
+ loopDetector = new LoopDetector();
5361
5588
  worktreeActive = false;
5362
5589
  agentMode = null;
5363
5590
  hasExitedPlanMode = false;
5364
5591
  pendingModeRestart = false;
5592
+ pendingModeAutoStart = false;
5365
5593
  sessionIds = /* @__PURE__ */ new Map();
5366
5594
  lastQueryModeRestart = false;
5367
5595
  startCommandStarted = false;
@@ -5565,6 +5793,12 @@ var AgentRunner = class {
5565
5793
  async executeInitialMode() {
5566
5794
  if (!this.taskContext) return;
5567
5795
  const mode = this.effectiveAgentMode;
5796
+ if (mode === "code-review") {
5797
+ await this.setState("running");
5798
+ await this.runQuerySafe(this.taskContext);
5799
+ this.stopped = true;
5800
+ return;
5801
+ }
5568
5802
  const shouldRun = mode === "building" || mode === "auto" || mode === "review" && !!this.taskContext.isParentTask;
5569
5803
  if (shouldRun) {
5570
5804
  await this.setState("running");
@@ -5621,6 +5855,12 @@ var AgentRunner = class {
5621
5855
  await this.handleModeRestartCycle();
5622
5856
  continue;
5623
5857
  }
5858
+ if (this.pendingModeAutoStart) {
5859
+ this.pendingModeAutoStart = false;
5860
+ this.interrupted = false;
5861
+ await this.handleModeRestartCycle();
5862
+ continue;
5863
+ }
5624
5864
  if (this._state === "idle") {
5625
5865
  const msg = await this.waitForUserContent();
5626
5866
  if (!msg) {
@@ -5748,6 +5988,7 @@ var AgentRunner = class {
5748
5988
  callbacks: this.callbacks,
5749
5989
  setupLog: this.setupLog,
5750
5990
  costTracker: this.costTracker,
5991
+ loopDetector: this.loopDetector,
5751
5992
  planSync: this.planSync,
5752
5993
  sessionIds: this.sessionIds,
5753
5994
  getEffectiveAgentMode: () => this.effectiveAgentMode,
@@ -5772,15 +6013,32 @@ var AgentRunner = class {
5772
6013
  handleModeChange(newAgentMode) {
5773
6014
  if (this.config.mode !== "pm") return;
5774
6015
  if (newAgentMode) this.agentMode = newAgentMode;
6016
+ this.updateExitedPlanModeFlag(newAgentMode);
5775
6017
  const effectiveMode = this.effectiveAgentMode;
6018
+ const isBuildCapable = effectiveMode === "building" || effectiveMode === "auto" && this.hasExitedPlanMode;
5776
6019
  this.connection.emitModeChanged(effectiveMode);
5777
6020
  this.connection.postChatMessage(
5778
6021
  `Mode switched to **${effectiveMode}**${effectiveMode === "building" ? " \u2014 I now have direct coding access." : ""}`
5779
6022
  );
5780
- if (effectiveMode === "building" && this.taskContext?.status === "Open") {
6023
+ if (isBuildCapable && this.taskContext?.status === "Open") {
5781
6024
  this.connection.updateStatus("InProgress");
5782
6025
  this.taskContext.status = "InProgress";
5783
6026
  }
6027
+ if (isBuildCapable) {
6028
+ this.pendingModeAutoStart = true;
6029
+ this.softStop();
6030
+ }
6031
+ }
6032
+ updateExitedPlanModeFlag(newAgentMode) {
6033
+ if (newAgentMode === "building") {
6034
+ this.hasExitedPlanMode = true;
6035
+ return;
6036
+ }
6037
+ if (newAgentMode !== "auto") return;
6038
+ const pastPlanning = this.taskContext?.status !== "Planning" && this.taskContext?.status !== "Unidentified";
6039
+ if (pastPlanning) {
6040
+ this.hasExitedPlanMode = true;
6041
+ }
5784
6042
  }
5785
6043
  softStop() {
5786
6044
  this.interrupted = true;
@@ -5926,17 +6184,17 @@ import {
5926
6184
  } from "@anthropic-ai/claude-agent-sdk";
5927
6185
 
5928
6186
  // src/tools/project-tools.ts
5929
- import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
5930
- import { z as z7 } from "zod";
6187
+ import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
6188
+ import { z as z8 } from "zod";
5931
6189
  function buildReadTools(connection) {
5932
6190
  return [
5933
- tool7(
6191
+ tool8(
5934
6192
  "list_tasks",
5935
6193
  "List tasks in the project. Optionally filter by status or assignee.",
5936
6194
  {
5937
- status: z7.string().optional().describe("Filter by task status (e.g. Planning, Open, InProgress, ReviewPR, Complete)"),
5938
- assigneeId: z7.string().optional().describe("Filter by assigned user ID"),
5939
- limit: z7.number().optional().describe("Max number of tasks to return (default 50)")
6195
+ status: z8.string().optional().describe("Filter by task status (e.g. Planning, Open, InProgress, ReviewPR, Complete)"),
6196
+ assigneeId: z8.string().optional().describe("Filter by assigned user ID"),
6197
+ limit: z8.number().optional().describe("Max number of tasks to return (default 50)")
5940
6198
  },
5941
6199
  async (params) => {
5942
6200
  try {
@@ -5950,10 +6208,10 @@ function buildReadTools(connection) {
5950
6208
  },
5951
6209
  { annotations: { readOnlyHint: true } }
5952
6210
  ),
5953
- tool7(
6211
+ tool8(
5954
6212
  "get_task",
5955
6213
  "Get detailed information about a task including its chat messages, child tasks, and codespace status.",
5956
- { task_id: z7.string().describe("The task ID to look up") },
6214
+ { task_id: z8.string().describe("The task ID to look up") },
5957
6215
  async ({ task_id }) => {
5958
6216
  try {
5959
6217
  const task = await connection.requestGetTask(task_id);
@@ -5966,14 +6224,14 @@ function buildReadTools(connection) {
5966
6224
  },
5967
6225
  { annotations: { readOnlyHint: true } }
5968
6226
  ),
5969
- tool7(
6227
+ tool8(
5970
6228
  "search_tasks",
5971
6229
  "Search tasks by tags, text query, or status filters.",
5972
6230
  {
5973
- tagNames: z7.array(z7.string()).optional().describe("Filter by tag names"),
5974
- searchQuery: z7.string().optional().describe("Text search in title/description"),
5975
- statusFilters: z7.array(z7.string()).optional().describe("Filter by statuses"),
5976
- limit: z7.number().optional().describe("Max results (default 20)")
6231
+ tagNames: z8.array(z8.string()).optional().describe("Filter by tag names"),
6232
+ searchQuery: z8.string().optional().describe("Text search in title/description"),
6233
+ statusFilters: z8.array(z8.string()).optional().describe("Filter by statuses"),
6234
+ limit: z8.number().optional().describe("Max results (default 20)")
5977
6235
  },
5978
6236
  async (params) => {
5979
6237
  try {
@@ -5987,7 +6245,7 @@ function buildReadTools(connection) {
5987
6245
  },
5988
6246
  { annotations: { readOnlyHint: true } }
5989
6247
  ),
5990
- tool7(
6248
+ tool8(
5991
6249
  "list_tags",
5992
6250
  "List all tags available in the project.",
5993
6251
  {},
@@ -6003,7 +6261,7 @@ function buildReadTools(connection) {
6003
6261
  },
6004
6262
  { annotations: { readOnlyHint: true } }
6005
6263
  ),
6006
- tool7(
6264
+ tool8(
6007
6265
  "get_project_summary",
6008
6266
  "Get a summary of the project including task counts by status and active builds.",
6009
6267
  {},
@@ -6023,15 +6281,15 @@ function buildReadTools(connection) {
6023
6281
  }
6024
6282
  function buildMutationTools(connection) {
6025
6283
  return [
6026
- tool7(
6284
+ tool8(
6027
6285
  "create_task",
6028
6286
  "Create a new task in the project.",
6029
6287
  {
6030
- title: z7.string().describe("Task title"),
6031
- description: z7.string().optional().describe("Task description"),
6032
- plan: z7.string().optional().describe("Implementation plan in markdown"),
6033
- status: z7.string().optional().describe("Initial status (default: Planning)"),
6034
- isBug: z7.boolean().optional().describe("Whether this is a bug report")
6288
+ title: z8.string().describe("Task title"),
6289
+ description: z8.string().optional().describe("Task description"),
6290
+ plan: z8.string().optional().describe("Implementation plan in markdown"),
6291
+ status: z8.string().optional().describe("Initial status (default: Planning)"),
6292
+ isBug: z8.boolean().optional().describe("Whether this is a bug report")
6035
6293
  },
6036
6294
  async (params) => {
6037
6295
  try {
@@ -6044,16 +6302,16 @@ function buildMutationTools(connection) {
6044
6302
  }
6045
6303
  }
6046
6304
  ),
6047
- tool7(
6305
+ tool8(
6048
6306
  "update_task",
6049
6307
  "Update an existing task's title, description, plan, status, or assignee.",
6050
6308
  {
6051
- task_id: z7.string().describe("The task ID to update"),
6052
- title: z7.string().optional().describe("New title"),
6053
- description: z7.string().optional().describe("New description"),
6054
- plan: z7.string().optional().describe("New plan in markdown"),
6055
- status: z7.string().optional().describe("New status"),
6056
- assignedUserId: z7.string().nullable().optional().describe("Assign to user ID, or null to unassign")
6309
+ task_id: z8.string().describe("The task ID to update"),
6310
+ title: z8.string().optional().describe("New title"),
6311
+ description: z8.string().optional().describe("New description"),
6312
+ plan: z8.string().optional().describe("New plan in markdown"),
6313
+ status: z8.string().optional().describe("New status"),
6314
+ assignedUserId: z8.string().nullable().optional().describe("Assign to user ID, or null to unassign")
6057
6315
  },
6058
6316
  async ({ task_id, ...fields }) => {
6059
6317
  try {
@@ -6285,8 +6543,8 @@ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
6285
6543
 
6286
6544
  // src/tools/audit-tools.ts
6287
6545
  import { randomUUID as randomUUID3 } from "crypto";
6288
- import { tool as tool8, createSdkMcpServer as createSdkMcpServer3 } from "@anthropic-ai/claude-agent-sdk";
6289
- import { z as z8 } from "zod";
6546
+ import { tool as tool9, createSdkMcpServer as createSdkMcpServer3 } from "@anthropic-ai/claude-agent-sdk";
6547
+ import { z as z9 } from "zod";
6290
6548
  function mapCreateTag(input) {
6291
6549
  return {
6292
6550
  type: "create_tag",
@@ -6368,14 +6626,14 @@ function collectRecommendation(toolName, input, collector, onRecommendation) {
6368
6626
  }
6369
6627
  function createAuditMcpServer(collector, onRecommendation) {
6370
6628
  const auditTools = [
6371
- tool8(
6629
+ tool9(
6372
6630
  "recommend_create_tag",
6373
6631
  "Recommend creating a new tag for an uncovered subsystem or area",
6374
6632
  {
6375
- name: z8.string().describe("Proposed tag name (lowercase, hyphenated)"),
6376
- color: z8.string().optional().describe("Hex color code"),
6377
- description: z8.string().describe("What this tag covers"),
6378
- reasoning: z8.string().describe("Why this tag should be created")
6633
+ name: z9.string().describe("Proposed tag name (lowercase, hyphenated)"),
6634
+ color: z9.string().optional().describe("Hex color code"),
6635
+ description: z9.string().describe("What this tag covers"),
6636
+ reasoning: z9.string().describe("Why this tag should be created")
6379
6637
  },
6380
6638
  async (args) => {
6381
6639
  const result = collectRecommendation(
@@ -6387,14 +6645,14 @@ function createAuditMcpServer(collector, onRecommendation) {
6387
6645
  return { content: [{ type: "text", text: result }] };
6388
6646
  }
6389
6647
  ),
6390
- tool8(
6648
+ tool9(
6391
6649
  "recommend_update_description",
6392
6650
  "Recommend updating a tag's description to better reflect its scope",
6393
6651
  {
6394
- tagId: z8.string(),
6395
- tagName: z8.string(),
6396
- description: z8.string().describe("Proposed new description"),
6397
- reasoning: z8.string()
6652
+ tagId: z9.string(),
6653
+ tagName: z9.string(),
6654
+ description: z9.string().describe("Proposed new description"),
6655
+ reasoning: z9.string()
6398
6656
  },
6399
6657
  async (args) => {
6400
6658
  const result = collectRecommendation(
@@ -6406,16 +6664,16 @@ function createAuditMcpServer(collector, onRecommendation) {
6406
6664
  return { content: [{ type: "text", text: result }] };
6407
6665
  }
6408
6666
  ),
6409
- tool8(
6667
+ tool9(
6410
6668
  "recommend_context_link",
6411
6669
  "Recommend linking a doc, rule, file, or folder to a tag's contextPaths",
6412
6670
  {
6413
- tagId: z8.string(),
6414
- tagName: z8.string(),
6415
- linkType: z8.enum(["rule", "doc", "file", "folder"]),
6416
- path: z8.string(),
6417
- label: z8.string().optional(),
6418
- reasoning: z8.string()
6671
+ tagId: z9.string(),
6672
+ tagName: z9.string(),
6673
+ linkType: z9.enum(["rule", "doc", "file", "folder"]),
6674
+ path: z9.string(),
6675
+ label: z9.string().optional(),
6676
+ reasoning: z9.string()
6419
6677
  },
6420
6678
  async (args) => {
6421
6679
  const result = collectRecommendation(
@@ -6427,16 +6685,16 @@ function createAuditMcpServer(collector, onRecommendation) {
6427
6685
  return { content: [{ type: "text", text: result }] };
6428
6686
  }
6429
6687
  ),
6430
- tool8(
6688
+ tool9(
6431
6689
  "flag_documentation_gap",
6432
6690
  "Flag a file that agents read heavily but has no tag documentation linked",
6433
6691
  {
6434
- tagName: z8.string().describe("Tag whose agents read this file"),
6435
- tagId: z8.string().optional(),
6436
- filePath: z8.string(),
6437
- readCount: z8.number(),
6438
- suggestedAction: z8.string().describe("What doc or rule should be created"),
6439
- reasoning: z8.string()
6692
+ tagName: z9.string().describe("Tag whose agents read this file"),
6693
+ tagId: z9.string().optional(),
6694
+ filePath: z9.string(),
6695
+ readCount: z9.number(),
6696
+ suggestedAction: z9.string().describe("What doc or rule should be created"),
6697
+ reasoning: z9.string()
6440
6698
  },
6441
6699
  async (args) => {
6442
6700
  const result = collectRecommendation(
@@ -6448,15 +6706,15 @@ function createAuditMcpServer(collector, onRecommendation) {
6448
6706
  return { content: [{ type: "text", text: result }] };
6449
6707
  }
6450
6708
  ),
6451
- tool8(
6709
+ tool9(
6452
6710
  "recommend_merge_tags",
6453
6711
  "Recommend merging one tag into another",
6454
6712
  {
6455
- tagId: z8.string().describe("Tag ID to be merged (removed after merge)"),
6456
- tagName: z8.string().describe("Name of the tag to be merged"),
6457
- mergeIntoTagId: z8.string().describe("Tag ID to merge into (kept)"),
6458
- mergeIntoTagName: z8.string(),
6459
- reasoning: z8.string()
6713
+ tagId: z9.string().describe("Tag ID to be merged (removed after merge)"),
6714
+ tagName: z9.string().describe("Name of the tag to be merged"),
6715
+ mergeIntoTagId: z9.string().describe("Tag ID to merge into (kept)"),
6716
+ mergeIntoTagName: z9.string(),
6717
+ reasoning: z9.string()
6460
6718
  },
6461
6719
  async (args) => {
6462
6720
  const result = collectRecommendation(
@@ -6468,14 +6726,14 @@ function createAuditMcpServer(collector, onRecommendation) {
6468
6726
  return { content: [{ type: "text", text: result }] };
6469
6727
  }
6470
6728
  ),
6471
- tool8(
6729
+ tool9(
6472
6730
  "recommend_rename_tag",
6473
6731
  "Recommend renaming a tag",
6474
6732
  {
6475
- tagId: z8.string(),
6476
- tagName: z8.string().describe("Current tag name"),
6477
- newName: z8.string().describe("Proposed new name"),
6478
- reasoning: z8.string()
6733
+ tagId: z9.string(),
6734
+ tagName: z9.string().describe("Current tag name"),
6735
+ newName: z9.string().describe("Proposed new name"),
6736
+ reasoning: z9.string()
6479
6737
  },
6480
6738
  async (args) => {
6481
6739
  const result = collectRecommendation(
@@ -6487,10 +6745,10 @@ function createAuditMcpServer(collector, onRecommendation) {
6487
6745
  return { content: [{ type: "text", text: result }] };
6488
6746
  }
6489
6747
  ),
6490
- tool8(
6748
+ tool9(
6491
6749
  "complete_audit",
6492
6750
  "Signal that the audit is complete with a summary of all findings",
6493
- { summary: z8.string().describe("Brief overview of all findings") },
6751
+ { summary: z9.string().describe("Brief overview of all findings") },
6494
6752
  async (args) => {
6495
6753
  collector.complete = true;
6496
6754
  collector.summary = args.summary ?? "Audit completed.";
@@ -6706,21 +6964,22 @@ function setupWorkDir(projectDir, assignment) {
6706
6964
  return { workDir, usesWorktree: shouldWorktree };
6707
6965
  }
6708
6966
  function spawnChildAgent(assignment, workDir) {
6709
- const { taskToken, apiUrl, taskId, mode, isAuto, useSandbox } = assignment;
6967
+ const { taskToken, apiUrl, taskId, mode, isAuto, useSandbox, agentMode } = assignment;
6710
6968
  const cliPath = path.resolve(__dirname, "cli.js");
6711
6969
  const childEnv = { ...process.env };
6712
6970
  delete childEnv.CONVEYOR_PROJECT_TOKEN;
6713
6971
  delete childEnv.CONVEYOR_PROJECT_ID;
6972
+ const effectiveAgentMode = agentMode ?? (isAuto ? "auto" : "");
6714
6973
  const child = fork(cliPath, [], {
6715
6974
  env: {
6716
6975
  ...childEnv,
6717
6976
  CONVEYOR_API_URL: apiUrl,
6718
6977
  CONVEYOR_TASK_TOKEN: taskToken,
6719
6978
  CONVEYOR_TASK_ID: taskId,
6720
- CONVEYOR_MODE: mode,
6979
+ CONVEYOR_MODE: agentMode === "code-review" ? "code-review" : mode,
6721
6980
  CONVEYOR_WORKSPACE: workDir,
6722
6981
  CONVEYOR_USE_WORKTREE: "false",
6723
- CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
6982
+ CONVEYOR_AGENT_MODE: effectiveAgentMode,
6724
6983
  CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
6725
6984
  CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false",
6726
6985
  CONVEYOR_FROM_PROJECT_RUNNER: "true"
@@ -7155,8 +7414,9 @@ var ProjectRunner = class {
7155
7414
  handleAssignment(assignment) {
7156
7415
  const { taskId, mode } = assignment;
7157
7416
  const shortId = taskId.slice(0, 8);
7158
- if (this.activeAgents.has(taskId)) {
7159
- logger8.info("Task already running, skipping", { taskId: shortId });
7417
+ const agentKey = assignment.agentMode === "code-review" ? `${taskId}:code-review` : taskId;
7418
+ if (this.activeAgents.has(agentKey)) {
7419
+ logger8.info("Task already running, skipping", { taskId: shortId, agentKey });
7160
7420
  return;
7161
7421
  }
7162
7422
  if (this.activeAgents.size >= MAX_CONCURRENT) {
@@ -7175,16 +7435,16 @@ var ProjectRunner = class {
7175
7435
  }
7176
7436
  const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
7177
7437
  const child = spawnChildAgent(assignment, workDir);
7178
- this.activeAgents.set(taskId, {
7438
+ this.activeAgents.set(agentKey, {
7179
7439
  process: child,
7180
7440
  worktreePath: workDir,
7181
7441
  mode,
7182
7442
  usesWorktree
7183
7443
  });
7184
7444
  this.connection.emitTaskStarted(taskId);
7185
- logger8.info("Started task", { taskId: shortId, mode, workDir });
7445
+ logger8.info("Started task", { taskId: shortId, mode, agentKey, workDir });
7186
7446
  child.on("exit", (code) => {
7187
- this.activeAgents.delete(taskId);
7447
+ this.activeAgents.delete(agentKey);
7188
7448
  const reason = code === 0 ? "completed" : `exited with code ${code}`;
7189
7449
  this.connection.emitTaskStopped(taskId, reason);
7190
7450
  logger8.info("Task exited", { taskId: shortId, reason });
@@ -7344,4 +7604,4 @@ export {
7344
7604
  ProjectRunner,
7345
7605
  FileCache
7346
7606
  };
7347
- //# sourceMappingURL=chunk-ANYHEBDY.js.map
7607
+ //# sourceMappingURL=chunk-T6IASOS2.js.map