CLI: improve stream recovery and add configurable consecutive mistake limit (#11775)

* cli: improve stream error recovery and add mistake-limit flag

* More progress
This commit is contained in:
Chris Estreich 2026-02-26 21:09:38 -08:00 committed by GitHub
parent 96d77fd2c8
commit 2f4ce367c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 398 additions and 22 deletions

View file

@ -185,6 +185,7 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.6` |
| `--mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
| `-r, --reasoning-effort <effort>` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` |
| `--consecutive-mistake-limit <n>` | Consecutive error/repetition limit before guidance prompt (`0` disables the limit) | `10` |
| `--ephemeral` | Run without persisting state (uses temporary storage) | `false` |
| `--oneshot` | Exit upon task completion | `false` |
| `--output-format <format>` | Output format with `--print`: `text`, `json`, or `stream-json` | `text` |

View file

@ -0,0 +1,35 @@
import type { ClineMessage } from "@roo-code/types"
import { detectAgentState } from "../agent-state.js"
import { taskCompleted } from "../events.js"
function createMessage(overrides: Partial<ClineMessage>): ClineMessage {
return { ts: Date.now() + Math.random() * 1000, type: "say", ...overrides }
}
describe("taskCompleted", () => {
it("returns true for completion_result", () => {
const previous = detectAgentState([createMessage({ type: "say", say: "text", text: "working" })])
const current = detectAgentState([createMessage({ type: "ask", ask: "completion_result", partial: false })])
expect(taskCompleted(previous, current)).toBe(true)
})
it("returns true for resume_completed_task", () => {
const previous = detectAgentState([createMessage({ type: "say", say: "text", text: "working" })])
const current = detectAgentState([createMessage({ type: "ask", ask: "resume_completed_task", partial: false })])
expect(taskCompleted(previous, current)).toBe(true)
})
it("returns false for recoverable idle asks", () => {
const previous = detectAgentState([createMessage({ type: "say", say: "text", text: "working" })])
const mistakeLimit = detectAgentState([
createMessage({ type: "ask", ask: "mistake_limit_reached", partial: false }),
])
const apiFailed = detectAgentState([createMessage({ type: "ask", ask: "api_req_failed", partial: false })])
expect(taskCompleted(previous, mistakeLimit)).toBe(false)
expect(taskCompleted(previous, apiFailed)).toBe(false)
})
})

View file

@ -5,6 +5,8 @@ import fs from "fs"
import type { ExtensionMessage, WebviewMessage } from "@roo-code/types"
import { DEFAULT_FLAGS } from "@/types/index.js"
import { type ExtensionHostOptions, ExtensionHost } from "../extension-host.js"
import { ExtensionClient } from "../extension-client.js"
import { AgentLoopState } from "../agent-state.js"
@ -593,6 +595,20 @@ describe("ExtensionHost", () => {
expect(initialSettings.mode).toBe("architect")
})
it("should use default consecutiveMistakeLimit when not provided", () => {
const host = createTestHost()
const initialSettings = getPrivate<Record<string, unknown>>(host, "initialSettings")
expect(initialSettings.consecutiveMistakeLimit).toBe(DEFAULT_FLAGS.consecutiveMistakeLimit)
})
it("should set consecutiveMistakeLimit from options", () => {
const host = createTestHost({ consecutiveMistakeLimit: 8 })
const initialSettings = getPrivate<Record<string, unknown>>(host, "initialSettings")
expect(initialSettings.consecutiveMistakeLimit).toBe(8)
})
it("should enable auto-approval in non-interactive mode", () => {
const host = createTestHost({ nonInteractive: true })

View file

@ -0,0 +1,129 @@
import type { ClineMessage } from "@roo-code/types"
import { Writable } from "stream"
import type { TaskCompletedEvent } from "../events.js"
import { JsonEventEmitter } from "../json-event-emitter.js"
import { AgentLoopState, type AgentStateInfo } from "../agent-state.js"
function createMockStdout(): { stdout: NodeJS.WriteStream; lines: () => Record<string, unknown>[] } {
const chunks: string[] = []
const writable = new Writable({
write(chunk, _encoding, callback) {
chunks.push(chunk.toString())
callback()
},
}) as unknown as NodeJS.WriteStream
const lines = () =>
chunks
.join("")
.split("\n")
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as Record<string, unknown>)
return { stdout: writable, lines }
}
function emitMessage(emitter: JsonEventEmitter, message: ClineMessage): void {
;(emitter as unknown as { handleMessage: (msg: ClineMessage, isUpdate: boolean) => void }).handleMessage(
message,
false,
)
}
function emitTaskCompleted(emitter: JsonEventEmitter, event: TaskCompletedEvent): void {
;(emitter as unknown as { handleTaskCompleted: (taskCompleted: TaskCompletedEvent) => void }).handleTaskCompleted(
event,
)
}
function createAskCompletionMessage(ts: number, text = ""): ClineMessage {
return {
ts,
type: "ask",
ask: "completion_result",
partial: false,
text,
} as ClineMessage
}
function createCompletedStateInfo(message: ClineMessage): AgentStateInfo {
return {
state: AgentLoopState.IDLE,
isWaitingForInput: true,
isRunning: false,
isStreaming: false,
currentAsk: "completion_result",
requiredAction: "start_task",
lastMessageTs: message.ts,
lastMessage: message,
description: "Task completed successfully. You can provide feedback or start a new task.",
}
}
describe("JsonEventEmitter result emission", () => {
it("prefers current completion message content over stale cached completion text", () => {
const { stdout, lines } = createMockStdout()
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
emitMessage(emitter, {
ts: 100,
type: "say",
say: "completion_result",
partial: false,
text: "FIRST",
} as ClineMessage)
const firstCompletionMessage = createAskCompletionMessage(101, "")
emitTaskCompleted(emitter, {
success: true,
stateInfo: createCompletedStateInfo(firstCompletionMessage),
message: firstCompletionMessage,
})
const secondCompletionMessage = createAskCompletionMessage(102, "SECOND")
emitTaskCompleted(emitter, {
success: true,
stateInfo: createCompletedStateInfo(secondCompletionMessage),
message: secondCompletionMessage,
})
const output = lines().filter((line) => line.type === "result")
expect(output).toHaveLength(2)
expect(output[0]?.content).toBe("FIRST")
expect(output[1]?.content).toBe("SECOND")
})
it("clears cached completion text after each result emission", () => {
const { stdout, lines } = createMockStdout()
const emitter = new JsonEventEmitter({ mode: "stream-json", stdout })
emitMessage(emitter, {
ts: 200,
type: "say",
say: "completion_result",
partial: false,
text: "FIRST",
} as ClineMessage)
const firstCompletionMessage = createAskCompletionMessage(201, "")
emitTaskCompleted(emitter, {
success: true,
stateInfo: createCompletedStateInfo(firstCompletionMessage),
message: firstCompletionMessage,
})
const secondCompletionMessage = createAskCompletionMessage(202, "")
emitTaskCompleted(emitter, {
success: true,
stateInfo: createCompletedStateInfo(secondCompletionMessage),
message: secondCompletionMessage,
})
const output = lines().filter((line) => line.type === "result")
expect(output).toHaveLength(2)
expect(output[0]?.content).toBe("FIRST")
expect(output[1]).not.toHaveProperty("content")
})
})

View file

@ -260,7 +260,7 @@ export function streamingEnded(previous: AgentStateInfo, current: AgentStateInfo
* Helper to determine if task completed.
*/
export function taskCompleted(previous: AgentStateInfo, current: AgentStateInfo): boolean {
const completionAsks = ["completion_result", "api_req_failed", "mistake_limit_reached"]
const completionAsks = ["completion_result", "resume_completed_task"]
const wasNotComplete = !previous.currentAsk || !completionAsks.includes(previous.currentAsk)
const isNowComplete = current.currentAsk !== undefined && completionAsks.includes(current.currentAsk)
return wasNotComplete && isNowComplete

View file

@ -26,7 +26,7 @@ import type {
import { createVSCodeAPI, IExtensionHost, ExtensionHostEventMap, setRuntimeConfigValues } from "@roo-code/vscode-shim"
import { DebugLogger, setDebugLogEnabled } from "@roo-code/core/cli"
import type { SupportedProvider } from "@/types/index.js"
import { DEFAULT_FLAGS, type SupportedProvider } from "@/types/index.js"
import type { User } from "@/lib/sdk/index.js"
import { getProviderSettings } from "@/lib/utils/provider.js"
import { createEphemeralStorageDir } from "@/lib/storage/index.js"
@ -66,6 +66,7 @@ const CLI_PACKAGE_ROOT = process.env.ROO_CLI_ROOT || findCliPackageRoot()
export interface ExtensionHostOptions {
mode: string
reasoningEffort?: ReasoningEffortExtended | "unspecified" | "disabled"
consecutiveMistakeLimit?: number
user: User | null
provider: SupportedProvider
apiKey?: string
@ -219,6 +220,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
// Populate initial settings.
const baseSettings: RooCodeSettings = {
mode: this.options.mode,
consecutiveMistakeLimit: this.options.consecutiveMistakeLimit ?? DEFAULT_FLAGS.consecutiveMistakeLimit,
commandExecutionTimeout: 30,
enableCheckpoints: false,
experiments: {

View file

@ -96,6 +96,7 @@ export class JsonEventEmitter {
private stdout: NodeJS.WriteStream
private events: JsonEvent[] = []
private unsubscribers: (() => void)[] = []
private pendingWrites = new Set<Promise<void>>()
private lastCost: JsonEventCost | undefined
private requestIdProvider: () => string | undefined
private schemaVersion: number
@ -598,8 +599,9 @@ export class JsonEventEmitter {
* Handle task completion and emit result event.
*/
private handleTaskCompleted(event: TaskCompletedEvent): void {
// Use tracked completion result content, falling back to event message
const resultContent = this.completionResultContent || event.message?.text || this.lastAssistantText
// Prefer the completion payload from the current event. If it is empty,
// fall back to the most recent tracked completion text, then assistant text.
const resultContent = event.message?.text || this.completionResultContent || this.lastAssistantText
this.emitEvent({
type: "result",
@ -610,6 +612,10 @@ export class JsonEventEmitter {
cost: this.lastCost,
})
// Prevent stale completion content from leaking into later turns.
this.completionResultContent = undefined
this.lastAssistantText = undefined
// For "json" mode, output the final accumulated result
if (this.mode === "json") {
this.outputFinalResult(event.success, resultContent)
@ -647,7 +653,7 @@ export class JsonEventEmitter {
* Output a single JSON line (NDJSON format).
*/
private outputLine(data: unknown): void {
this.stdout.write(JSON.stringify(data) + "\n")
this.writeToStdout(JSON.stringify(data) + "\n")
}
/**
@ -662,7 +668,31 @@ export class JsonEventEmitter {
events: this.events.filter((e) => e.type !== "result"), // Exclude the result event itself
}
this.stdout.write(JSON.stringify(output, null, 2) + "\n")
this.writeToStdout(JSON.stringify(output, null, 2) + "\n")
}
private writeToStdout(content: string): void {
const writePromise = new Promise<void>((resolve, reject) => {
this.stdout.write(content, (error?: Error | null) => {
if (error) {
reject(error)
return
}
resolve()
})
})
this.pendingWrites.add(writePromise)
void writePromise.finally(() => {
this.pendingWrites.delete(writePromise)
})
}
async flush(): Promise<void> {
while (this.pendingWrites.size > 0) {
await Promise.all([...this.pendingWrites])
}
}
/**

View file

@ -343,13 +343,16 @@ export class MessageProcessor {
// Task completed
if (taskCompleted(previousState, currentState)) {
const completedSuccessfully =
currentState.currentAsk === "completion_result" || currentState.currentAsk === "resume_completed_task"
if (this.options.debug) {
debugLog("[MessageProcessor] EMIT taskCompleted", {
success: currentState.currentAsk === "completion_result",
success: completedSuccessfully,
})
}
const completedEvent: TaskCompletedEvent = {
success: currentState.currentAsk === "completion_result",
success: completedSuccessfully,
stateInfo: currentState,
message: currentState.lastMessage,
}

View file

@ -3,8 +3,9 @@ import path from "path"
import { fileURLToPath } from "url"
import { createElement } from "react"
import type { HistoryItem } from "@roo-code/types"
import pWaitFor from "p-wait-for"
import type { HistoryItem } from "@roo-code/types"
import { setLogger } from "@roo-code/vscode-shim"
import {
@ -35,6 +36,17 @@ import { runStdinStreamMode } from "./stdin-stream.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROO_MODEL_WARMUP_TIMEOUT_MS = 10_000
const SIGNAL_ONLY_EXIT_KEEPALIVE_MS = 60_000
const STREAM_RESUME_WAIT_TIMEOUT_MS = 2_000
async function bootstrapResumeForStdinStream(host: ExtensionHost, sessionId: string): Promise<void> {
host.sendToExtension({ type: "showTaskWithId", text: sessionId })
// Best-effort wait so early stdin "message" commands can target the resumed task.
await pWaitFor(() => host.client.hasActiveTask() || host.isWaitingForInput(), {
interval: 25,
timeout: STREAM_RESUME_WAIT_TIMEOUT_MS,
}).catch(() => undefined)
}
function normalizeError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error))
@ -185,10 +197,21 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
(settings.dangerouslySkipPermissions === undefined ? undefined : !settings.dangerouslySkipPermissions)
const effectiveRequireApproval = flagOptions.requireApproval || legacyRequireApprovalFromSettings || false
const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false
const rawConsecutiveMistakeLimit =
flagOptions.consecutiveMistakeLimit ?? settings.consecutiveMistakeLimit ?? DEFAULT_FLAGS.consecutiveMistakeLimit
const effectiveConsecutiveMistakeLimit = Number(rawConsecutiveMistakeLimit)
if (!Number.isInteger(effectiveConsecutiveMistakeLimit) || effectiveConsecutiveMistakeLimit < 0) {
console.error(
`[CLI] Error: Invalid consecutive mistake limit: ${rawConsecutiveMistakeLimit}; must be a non-negative integer`,
)
process.exit(1)
}
const extensionHostOptions: ExtensionHostOptions = {
mode: effectiveMode,
reasoningEffort: effectiveReasoningEffort === "unspecified" ? undefined : effectiveReasoningEffort,
consecutiveMistakeLimit: effectiveConsecutiveMistakeLimit,
user: null,
provider: effectiveProvider,
model: effectiveModel,
@ -336,12 +359,6 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
process.exit(1)
}
if (flagOptions.stdinPromptStream && isResumeRequested) {
console.error("[CLI] Error: cannot use --session-id/--continue with --stdin-prompt-stream")
console.error("[CLI] Usage: roo --print --output-format stream-json --stdin-prompt-stream [options]")
process.exit(1)
}
const useStdinPromptStream = flagOptions.stdinPromptStream
if (!isTuiEnabled) {
@ -445,6 +462,27 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
keepAliveInterval = undefined
}
const flushStdout = async () => {
try {
if (!process.stdout.writable || process.stdout.destroyed) {
return
}
await new Promise<void>((resolve, reject) => {
process.stdout.write("", (error?: Error | null) => {
if (error) {
reject(error)
return
}
resolve()
})
})
} catch {
// Best effort: shutdown should proceed even if stdout flush fails.
}
}
const ensureKeepAliveInterval = () => {
if (!signalOnlyExit || keepAliveInterval) {
return
@ -521,6 +559,10 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
}
await disposeHost()
if (jsonEmitter) {
await jsonEmitter.flush()
}
await flushStdout()
process.exit(exitCode)
}
@ -551,6 +593,25 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
throw new Error("--stdin-prompt-stream requires --output-format=stream-json to emit control events")
}
if (isResumeRequested) {
const resolvedSessionId =
requestedSessionId ||
getMostRecentTaskIdInWorkspace(taskHistorySnapshot, effectiveWorkspacePath)
if (requestedSessionId && taskHistorySnapshot.length > 0) {
const hasRequestedTask = taskHistorySnapshot.some((item) => item.id === requestedSessionId)
if (!hasRequestedTask) {
throw new Error(`Session not found in task history: ${requestedSessionId}`)
}
}
if (!resolvedSessionId) {
throw new Error("No previous tasks found to continue in this workspace.")
}
await bootstrapResumeForStdinStream(host, resolvedSessionId)
}
await runStdinStreamMode({
host,
jsonEmitter,
@ -582,6 +643,10 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
}
await disposeHost()
if (jsonEmitter) {
await jsonEmitter.flush()
}
await flushStdout()
if (signalOnlyExit) {
await parkUntilSignal("Task loop completed")
@ -595,6 +660,10 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
} catch (error) {
emitRuntimeError(normalizeError(error))
await disposeHost()
if (jsonEmitter) {
await jsonEmitter.flush()
}
await flushStdout()
if (signalOnlyExit) {
await parkUntilSignal("Task loop failed")

View file

@ -186,6 +186,10 @@ function isCancellationLikeError(error: unknown): boolean {
const RESUME_ASKS = new Set(["resume_task", "resume_completed_task"])
const CANCEL_RECOVERY_WAIT_TIMEOUT_MS = 8_000
const CANCEL_RECOVERY_POLL_INTERVAL_MS = 100
const STDIN_EOF_RESUME_WAIT_TIMEOUT_MS = 2_000
const STDIN_EOF_POLL_INTERVAL_MS = 100
const STDIN_EOF_IDLE_ASKS = new Set(["completion_result", "resume_completed_task"])
const STDIN_EOF_IDLE_STABLE_POLLS = 2
function isResumableState(host: ExtensionHost): boolean {
const agentState = host.client.getAgentState()
@ -208,6 +212,69 @@ async function waitForPostCancelRecovery(host: ExtensionHost): Promise<void> {
}
}
async function waitForTaskProgressAfterStdinClosed(
host: ExtensionHost,
getQueueState: () => { hasSeenQueueState: boolean; queueDepth: number },
): Promise<void> {
while (host.client.hasActiveTask()) {
if (!host.isWaitingForInput()) {
await new Promise((resolve) => setTimeout(resolve, STDIN_EOF_POLL_INTERVAL_MS))
continue
}
const deadline = Date.now() + STDIN_EOF_RESUME_WAIT_TIMEOUT_MS
while (Date.now() < deadline) {
if (!host.client.hasActiveTask() || !host.isWaitingForInput()) {
break
}
await new Promise((resolve) => setTimeout(resolve, STDIN_EOF_POLL_INTERVAL_MS))
}
if (host.client.hasActiveTask() && host.isWaitingForInput()) {
const currentAsk = host.client.getCurrentAsk()
const { hasSeenQueueState, queueDepth } = getQueueState()
// EOF is allowed when the task has reached an idle completion boundary and
// there is no queued user input waiting to be processed.
if (
hasSeenQueueState &&
queueDepth === 0 &&
typeof currentAsk === "string" &&
STDIN_EOF_IDLE_ASKS.has(currentAsk)
) {
let isStable = true
for (let i = 1; i < STDIN_EOF_IDLE_STABLE_POLLS; i++) {
await new Promise((resolve) => setTimeout(resolve, STDIN_EOF_POLL_INTERVAL_MS))
if (!host.client.hasActiveTask() || !host.isWaitingForInput()) {
isStable = false
break
}
const nextAsk = host.client.getCurrentAsk()
const nextQueueState = getQueueState()
if (
nextAsk !== currentAsk ||
!nextQueueState.hasSeenQueueState ||
nextQueueState.queueDepth !== 0
) {
isStable = false
break
}
}
if (isStable) {
return
}
}
throw new Error(`stdin ended while task was waiting for input (${currentAsk ?? "unknown"})`)
}
}
}
export async function runStdinStreamMode({ host, jsonEmitter, setStreamRequestId }: StdinStreamModeOptions) {
let hasReceivedStdinCommand = false
let shouldShutdown = false
@ -633,13 +700,15 @@ export async function runStdinStreamMode({ host, jsonEmitter, setStreamRequestId
host.client.cancelTask()
}
if (!shouldShutdown && host.client.hasActiveTask() && host.isWaitingForInput()) {
const currentAsk = host.client.getCurrentAsk()
throw new Error(`stdin ended while task was waiting for input (${currentAsk ?? "unknown"})`)
}
if (!shouldShutdown && activeTaskPromise) {
await activeTaskPromise
if (!shouldShutdown) {
if (activeTaskPromise) {
await activeTaskPromise
} else if (host.client.hasActiveTask()) {
await waitForTaskProgressAfterStdinClosed(host, () => ({
hasSeenQueueState,
queueDepth: lastQueueDepth,
}))
}
}
} finally {
offClientError()

View file

@ -50,6 +50,11 @@ program
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
DEFAULT_FLAGS.reasoningEffort,
)
.option(
"--consecutive-mistake-limit <limit>",
"Consecutive error/repetition limit before guidance prompt (0 disables the limit)",
(value) => Number.parseInt(value, 10),
)
.option("--exit-on-error", "Exit on API request errors instead of retrying", false)
.option("--ephemeral", "Run without persisting state (uses temporary storage)", false)
.option("--oneshot", "Exit upon task completion", false)

View file

@ -105,6 +105,7 @@ describe("Settings Storage", () => {
provider: "anthropic" as const,
model: "claude-opus-4.6",
reasoningEffort: "medium" as const,
consecutiveMistakeLimit: 5,
})
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
@ -114,6 +115,7 @@ describe("Settings Storage", () => {
expect(settings.provider).toBe("anthropic")
expect(settings.model).toBe("claude-opus-4.6")
expect(settings.reasoningEffort).toBe("medium")
expect(settings.consecutiveMistakeLimit).toBe(5)
})
it("should create config directory if it doesn't exist", async () => {
@ -168,6 +170,7 @@ describe("Settings Storage", () => {
provider: "openai-native" as const,
model: "gpt-4o",
reasoningEffort: "low" as const,
consecutiveMistakeLimit: 7,
}
await saveSettings(defaultSettings)
@ -177,6 +180,14 @@ describe("Settings Storage", () => {
expect(loaded.provider).toBe("openai-native")
expect(loaded.model).toBe("gpt-4o")
expect(loaded.reasoningEffort).toBe("low")
expect(loaded.consecutiveMistakeLimit).toBe(7)
})
it("should support consecutiveMistakeLimit setting", async () => {
await saveSettings({ consecutiveMistakeLimit: 0 })
const loaded = await loadSettings()
expect(loaded.consecutiveMistakeLimit).toBe(0)
})
it("should support requireApproval setting", async () => {
@ -218,6 +229,7 @@ describe("Settings Storage", () => {
provider: "anthropic" as const,
model: "claude-sonnet-4-20250514",
reasoningEffort: "high" as const,
consecutiveMistakeLimit: 9,
requireApproval: true,
oneshot: true,
}
@ -229,6 +241,7 @@ describe("Settings Storage", () => {
expect(loaded.provider).toBe("anthropic")
expect(loaded.model).toBe("claude-sonnet-4-20250514")
expect(loaded.reasoningEffort).toBe("high")
expect(loaded.consecutiveMistakeLimit).toBe(9)
expect(loaded.requireApproval).toBe(true)
expect(loaded.oneshot).toBe(true)
})

View file

@ -4,6 +4,7 @@ export const DEFAULT_FLAGS = {
mode: "code",
reasoningEffort: "medium" as const,
model: "anthropic/claude-opus-4.6",
consecutiveMistakeLimit: 10,
}
export const REASONING_EFFORTS = [...reasoningEffortsExtended, "unspecified", "disabled"]

View file

@ -35,6 +35,7 @@ export type FlagOptions = {
model?: string
mode?: string
reasoningEffort?: ReasoningEffortFlagOptions
consecutiveMistakeLimit?: number
ephemeral: boolean
oneshot: boolean
outputFormat?: OutputFormat
@ -61,6 +62,8 @@ export interface CliSettings {
model?: string
/** Default reasoning effort level */
reasoningEffort?: ReasoningEffortFlagOptions
/** Default consecutive error/repetition limit before guidance prompts */
consecutiveMistakeLimit?: number
/** Require manual approval for tools/commands/browser/MCP actions */
requireApproval?: boolean
/** @deprecated Legacy inverse setting kept for backward compatibility */