topchester-ai 0.63.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runTopchesterCli } from "./cli-fySUzvp-.mjs";
2
+ import { t as runTopchesterCli } from "./cli-2v1iJ0YF.mjs";
3
3
  //#region src/bin.ts
4
4
  await runTopchesterCli();
5
5
  //#endregion
@@ -1843,7 +1843,7 @@ async function deleteWorkspaceFile(workspaceRoot, path) {
1843
1843
  const finishTaskTool = defineTool({
1844
1844
  name: "finish_task",
1845
1845
  description: "Finish the current task only after the requested work has actually been completed with tools. Do not use this to claim edits, file reads, or validation that did not happen.",
1846
- prompt: "finish_task: complete the task with a brief final response only after tool results prove the work is done. For implementation tasks, do not call finish_task until source files were changed by edit_file, write_file, apply_patch, or another mutating tool, unless no code change is truly required. Example: {\"tool\":\"finish_task\",\"args\":{\"final_response\":\"Changed src/foo.ts and ran pnpm test foo.test.ts.\",\"files_changed\":[\"src/foo.ts\"],\"validation\":[\"pnpm test foo.test.ts\"],\"remaining_issues\":[]}}",
1846
+ prompt: "finish_task: complete the task with a brief final response only after tool results prove the work is done. In benchmark or require-finish mode, this is the only valid terminal action; normal assistant messages are progress notes and do not finish the task. For implementation tasks, do not call finish_task until source files were changed by edit_file, write_file, apply_patch, or another mutating tool, unless no code change is truly required. Example: {\"tool\":\"finish_task\",\"args\":{\"final_response\":\"Changed src/foo.ts and ran pnpm test foo.test.ts.\",\"files_changed\":[\"src/foo.ts\"],\"validation\":[\"pnpm test foo.test.ts\"],\"remaining_issues\":[]}}",
1847
1847
  argsSchema: z.object({
1848
1848
  final_response: z.string().describe("Brief final response for the user."),
1849
1849
  files_changed: z.array(z.string()).optional().describe("Workspace-relative files actually changed by successful edit tools."),
@@ -4083,7 +4083,7 @@ function truncateText(text, width) {
4083
4083
  const planTodoTool = defineTool({
4084
4084
  name: "plan_todo",
4085
4085
  description: "Replace the visible session task plan for multi-step work.",
4086
- prompt: "plan_todo: replace the visible session task plan for non-trivial multi-step work; keep 2-6 short items, exactly one in_progress item while work remains, and use [] only to clear. Do not use plan_todo just to report completed work before a final answer. To use it, reply with only JSON: {\"tool\":\"plan_todo\",\"args\":{\"items\":[{\"text\":\"Inspect relevant files\",\"status\":\"in_progress\"},{\"text\":\"Implement focused change\",\"status\":\"pending\"}]}}",
4086
+ prompt: "plan_todo: replace the visible session task plan for genuinely multi-step work. Usually create it once after initial orientation, keep 2-6 short milestone items, exactly one in_progress item while work remains, and batch updates when milestones change. Do not call plan_todo twice in a row, after routine reads/searches, after failed edit attempts, for wording-only changes, or just to report completed work before a final answer. To use it, reply with only JSON: {\"tool\":\"plan_todo\",\"args\":{\"items\":[{\"text\":\"Inspect relevant files\",\"status\":\"in_progress\"},{\"text\":\"Implement focused change\",\"status\":\"pending\"}]}}",
4087
4087
  argsSchema: planTodoArgsSchema,
4088
4088
  async execute(context, args) {
4089
4089
  if (!context.taskPlan) throw new Error("plan_todo requires runtime task-plan state.");
@@ -12390,8 +12390,10 @@ function getChatSystemPrompt(options = {}) {
12390
12390
  "- Do not claim to have read, created, edited, staged, committed, or run anything unless a tool result in this turn confirms it.",
12391
12391
  "- If the task requires implementation, do not finish with a prose summary before a successful source-editing tool result. A summary of intended changes is not a substitute for editing files.",
12392
12392
  ...canUseTool("plan_todo") ? [
12393
- "- Use plan_todo for non-trivial multi-step work before the first substantive repository tool call.",
12394
- "- Keep plan_todo items short, user-safe, and usually 2 to 6 items. Maintain exactly one in_progress item while work remains, update it after major progress changes, and clear it only when abandoning the plan or when no visible plan is useful.",
12393
+ "- Use plan_todo for genuinely multi-step work when a visible checklist helps; for small or straightforward tasks, do the work directly.",
12394
+ "- If you use plan_todo, usually create it once after initial orientation. Keep items short, user-safe, and usually 2 to 6 items with exactly one in_progress item while work remains.",
12395
+ "- Batch plan_todo updates. Update only when a milestone completes, scope changes, or the next major phase starts; do not update after routine reads, searches, failed edit attempts, or wording-only changes.",
12396
+ "- Never call plan_todo twice in a row.",
12395
12397
  "- Do not use plan_todo for simple one-step answers, tiny reads, or trivial edits.",
12396
12398
  "- Do not call plan_todo only to summarize completed work before a final answer. If no visible plan is active and the work is done, answer directly."
12397
12399
  ] : [],
@@ -12797,17 +12799,18 @@ function formatToolResultForPrompt(result) {
12797
12799
  }
12798
12800
  /**
12799
12801
  * Builds the follow-up instruction appended after each tool result. It keeps
12800
- * the model on the active task, reminds it to maintain the visible plan, and
12801
- * restates the current tool-call protocol so the next model step remains
12802
- * parseable by the runtime.
12802
+ * the model on the active task, lightly references the visible plan when one
12803
+ * is active, and restates the current tool-call protocol so the next model
12804
+ * step remains parseable by the runtime.
12803
12805
  */
12804
- function formatContinuationInstruction(protocol, result, canUsePlanTodo = true) {
12806
+ function formatContinuationInstruction(protocol, result, canUsePlanTodo = true, planTodoMode = "normal") {
12805
12807
  const toolInstruction = protocol === "text-xml" ? "If another tool is needed, reply with only one XML tool call." : protocol === "text-json" ? "If another tool is needed, reply with only that tool JSON." : "If another tool is needed, use the available tool calling path.";
12806
12808
  return [
12807
12809
  "Continue the user's request using the tool result above and the visible plan when one is active.",
12808
12810
  result.tool === "find_file" ? "find_file results are paths only; if the user asked to read or answer from file contents, call read_file on the relevant path before answering. Do not ask the user to provide the read_file result or permission." : "",
12809
- canUsePlanTodo ? "Update plan_todo after major progress changes." : "",
12810
- canUsePlanTodo ? "Before a final answer, close the visible plan by calling plan_todo with all finished items marked completed, or with [] if abandoning the plan." : "",
12811
+ canUsePlanTodo && planTodoMode === "compact" ? "Compact plan mode is active: do not update plan_todo unless task scope materially changes; avoid final plan-closure calls." : "",
12812
+ canUsePlanTodo && planTodoMode === "normal" ? "Use plan_todo sparingly: batch updates only when a milestone completes, scope changes, or the next major phase starts." : "",
12813
+ canUsePlanTodo && planTodoMode === "normal" ? "Before a final answer, close the visible plan by calling plan_todo with all finished items marked completed, or with [] if abandoning the plan." : "",
12811
12814
  toolInstruction,
12812
12815
  "Otherwise answer the user. Do not guess."
12813
12816
  ].filter(Boolean).join(" ");
@@ -12852,6 +12855,22 @@ function formatNoEditCompletionFailure(draftAnswer) {
12852
12855
  const trimmedDraft = draftAnswer.trim();
12853
12856
  return ["I could not complete the implementation because no successful source-file edit occurred in this turn.", trimmedDraft ? `The model tried to finish with this unsupported summary:\n${trimmedDraft}` : ""].filter(Boolean).join("\n");
12854
12857
  }
12858
+ function formatFinishTaskRequiredRepairInstruction(protocol, draftAnswer) {
12859
+ const toolInstruction = protocol === "text-xml" ? "Reply now with only one XML tool call for the next implementation or validation step, or call finish_task if the work is complete." : protocol === "text-json" ? "Reply now with only one tool JSON object for the next implementation or validation step, or call finish_task if the work is complete." : "Use the available tool calling path now for the next implementation or validation step, or call finish_task if the work is complete.";
12860
+ const trimmedDraft = draftAnswer.trim();
12861
+ return [
12862
+ "This run requires finish_task as the only terminal action.",
12863
+ "A normal assistant message is treated as a progress note, not completion.",
12864
+ "Do not summarize next steps in prose. Use tools to perform the next step.",
12865
+ "Call finish_task only when the requested work is complete and its files_changed, validation, and remaining_issues fields are accurate.",
12866
+ toolInstruction,
12867
+ trimmedDraft ? `Previous progress note that did not finish the task:\n${trimmedDraft}` : ""
12868
+ ].filter(Boolean).join("\n");
12869
+ }
12870
+ function formatFinishTaskRequiredFailure(draftAnswer) {
12871
+ const trimmedDraft = draftAnswer.trim();
12872
+ return ["I stopped because this run requires finish_task, but the model kept replying with normal assistant messages instead of using tools.", trimmedDraft ? `Last progress note:\n${trimmedDraft}` : ""].filter(Boolean).join("\n");
12873
+ }
12855
12874
  function getTextToolCallSources(protocol) {
12856
12875
  return protocol === "text-xml" ? ["text-xml"] : protocol === "text-json" ? ["text-json"] : ["text-json", "text-xml"];
12857
12876
  }
@@ -13146,7 +13165,13 @@ function isCompletedPlanTodoItem(item) {
13146
13165
  }
13147
13166
  //#endregion
13148
13167
  //#region src/agent/runtime/index.ts
13149
- const MAX_TOOL_CALLS_PER_TURN = 75;
13168
+ const DEFAULT_MAX_TOOL_CALLS_PER_TURN = 75;
13169
+ const MAX_TOOL_CALLS_PER_TURN_ENV = "TOPCHESTER_MAX_TOOL_CALLS_PER_TURN";
13170
+ const PLAN_TODO_MODE_ENV = "TOPCHESTER_PLAN_TODO_MODE";
13171
+ const MAX_PLAN_TODO_UPDATES_PER_TURN_ENV = "TOPCHESTER_MAX_PLAN_TODO_UPDATES_PER_TURN";
13172
+ const REQUIRE_FINISH_TASK_ENV = "TOPCHESTER_REQUIRE_FINISH_TASK";
13173
+ const DEFAULT_COMPACT_MAX_PLAN_TODO_UPDATES_PER_TURN = 3;
13174
+ const MAX_FINISH_TASK_REQUIRED_REPAIRS = 3;
13150
13175
  const DEFAULT_TASK_CONCURRENCY = 3;
13151
13176
  var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13152
13177
  context;
@@ -13273,12 +13298,18 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13273
13298
  let requestedPlanClosure = false;
13274
13299
  let invalidToolCallRepairs = 0;
13275
13300
  let noEditCompletionRepairs = 0;
13301
+ let finishTaskRequiredRepairs = 0;
13276
13302
  let hasSourceMutation = false;
13303
+ const maxToolCallsPerTurn = readMaxToolCallsPerTurn();
13304
+ const planTodoMode = readPlanTodoMode();
13305
+ const maxPlanTodoUpdatesPerTurn = readMaxPlanTodoUpdatesPerTurn(planTodoMode);
13306
+ let planTodoUpdates = 0;
13277
13307
  const implementationTask = isImplementationTaskRequest(message);
13308
+ const requireFinishTask = isFinishTaskRequiredByEnv() && isToolAllowed(permissions, "finish_task");
13278
13309
  const projectInstructionToolState = { shownSourceKeys: /* @__PURE__ */ new Set() };
13279
13310
  const persistedProjectInstructionKeys = /* @__PURE__ */ new Set();
13280
13311
  try {
13281
- for (let toolCalls = 0; toolCalls <= MAX_TOOL_CALLS_PER_TURN; toolCalls += 1) {
13312
+ for (let toolCalls = 0; toolCalls <= maxToolCallsPerTurn; toolCalls += 1) {
13282
13313
  const startedAt = Date.now();
13283
13314
  const projectInstructions = await this.resolveBaseProjectInstructions();
13284
13315
  for (const event of createInstructionContextEventsFromProjectInstructions(projectInstructions, persistedProjectInstructionKeys)) yield event;
@@ -13385,7 +13416,19 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13385
13416
  yield agentEvent.status("ready");
13386
13417
  return;
13387
13418
  }
13388
- if (hasOpenTaskPlan(plan)) {
13419
+ if (requireFinishTask) {
13420
+ if (finishTaskRequiredRepairs < MAX_FINISH_TASK_REQUIRED_REPAIRS) {
13421
+ finishTaskRequiredRepairs += 1;
13422
+ nextPrompt = `${nextPrompt}\n\n${formatFinishTaskRequiredRepairInstruction(result.toolProtocol, finalText)}`;
13423
+ continue;
13424
+ }
13425
+ const finalMessage = formatFinishTaskRequiredFailure(finalText);
13426
+ yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
13427
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
13428
+ yield agentEvent.status("ready");
13429
+ return;
13430
+ }
13431
+ if (planTodoMode === "normal" && hasOpenTaskPlan(plan)) {
13389
13432
  if (!requestedPlanClosure) {
13390
13433
  requestedPlanClosure = true;
13391
13434
  nextPrompt = `${nextPrompt}\n\n${formatOpenPlanClosureInstruction(finalText, result.toolProtocol)}`;
@@ -13399,11 +13442,11 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13399
13442
  yield agentEvent.status("ready");
13400
13443
  return;
13401
13444
  }
13402
- if (toolCalls === MAX_TOOL_CALLS_PER_TURN) {
13445
+ if (toolCalls === maxToolCallsPerTurn) {
13403
13446
  yield agentEvent.choice({
13404
13447
  tone: "warning",
13405
13448
  title: "Tool call limit reached",
13406
- body: `Stopped after ${MAX_TOOL_CALLS_PER_TURN} tool calls in one turn. Continue starts another turn; abort leaves the call stopped.`,
13449
+ body: `Stopped after ${maxToolCallsPerTurn} tool calls in one turn. Continue starts another turn; abort leaves the call stopped.`,
13407
13450
  actions: [choiceAction("Continue", "Continue the previous task from where you stopped."), choiceAction("Abort", ABORT_CHOICE_VALUE)]
13408
13451
  });
13409
13452
  yield agentEvent.status("ready");
@@ -13481,7 +13524,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13481
13524
  }
13482
13525
  }
13483
13526
  afterTool = "task";
13484
- nextPrompt = this.appendRuntimeSteeringToContinuationPrompt(this.appendHookContextsToPrompt(`${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHookContexts), options.steering);
13527
+ nextPrompt = this.appendRuntimeSteeringToContinuationPrompt(this.appendHookContextsToPrompt(`${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"), planTodoMode)}`, "PostToolUse", postHookContexts), options.steering);
13485
13528
  continue;
13486
13529
  }
13487
13530
  if (modelToolCalls.length > 1 && modelToolCalls.every((call) => toolCatalog.isParallelSafe(call.tool))) {
@@ -13545,7 +13588,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13545
13588
  }
13546
13589
  }
13547
13590
  afterTool = parallelCalls.at(-1)?.tool;
13548
- nextPrompt = this.appendRuntimeSteeringToContinuationPrompt(this.appendHookContextsToPrompt(`${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHookContexts), options.steering);
13591
+ nextPrompt = this.appendRuntimeSteeringToContinuationPrompt(this.appendHookContextsToPrompt(`${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"), planTodoMode)}`, "PostToolUse", postHookContexts), options.steering);
13549
13592
  continue;
13550
13593
  }
13551
13594
  const executableToolCall = toolCall;
@@ -13557,47 +13600,55 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13557
13600
  yield agentEvent.status("ready");
13558
13601
  return;
13559
13602
  }
13560
- const preHookRun = this.startPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
13561
- for await (const event of preHookRun.statusEvents) yield event;
13562
- const preHook = await preHookRun.result;
13563
- for (const event of this.hookResultToEvents(preHook)) yield event;
13564
13603
  let toolResult;
13565
- if (preHook.stopped) {
13566
- if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
13567
- yield agentEvent.status("ready");
13568
- return;
13569
- }
13570
- if (preHook.blocked) toolResult = createToolErrorResult(executableToolCall.tool, preHook.blocked.message);
13604
+ const planTodoRejection = validatePlanTodoCall(executableToolCall, this.taskPlan.get(), {
13605
+ afterTool,
13606
+ planTodoUpdates,
13607
+ maxPlanTodoUpdatesPerTurn
13608
+ });
13609
+ if (planTodoRejection) toolResult = createToolErrorResult(executableToolCall.tool, planTodoRejection);
13571
13610
  else {
13572
- const approval = await this.resolveBashApproval(executableToolCall, toolCall.id, options, session, abortSignal);
13573
- for (const event of approval.events) yield event;
13574
- if (approval.cancelled && approval.stopped) {
13575
- if (!approval.events.some((event) => event.type === "message" && event.text === approval.reason)) yield agentEvent.systemMessage(approval.reason);
13611
+ const preHookRun = this.startPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
13612
+ for await (const event of preHookRun.statusEvents) yield event;
13613
+ const preHook = await preHookRun.result;
13614
+ for (const event of this.hookResultToEvents(preHook)) yield event;
13615
+ if (preHook.stopped) {
13616
+ if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
13576
13617
  yield agentEvent.status("ready");
13577
13618
  return;
13578
13619
  }
13579
- if (approval.cancelled) toolResult = createToolErrorResult(executableToolCall.tool, approval.reason);
13620
+ if (preHook.blocked) toolResult = createToolErrorResult(executableToolCall.tool, preHook.blocked.message);
13580
13621
  else {
13581
- const toolEventQueue = createRuntimeEventQueue();
13582
- const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
13583
- logger: this.context.logger,
13584
- config: this.context.config,
13585
- bashApprovals: { allowExactCommands: approval.approvedCommands },
13586
- taskPlan: this.taskPlan,
13587
- profile,
13588
- permissions,
13589
- subagents,
13590
- projectInstructions: projectInstructionToolState,
13591
- currentUserMessage: message,
13592
- abortSignal,
13593
- toolCallId: toolCall.id,
13594
- toolCatalog,
13595
- eventSink: (event) => toolEventQueue.push(event)
13596
- }).finally(() => {
13597
- toolEventQueue.close();
13598
- });
13599
- for await (const event of toolEventQueue) yield event;
13600
- toolResult = await toolResultPromise;
13622
+ const approval = await this.resolveBashApproval(executableToolCall, toolCall.id, options, session, abortSignal);
13623
+ for (const event of approval.events) yield event;
13624
+ if (approval.cancelled && approval.stopped) {
13625
+ if (!approval.events.some((event) => event.type === "message" && event.text === approval.reason)) yield agentEvent.systemMessage(approval.reason);
13626
+ yield agentEvent.status("ready");
13627
+ return;
13628
+ }
13629
+ if (approval.cancelled) toolResult = createToolErrorResult(executableToolCall.tool, approval.reason);
13630
+ else {
13631
+ const toolEventQueue = createRuntimeEventQueue();
13632
+ const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
13633
+ logger: this.context.logger,
13634
+ config: this.context.config,
13635
+ bashApprovals: { allowExactCommands: approval.approvedCommands },
13636
+ taskPlan: this.taskPlan,
13637
+ profile,
13638
+ permissions,
13639
+ subagents,
13640
+ projectInstructions: projectInstructionToolState,
13641
+ currentUserMessage: message,
13642
+ abortSignal,
13643
+ toolCallId: toolCall.id,
13644
+ toolCatalog,
13645
+ eventSink: (event) => toolEventQueue.push(event)
13646
+ }).finally(() => {
13647
+ toolEventQueue.close();
13648
+ });
13649
+ for await (const event of toolEventQueue) yield event;
13650
+ toolResult = await toolResultPromise;
13651
+ }
13601
13652
  }
13602
13653
  }
13603
13654
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
@@ -13605,14 +13656,17 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13605
13656
  const finishError = validateFinishTaskResult(toolResult, {
13606
13657
  implementationTask,
13607
13658
  hasSourceMutation,
13608
- hasOpenPlan: hasOpenTaskPlan(this.taskPlan.get()),
13659
+ hasOpenPlan: planTodoMode === "normal" && hasOpenTaskPlan(this.taskPlan.get()),
13609
13660
  canUseSourceEditTool: canStillUseSourceEditTool(permissions)
13610
13661
  });
13611
13662
  if (finishError) toolResult = createToolErrorResult(executableToolCall.tool, finishError);
13612
13663
  }
13613
13664
  if (isSourceMutationResult(toolResult)) hasSourceMutation = true;
13614
13665
  yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult), getToolCallDisplayDiff(toolResult));
13615
- if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
13666
+ if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") {
13667
+ planTodoUpdates += 1;
13668
+ yield agentEvent.taskPlan(toolResult.plan);
13669
+ }
13616
13670
  const postHookRun = this.startPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
13617
13671
  for await (const event of postHookRun.statusEvents) yield event;
13618
13672
  const postHook = await postHookRun.result;
@@ -13630,7 +13684,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
13630
13684
  return;
13631
13685
  }
13632
13686
  afterTool = executableToolCall.tool;
13633
- nextPrompt = this.appendRuntimeSteeringToContinuationPrompt(this.appendHookContextsToPrompt(`${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHook.contexts), options.steering);
13687
+ nextPrompt = this.appendRuntimeSteeringToContinuationPrompt(this.appendHookContextsToPrompt(`${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"), planTodoMode)}`, "PostToolUse", postHook.contexts), options.steering);
13634
13688
  }
13635
13689
  const finalMessage = "I stopped because the tool loop ended unexpectedly.";
13636
13690
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
@@ -14014,6 +14068,50 @@ function getToolCallDisplayDiff(result) {
14014
14068
  if (result.tool === "edit_file" && !isToolErrorResult(result) && "diff" in result) return result.diff;
14015
14069
  if (result.tool === "apply_patch" && !isToolErrorResult(result) && result.diffs.length > 0) return result.diffs.join("\n");
14016
14070
  }
14071
+ function readMaxToolCallsPerTurn() {
14072
+ const raw = process.env[MAX_TOOL_CALLS_PER_TURN_ENV]?.trim();
14073
+ if (!raw) return DEFAULT_MAX_TOOL_CALLS_PER_TURN;
14074
+ const parsed = Number(raw);
14075
+ if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_MAX_TOOL_CALLS_PER_TURN;
14076
+ return parsed;
14077
+ }
14078
+ function readPlanTodoMode() {
14079
+ return process.env[PLAN_TODO_MODE_ENV]?.trim().toLowerCase() === "compact" ? "compact" : "normal";
14080
+ }
14081
+ function isFinishTaskRequiredByEnv() {
14082
+ const raw = process.env[REQUIRE_FINISH_TASK_ENV]?.trim().toLowerCase();
14083
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
14084
+ }
14085
+ function readMaxPlanTodoUpdatesPerTurn(mode) {
14086
+ const raw = process.env[MAX_PLAN_TODO_UPDATES_PER_TURN_ENV]?.trim();
14087
+ if (!raw) return mode === "compact" ? DEFAULT_COMPACT_MAX_PLAN_TODO_UPDATES_PER_TURN : void 0;
14088
+ const parsed = Number(raw);
14089
+ if (!Number.isInteger(parsed) || parsed < 0) return mode === "compact" ? DEFAULT_COMPACT_MAX_PLAN_TODO_UPDATES_PER_TURN : void 0;
14090
+ return parsed;
14091
+ }
14092
+ function validatePlanTodoCall(call, currentPlan, state) {
14093
+ if (call.tool !== "plan_todo") return;
14094
+ const allCompleted = isAllCompletedPlanTodoCall(call.args);
14095
+ if (state.afterTool === "plan_todo" && !allCompleted) return "plan_todo rejected because the previous tool call was also plan_todo. Batch checklist updates and proceed with repository work.";
14096
+ if (state.maxPlanTodoUpdatesPerTurn !== void 0 && state.planTodoUpdates >= state.maxPlanTodoUpdatesPerTurn) return `plan_todo rejected because this turn already used ${state.planTodoUpdates} plan update(s), which meets the configured limit of ${state.maxPlanTodoUpdatesPerTurn}. Continue with repository work and summarize progress in the final response.`;
14097
+ if (isSamePlanTodoItems(call.args, currentPlan) && !allCompleted) return "plan_todo rejected because it does not change the visible plan. Continue with the next substantive tool call or final response.";
14098
+ }
14099
+ function isAllCompletedPlanTodoCall(args) {
14100
+ const items = args.items;
14101
+ return Array.isArray(items) && items.length > 0 && items.every((item) => {
14102
+ return Boolean(item && typeof item === "object" && "status" in item && item.status === "completed");
14103
+ });
14104
+ }
14105
+ function isSamePlanTodoItems(args, currentPlan) {
14106
+ const items = args.items;
14107
+ if (!Array.isArray(items) || items.length !== currentPlan.items.length) return false;
14108
+ return items.every((item, index) => {
14109
+ const current = currentPlan.items[index];
14110
+ if (!current || typeof item !== "object" || item === null) return false;
14111
+ const candidate = item;
14112
+ return candidate.text === current.text && candidate.status === current.status;
14113
+ });
14114
+ }
14017
14115
  function isImplementationTaskRequest(message) {
14018
14116
  return /\b(implement|complete|fix|add|update|change|modify|refactor|write|create|wire|hook|support|benchmark|task end-to-end)\b/iu.test(message);
14019
14117
  }
@@ -16431,4 +16529,4 @@ function formatDryRunSyncStatus(status) {
16431
16529
  //#endregion
16432
16530
  export { runTopchesterCli as t };
16433
16531
 
16434
- //# sourceMappingURL=cli-fySUzvp-.mjs.map
16532
+ //# sourceMappingURL=cli-2v1iJ0YF.mjs.map