@rallycry/conveyor-agent 6.0.6 → 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
 
@@ -1638,6 +1656,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
1638
1656
  `- "Open" \u2014 Ready to execute. Use start_child_cloud_build to fire it.`,
1639
1657
  `- "InProgress" \u2014 Currently being worked on by a Task Runner. Wait \u2014 it will move to ReviewPR when done.`,
1640
1658
  `- "ReviewPR" \u2014 Task Runner finished and opened a PR. Review and merge it.`,
1659
+ `- "Hold" \u2014 PR exists but is on hold for team review. Do not merge \u2014 skip and move on.`,
1641
1660
  `- "ReviewDev" \u2014 PR was merged to dev. This child is complete. Move on.`,
1642
1661
  `- "Complete" \u2014 Fully done. Move on.`,
1643
1662
  ``,
@@ -1654,6 +1673,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
1654
1673
  ` - "InProgress": A Task Runner is actively working on this child. Do nothing \u2014 wait for it to finish.`,
1655
1674
  ` - "Open": This is the next child to execute. Fire it with start_child_cloud_build.`,
1656
1675
  ` - If it fails because the child is missing story points or an agent: notify the team in chat and go idle.`,
1676
+ ` - "Hold": On hold \u2014 team must review before merge. Skip.`,
1657
1677
  ` - "ReviewDev" / "Complete": Already done. Skip.`,
1658
1678
  ` - "Planning": Not ready. If this is blocking progress, notify the team.`,
1659
1679
  ``,
@@ -1661,7 +1681,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
1661
1681
  ``,
1662
1682
  `4. After firing a child build: report which task you fired to chat, then explicitly state you are going idle. The system will relaunch you when the child completes or changes status.`,
1663
1683
  ``,
1664
- `5. When ALL children are in "ReviewDev" or "Complete" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with force_update_task_status("Complete").`,
1684
+ `5. 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").`,
1665
1685
  ``,
1666
1686
  `## Important Rules`,
1667
1687
  `- Process children ONE at a time, in ordinal order.`,
@@ -2033,10 +2053,52 @@ function buildModePrompt(agentMode, context) {
2033
2053
  ].join("\n");
2034
2054
  case "auto":
2035
2055
  return buildAutoPrompt(context);
2056
+ case "code-review":
2057
+ return buildCodeReviewPrompt();
2036
2058
  default:
2037
2059
  return null;
2038
2060
  }
2039
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
+ }
2040
2102
 
2041
2103
  // src/execution/system-prompt.ts
2042
2104
  function formatProjectAgentLine(pa) {
@@ -2408,6 +2470,18 @@ ${context.plan}`);
2408
2470
  }
2409
2471
  return parts;
2410
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
+ }
2411
2485
  function buildFreshInstructions(isPm, isAutoMode, context, agentMode) {
2412
2486
  if (isPm && agentMode === "building") {
2413
2487
  return [
@@ -2505,6 +2579,10 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
2505
2579
  function buildInstructions(mode, context, scenario, agentMode, isAuto) {
2506
2580
  const parts = [`
2507
2581
  ## Instructions`];
2582
+ if (agentMode === "code-review") {
2583
+ parts.push(...buildCodeReviewInstructions(context));
2584
+ return parts;
2585
+ }
2508
2586
  const isPm = mode === "pm";
2509
2587
  if (scenario === "fresh") {
2510
2588
  parts.push(...buildFreshInstructions(isPm, agentMode === "auto", context, agentMode));
@@ -2556,7 +2634,7 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
2556
2634
  }
2557
2635
  async function buildInitialPrompt(mode, context, isAuto, agentMode) {
2558
2636
  const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
2559
- if (!isPackRunner) {
2637
+ if (!isPackRunner && agentMode !== "code-review") {
2560
2638
  const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
2561
2639
  if (sessionRelaunch) return sessionRelaunch;
2562
2640
  }
@@ -2643,7 +2721,7 @@ function buildForceUpdateTaskStatusTool(connection) {
2643
2721
  "force_update_task_status",
2644
2722
  "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.",
2645
2723
  {
2646
- status: z.enum(["InProgress", "ReviewPR", "ReviewDev", "Complete"]).describe("The new status for the task"),
2724
+ status: z.enum(["InProgress", "ReviewPR", "Hold", "ReviewDev", "Complete"]).describe("The new status for the task"),
2647
2725
  task_id: z.string().optional().describe("Child task ID to update. Omit to update the current task.")
2648
2726
  },
2649
2727
  async ({ status, task_id }) => {
@@ -3427,7 +3505,8 @@ async function injectTelemetry(cdpClient) {
3427
3505
  return { success: false, error: result.value };
3428
3506
  }
3429
3507
  try {
3430
- const parsed = JSON.parse(result.value);
3508
+ let parsed = JSON.parse(result.value);
3509
+ if (typeof parsed === "string") parsed = JSON.parse(parsed);
3431
3510
  return {
3432
3511
  success: parsed.success === true,
3433
3512
  patches: parsed.patches
@@ -4362,6 +4441,73 @@ function buildDebugTools(manager) {
4362
4441
  ];
4363
4442
  }
4364
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
+
4365
4511
  // src/tools/index.ts
4366
4512
  function textResult(text) {
4367
4513
  return { content: [{ type: "text", text }] };
@@ -4392,8 +4538,22 @@ function getModeTools(agentMode, connection, config, context) {
4392
4538
  }
4393
4539
  }
4394
4540
  function createConveyorMcpServer(connection, config, context, agentMode, debugManager) {
4395
- const commonTools = buildCommonTools(connection, config);
4396
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);
4397
4557
  const modeTools = getModeTools(effectiveMode, connection, config, context);
4398
4558
  const discoveryTools = effectiveMode === "discovery" || effectiveMode === "auto" ? buildDiscoveryTools(connection, context) : [];
4399
4559
  const debugTools = debugManager && effectiveMode === "building" ? buildDebugTools(debugManager) : [];
@@ -4408,6 +4568,7 @@ function createConveyorMcpServer(connection, config, context, agentMode, debugMa
4408
4568
  import { randomUUID } from "crypto";
4409
4569
  var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
4410
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+/;
4411
4572
  function isPlanFile(input) {
4412
4573
  const filePath = String(input.file_path ?? input.path ?? "");
4413
4574
  return filePath.includes(".claude/plans/");
@@ -4457,6 +4618,24 @@ function handleReviewToolAccess(toolName, input, isParentTask) {
4457
4618
  }
4458
4619
  return { behavior: "allow", updatedInput: input };
4459
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
+ }
4460
4639
  function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask) {
4461
4640
  if (hasExitedPlanMode) {
4462
4641
  return isParentTask ? handleReviewToolAccess(toolName, input, true) : handleBuildingToolAccess(toolName, input);
@@ -4561,6 +4740,9 @@ function buildCanUseTool(host) {
4561
4740
  case "auto":
4562
4741
  result = handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
4563
4742
  break;
4743
+ case "code-review":
4744
+ result = handleCodeReviewToolAccess(toolName, input);
4745
+ break;
4564
4746
  default:
4565
4747
  result = { behavior: "allow", updatedInput: input };
4566
4748
  }
@@ -4608,10 +4790,13 @@ function buildHooks(host) {
4608
4790
  };
4609
4791
  }
4610
4792
  function isReadOnlyMode(mode, hasExitedPlanMode) {
4611
- return mode === "discovery" || mode === "help" || mode === "auto" && !hasExitedPlanMode;
4793
+ return mode === "discovery" || mode === "help" || mode === "code-review" || mode === "auto" && !hasExitedPlanMode;
4612
4794
  }
4613
4795
  function buildDisallowedTools(settings, mode, hasExitedPlanMode) {
4614
4796
  const modeDisallowed = isReadOnlyMode(mode, hasExitedPlanMode) ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
4797
+ if (mode === "code-review") {
4798
+ modeDisallowed.push("ExitPlanMode", "EnterPlanMode");
4799
+ }
4615
4800
  const configured = settings.disallowedTools ?? [];
4616
4801
  const combined = [...configured, ...modeDisallowed];
4617
4802
  return combined.length > 0 ? combined : void 0;
@@ -4644,11 +4829,11 @@ function buildQueryOptions(host, context) {
4644
4829
  tools: { type: "preset", preset: "claude_code" },
4645
4830
  mcpServers: { conveyor: createConveyorMcpServer(host.connection, host.config, context, mode) },
4646
4831
  hooks: buildHooks(host),
4647
- maxTurns: settings.maxTurns,
4832
+ maxTurns: mode === "code-review" ? Math.min(settings.maxTurns ?? 15, 15) : settings.maxTurns,
4648
4833
  effort: settings.effort,
4649
4834
  thinking: settings.thinking,
4650
4835
  betas: settings.betas,
4651
- maxBudgetUsd: settings.maxBudgetUsd ?? 50,
4836
+ maxBudgetUsd: mode === "code-review" ? Math.min(settings.maxBudgetUsd ?? 10, 10) : settings.maxBudgetUsd ?? 50,
4652
4837
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
4653
4838
  enableFileCheckpointing: settings.enableFileCheckpointing,
4654
4839
  stderr: (data) => {
@@ -4888,6 +5073,17 @@ function handleProcessResult(result, context, host, options) {
4888
5073
  if (result.authError) {
4889
5074
  return { action: "return_promise", promise: handleAuthError(context, host, options) };
4890
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
+ }
4891
5087
  if (!result.retriable) return { action: "return" };
4892
5088
  return {
4893
5089
  action: "continue",
@@ -4957,6 +5153,38 @@ var CostTracker = class {
4957
5153
  }
4958
5154
  };
4959
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
+
4960
5188
  // src/runner/plan-sync.ts
4961
5189
  import { readdirSync, statSync, readFileSync } from "fs";
4962
5190
  import { join as join3 } from "path";
@@ -5308,6 +5536,7 @@ function buildQueryHost(deps) {
5308
5536
  callbacks: deps.callbacks,
5309
5537
  setupLog: deps.setupLog,
5310
5538
  costTracker: deps.costTracker,
5539
+ loopDetector: deps.loopDetector,
5311
5540
  get agentMode() {
5312
5541
  return deps.getEffectiveAgentMode();
5313
5542
  },
@@ -5355,10 +5584,12 @@ var AgentRunner = class {
5355
5584
  taskContext = null;
5356
5585
  planSync;
5357
5586
  costTracker = new CostTracker();
5587
+ loopDetector = new LoopDetector();
5358
5588
  worktreeActive = false;
5359
5589
  agentMode = null;
5360
5590
  hasExitedPlanMode = false;
5361
5591
  pendingModeRestart = false;
5592
+ pendingModeAutoStart = false;
5362
5593
  sessionIds = /* @__PURE__ */ new Map();
5363
5594
  lastQueryModeRestart = false;
5364
5595
  startCommandStarted = false;
@@ -5472,6 +5703,10 @@ var AgentRunner = class {
5472
5703
  }
5473
5704
  this.taskContext._runnerSessionId = randomUUID2();
5474
5705
  if (this.taskContext.agentMode) this.agentMode = this.taskContext.agentMode;
5706
+ const pastPlanning = this.taskContext.status !== "Planning" && this.taskContext.status !== "Unidentified";
5707
+ if (this.agentMode === "auto" && pastPlanning) {
5708
+ this.hasExitedPlanMode = true;
5709
+ }
5475
5710
  this.logEffectiveSettings();
5476
5711
  if (process.env.CODESPACES === "true") unshallowRepo(this.config.workspaceDir);
5477
5712
  return true;
@@ -5558,6 +5793,12 @@ var AgentRunner = class {
5558
5793
  async executeInitialMode() {
5559
5794
  if (!this.taskContext) return;
5560
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
+ }
5561
5802
  const shouldRun = mode === "building" || mode === "auto" || mode === "review" && !!this.taskContext.isParentTask;
5562
5803
  if (shouldRun) {
5563
5804
  await this.setState("running");
@@ -5614,6 +5855,12 @@ var AgentRunner = class {
5614
5855
  await this.handleModeRestartCycle();
5615
5856
  continue;
5616
5857
  }
5858
+ if (this.pendingModeAutoStart) {
5859
+ this.pendingModeAutoStart = false;
5860
+ this.interrupted = false;
5861
+ await this.handleModeRestartCycle();
5862
+ continue;
5863
+ }
5617
5864
  if (this._state === "idle") {
5618
5865
  const msg = await this.waitForUserContent();
5619
5866
  if (!msg) {
@@ -5741,6 +5988,7 @@ var AgentRunner = class {
5741
5988
  callbacks: this.callbacks,
5742
5989
  setupLog: this.setupLog,
5743
5990
  costTracker: this.costTracker,
5991
+ loopDetector: this.loopDetector,
5744
5992
  planSync: this.planSync,
5745
5993
  sessionIds: this.sessionIds,
5746
5994
  getEffectiveAgentMode: () => this.effectiveAgentMode,
@@ -5765,15 +6013,32 @@ var AgentRunner = class {
5765
6013
  handleModeChange(newAgentMode) {
5766
6014
  if (this.config.mode !== "pm") return;
5767
6015
  if (newAgentMode) this.agentMode = newAgentMode;
6016
+ this.updateExitedPlanModeFlag(newAgentMode);
5768
6017
  const effectiveMode = this.effectiveAgentMode;
6018
+ const isBuildCapable = effectiveMode === "building" || effectiveMode === "auto" && this.hasExitedPlanMode;
5769
6019
  this.connection.emitModeChanged(effectiveMode);
5770
6020
  this.connection.postChatMessage(
5771
6021
  `Mode switched to **${effectiveMode}**${effectiveMode === "building" ? " \u2014 I now have direct coding access." : ""}`
5772
6022
  );
5773
- if (effectiveMode === "building" && this.taskContext?.status === "Open") {
6023
+ if (isBuildCapable && this.taskContext?.status === "Open") {
5774
6024
  this.connection.updateStatus("InProgress");
5775
6025
  this.taskContext.status = "InProgress";
5776
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
+ }
5777
6042
  }
5778
6043
  softStop() {
5779
6044
  this.interrupted = true;
@@ -5919,17 +6184,17 @@ import {
5919
6184
  } from "@anthropic-ai/claude-agent-sdk";
5920
6185
 
5921
6186
  // src/tools/project-tools.ts
5922
- import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
5923
- import { z as z7 } from "zod";
6187
+ import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
6188
+ import { z as z8 } from "zod";
5924
6189
  function buildReadTools(connection) {
5925
6190
  return [
5926
- tool7(
6191
+ tool8(
5927
6192
  "list_tasks",
5928
6193
  "List tasks in the project. Optionally filter by status or assignee.",
5929
6194
  {
5930
- status: z7.string().optional().describe("Filter by task status (e.g. Planning, Open, InProgress, ReviewPR, Complete)"),
5931
- assigneeId: z7.string().optional().describe("Filter by assigned user ID"),
5932
- 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)")
5933
6198
  },
5934
6199
  async (params) => {
5935
6200
  try {
@@ -5943,10 +6208,10 @@ function buildReadTools(connection) {
5943
6208
  },
5944
6209
  { annotations: { readOnlyHint: true } }
5945
6210
  ),
5946
- tool7(
6211
+ tool8(
5947
6212
  "get_task",
5948
6213
  "Get detailed information about a task including its chat messages, child tasks, and codespace status.",
5949
- { task_id: z7.string().describe("The task ID to look up") },
6214
+ { task_id: z8.string().describe("The task ID to look up") },
5950
6215
  async ({ task_id }) => {
5951
6216
  try {
5952
6217
  const task = await connection.requestGetTask(task_id);
@@ -5959,14 +6224,14 @@ function buildReadTools(connection) {
5959
6224
  },
5960
6225
  { annotations: { readOnlyHint: true } }
5961
6226
  ),
5962
- tool7(
6227
+ tool8(
5963
6228
  "search_tasks",
5964
6229
  "Search tasks by tags, text query, or status filters.",
5965
6230
  {
5966
- tagNames: z7.array(z7.string()).optional().describe("Filter by tag names"),
5967
- searchQuery: z7.string().optional().describe("Text search in title/description"),
5968
- statusFilters: z7.array(z7.string()).optional().describe("Filter by statuses"),
5969
- 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)")
5970
6235
  },
5971
6236
  async (params) => {
5972
6237
  try {
@@ -5980,7 +6245,7 @@ function buildReadTools(connection) {
5980
6245
  },
5981
6246
  { annotations: { readOnlyHint: true } }
5982
6247
  ),
5983
- tool7(
6248
+ tool8(
5984
6249
  "list_tags",
5985
6250
  "List all tags available in the project.",
5986
6251
  {},
@@ -5996,7 +6261,7 @@ function buildReadTools(connection) {
5996
6261
  },
5997
6262
  { annotations: { readOnlyHint: true } }
5998
6263
  ),
5999
- tool7(
6264
+ tool8(
6000
6265
  "get_project_summary",
6001
6266
  "Get a summary of the project including task counts by status and active builds.",
6002
6267
  {},
@@ -6016,15 +6281,15 @@ function buildReadTools(connection) {
6016
6281
  }
6017
6282
  function buildMutationTools(connection) {
6018
6283
  return [
6019
- tool7(
6284
+ tool8(
6020
6285
  "create_task",
6021
6286
  "Create a new task in the project.",
6022
6287
  {
6023
- title: z7.string().describe("Task title"),
6024
- description: z7.string().optional().describe("Task description"),
6025
- plan: z7.string().optional().describe("Implementation plan in markdown"),
6026
- status: z7.string().optional().describe("Initial status (default: Planning)"),
6027
- 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")
6028
6293
  },
6029
6294
  async (params) => {
6030
6295
  try {
@@ -6037,16 +6302,16 @@ function buildMutationTools(connection) {
6037
6302
  }
6038
6303
  }
6039
6304
  ),
6040
- tool7(
6305
+ tool8(
6041
6306
  "update_task",
6042
6307
  "Update an existing task's title, description, plan, status, or assignee.",
6043
6308
  {
6044
- task_id: z7.string().describe("The task ID to update"),
6045
- title: z7.string().optional().describe("New title"),
6046
- description: z7.string().optional().describe("New description"),
6047
- plan: z7.string().optional().describe("New plan in markdown"),
6048
- status: z7.string().optional().describe("New status"),
6049
- 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")
6050
6315
  },
6051
6316
  async ({ task_id, ...fields }) => {
6052
6317
  try {
@@ -6278,8 +6543,8 @@ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
6278
6543
 
6279
6544
  // src/tools/audit-tools.ts
6280
6545
  import { randomUUID as randomUUID3 } from "crypto";
6281
- import { tool as tool8, createSdkMcpServer as createSdkMcpServer3 } from "@anthropic-ai/claude-agent-sdk";
6282
- 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";
6283
6548
  function mapCreateTag(input) {
6284
6549
  return {
6285
6550
  type: "create_tag",
@@ -6361,14 +6626,14 @@ function collectRecommendation(toolName, input, collector, onRecommendation) {
6361
6626
  }
6362
6627
  function createAuditMcpServer(collector, onRecommendation) {
6363
6628
  const auditTools = [
6364
- tool8(
6629
+ tool9(
6365
6630
  "recommend_create_tag",
6366
6631
  "Recommend creating a new tag for an uncovered subsystem or area",
6367
6632
  {
6368
- name: z8.string().describe("Proposed tag name (lowercase, hyphenated)"),
6369
- color: z8.string().optional().describe("Hex color code"),
6370
- description: z8.string().describe("What this tag covers"),
6371
- 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")
6372
6637
  },
6373
6638
  async (args) => {
6374
6639
  const result = collectRecommendation(
@@ -6380,14 +6645,14 @@ function createAuditMcpServer(collector, onRecommendation) {
6380
6645
  return { content: [{ type: "text", text: result }] };
6381
6646
  }
6382
6647
  ),
6383
- tool8(
6648
+ tool9(
6384
6649
  "recommend_update_description",
6385
6650
  "Recommend updating a tag's description to better reflect its scope",
6386
6651
  {
6387
- tagId: z8.string(),
6388
- tagName: z8.string(),
6389
- description: z8.string().describe("Proposed new description"),
6390
- reasoning: z8.string()
6652
+ tagId: z9.string(),
6653
+ tagName: z9.string(),
6654
+ description: z9.string().describe("Proposed new description"),
6655
+ reasoning: z9.string()
6391
6656
  },
6392
6657
  async (args) => {
6393
6658
  const result = collectRecommendation(
@@ -6399,16 +6664,16 @@ function createAuditMcpServer(collector, onRecommendation) {
6399
6664
  return { content: [{ type: "text", text: result }] };
6400
6665
  }
6401
6666
  ),
6402
- tool8(
6667
+ tool9(
6403
6668
  "recommend_context_link",
6404
6669
  "Recommend linking a doc, rule, file, or folder to a tag's contextPaths",
6405
6670
  {
6406
- tagId: z8.string(),
6407
- tagName: z8.string(),
6408
- linkType: z8.enum(["rule", "doc", "file", "folder"]),
6409
- path: z8.string(),
6410
- label: z8.string().optional(),
6411
- 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()
6412
6677
  },
6413
6678
  async (args) => {
6414
6679
  const result = collectRecommendation(
@@ -6420,16 +6685,16 @@ function createAuditMcpServer(collector, onRecommendation) {
6420
6685
  return { content: [{ type: "text", text: result }] };
6421
6686
  }
6422
6687
  ),
6423
- tool8(
6688
+ tool9(
6424
6689
  "flag_documentation_gap",
6425
6690
  "Flag a file that agents read heavily but has no tag documentation linked",
6426
6691
  {
6427
- tagName: z8.string().describe("Tag whose agents read this file"),
6428
- tagId: z8.string().optional(),
6429
- filePath: z8.string(),
6430
- readCount: z8.number(),
6431
- suggestedAction: z8.string().describe("What doc or rule should be created"),
6432
- 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()
6433
6698
  },
6434
6699
  async (args) => {
6435
6700
  const result = collectRecommendation(
@@ -6441,15 +6706,15 @@ function createAuditMcpServer(collector, onRecommendation) {
6441
6706
  return { content: [{ type: "text", text: result }] };
6442
6707
  }
6443
6708
  ),
6444
- tool8(
6709
+ tool9(
6445
6710
  "recommend_merge_tags",
6446
6711
  "Recommend merging one tag into another",
6447
6712
  {
6448
- tagId: z8.string().describe("Tag ID to be merged (removed after merge)"),
6449
- tagName: z8.string().describe("Name of the tag to be merged"),
6450
- mergeIntoTagId: z8.string().describe("Tag ID to merge into (kept)"),
6451
- mergeIntoTagName: z8.string(),
6452
- 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()
6453
6718
  },
6454
6719
  async (args) => {
6455
6720
  const result = collectRecommendation(
@@ -6461,14 +6726,14 @@ function createAuditMcpServer(collector, onRecommendation) {
6461
6726
  return { content: [{ type: "text", text: result }] };
6462
6727
  }
6463
6728
  ),
6464
- tool8(
6729
+ tool9(
6465
6730
  "recommend_rename_tag",
6466
6731
  "Recommend renaming a tag",
6467
6732
  {
6468
- tagId: z8.string(),
6469
- tagName: z8.string().describe("Current tag name"),
6470
- newName: z8.string().describe("Proposed new name"),
6471
- 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()
6472
6737
  },
6473
6738
  async (args) => {
6474
6739
  const result = collectRecommendation(
@@ -6480,10 +6745,10 @@ function createAuditMcpServer(collector, onRecommendation) {
6480
6745
  return { content: [{ type: "text", text: result }] };
6481
6746
  }
6482
6747
  ),
6483
- tool8(
6748
+ tool9(
6484
6749
  "complete_audit",
6485
6750
  "Signal that the audit is complete with a summary of all findings",
6486
- { summary: z8.string().describe("Brief overview of all findings") },
6751
+ { summary: z9.string().describe("Brief overview of all findings") },
6487
6752
  async (args) => {
6488
6753
  collector.complete = true;
6489
6754
  collector.summary = args.summary ?? "Audit completed.";
@@ -6699,21 +6964,22 @@ function setupWorkDir(projectDir, assignment) {
6699
6964
  return { workDir, usesWorktree: shouldWorktree };
6700
6965
  }
6701
6966
  function spawnChildAgent(assignment, workDir) {
6702
- const { taskToken, apiUrl, taskId, mode, isAuto, useSandbox } = assignment;
6967
+ const { taskToken, apiUrl, taskId, mode, isAuto, useSandbox, agentMode } = assignment;
6703
6968
  const cliPath = path.resolve(__dirname, "cli.js");
6704
6969
  const childEnv = { ...process.env };
6705
6970
  delete childEnv.CONVEYOR_PROJECT_TOKEN;
6706
6971
  delete childEnv.CONVEYOR_PROJECT_ID;
6972
+ const effectiveAgentMode = agentMode ?? (isAuto ? "auto" : "");
6707
6973
  const child = fork(cliPath, [], {
6708
6974
  env: {
6709
6975
  ...childEnv,
6710
6976
  CONVEYOR_API_URL: apiUrl,
6711
6977
  CONVEYOR_TASK_TOKEN: taskToken,
6712
6978
  CONVEYOR_TASK_ID: taskId,
6713
- CONVEYOR_MODE: mode,
6979
+ CONVEYOR_MODE: agentMode === "code-review" ? "code-review" : mode,
6714
6980
  CONVEYOR_WORKSPACE: workDir,
6715
6981
  CONVEYOR_USE_WORKTREE: "false",
6716
- CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
6982
+ CONVEYOR_AGENT_MODE: effectiveAgentMode,
6717
6983
  CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
6718
6984
  CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false",
6719
6985
  CONVEYOR_FROM_PROJECT_RUNNER: "true"
@@ -7148,8 +7414,9 @@ var ProjectRunner = class {
7148
7414
  handleAssignment(assignment) {
7149
7415
  const { taskId, mode } = assignment;
7150
7416
  const shortId = taskId.slice(0, 8);
7151
- if (this.activeAgents.has(taskId)) {
7152
- 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 });
7153
7420
  return;
7154
7421
  }
7155
7422
  if (this.activeAgents.size >= MAX_CONCURRENT) {
@@ -7168,16 +7435,16 @@ var ProjectRunner = class {
7168
7435
  }
7169
7436
  const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
7170
7437
  const child = spawnChildAgent(assignment, workDir);
7171
- this.activeAgents.set(taskId, {
7438
+ this.activeAgents.set(agentKey, {
7172
7439
  process: child,
7173
7440
  worktreePath: workDir,
7174
7441
  mode,
7175
7442
  usesWorktree
7176
7443
  });
7177
7444
  this.connection.emitTaskStarted(taskId);
7178
- logger8.info("Started task", { taskId: shortId, mode, workDir });
7445
+ logger8.info("Started task", { taskId: shortId, mode, agentKey, workDir });
7179
7446
  child.on("exit", (code) => {
7180
- this.activeAgents.delete(taskId);
7447
+ this.activeAgents.delete(agentKey);
7181
7448
  const reason = code === 0 ? "completed" : `exited with code ${code}`;
7182
7449
  this.connection.emitTaskStopped(taskId, reason);
7183
7450
  logger8.info("Task exited", { taskId: shortId, reason });
@@ -7337,4 +7604,4 @@ export {
7337
7604
  ProjectRunner,
7338
7605
  FileCache
7339
7606
  };
7340
- //# sourceMappingURL=chunk-RHRQJO5E.js.map
7607
+ //# sourceMappingURL=chunk-T6IASOS2.js.map