import { Task } from './todo'; import { RateLimitError } from './rateLimit'; export type CliExitDecision = { kind: 'done'; } | { kind: 'deferred'; } | { kind: 'rate_limit'; error: RateLimitError; } | { kind: 'remind'; } | { kind: 'give_up'; } | { kind: 'hard_fail'; reason: string; }; /** * Process-level signals captured by the caller at CLI-exit time, used to tell * a provider hard-failure apart from a ran-but-unmarked completion. */ export interface CliExitContext { /** The CLI process exited non-zero — the provider's real failure surface. */ exitedNonZero?: boolean; /** * The provider wrote an error sentinel to its stdout capture this turn even * though it exited 0 (opencode SDK appends `[ERROR] …` on session.error and * then still writes exit-code 0). Carries a short reason label when set. */ stdoutError?: string | null; } export declare class CliExitHandler { private readonly workspaceRoot; private readonly todoPath; private readonly task; private readonly taskStartTime; private readonly isTaskDone; private reminderSent; constructor(workspaceRoot: string, todoPath: string, task: Task, taskStartTime: number, isTaskDone: () => boolean); /** Run the decision tree. Stateless except for the one-shot reminder flag. */ decide(ctx?: CliExitContext): CliExitDecision; /** * Decide whether THIS exit was a provider hard-failure that produced no real * work — as opposed to a turn that ran fine and merely left the box unticked. * Returns a short reason label on a hard-failure, else null. * * Signals (any one is sufficient): * - a recent SessionEnd whose `reason` is a provider-failure reason * - a recent StopFailure hook (grok/opencode failed turn) * - a recent `reauth_required` hook (token/OAuth gate) * - the provider wrote an [ERROR] sentinel to stdout but still exited 0 * (opencode SDK session.error / "No model available") * - a non-zero process exit with NO assistant work this cycle (crash / * launch-failed / copilot free-plan "No model available") * * A `watchdog` kill is treated as a hard-failure ONLY when the turn produced * no work — a watchdog that fired after real output is a ran-but-unmarked * turn (partial progress), which stays on the auto-mark path. */ private findHardFailure; /** * Scan the recent hook-events tail (events after this task started) for a * provider-failure signal and whether any assistant work happened this turn. * Mirrors findRecentRateLimit()'s read strategy. */ private scanHooksForFailure; /** Has the AI moved the task to [~]? */ private taskIsInProgress; /** * Scan recent hook events (StopFailure / SessionEnd) for a rate-limit * signal that arrived AFTER this task started. Returns null when none. * Looks at both `error: "rate_limit"` and `last_assistant_message` text * — Claude surfaces throttles through both channels at different times. */ private findRecentRateLimit; }