/** * [WHO]: GrubController - drives autonomous iterative tasks with durable state and completion validation * [FROM]: Depends on node:crypto, node:path, ./grub-types, ./grub-persistence, ./grub-feature-list, ./grub-prompts * [TO]: Consumed by extension entry point (./index.ts) * [HERE]: extensions/builtin/grub/grub-controller.ts - state machine for /grub iterations with cross-session persistence and feature-list-gated completion */ import { type GrubLocale } from "./grub-i18n.js"; import type { GrubControllerState, GrubDecision, GrubTaskSnapshot, GrubTaskState } from "./grub-types.js"; import type { Usage } from "@catui/ai/types"; export interface GrubStartOptions { maxIterations?: number; maxConsecutiveFailures?: number; maxInitializerFailures?: number; locale?: GrubLocale; } export declare class GrubController { private activeTask?; private lastTerminalTask?; getState(): GrubControllerState; hasActiveTask(): boolean; getActiveTask(): GrubTaskState | undefined; start(goal: string, cwd: string, options?: GrubStartOptions): GrubTaskState; /** * Adopt a previously persisted task, e.g. after process restart. Does not * auto-dispatch the next iteration; the caller decides whether to continue. */ adoptResumedTask(task: GrubTaskState): GrubTaskState; stop(reason: string, status?: GrubTaskSnapshot["status"]): GrubTaskSnapshot | undefined; /** * Fold a single agent-run result into the active task's cumulative totals. * Called by the extension when an `agent_result` event fires while a grub * task is in flight. No-op when there is no active task (e.g. between * consecutive grub runs) so old events do not pollute the next run. */ accumulateRunResult(input: { turnCount?: number; toolCallCount?: number; durationMs?: number; usage?: Usage; }): void; /** Read-only accessor used by tests and by the extension for live preview. */ getActiveCumulative(): { turnCount: number; toolCallCount: number; durationMs: number; usage: Usage; } | undefined; isGrubPrompt(prompt: string): boolean; buildPrompt(): string; markDispatched(): GrubTaskState; /** * Enforce the feature-list contract at the state-machine boundary. The * initializer may replace the placeholder with the first complete list; after * that, every turn may only change passes/evidence fields. */ validateFeatureListAfterTurn(): { ok: true; } | { ok: false; message: string; }; /** * Validate a completion decision against the feature-list. If the decision * says `complete` but the persisted feature-list still has pending entries, * the decision is downgraded to `continue` with a synthetic nextStep. * Returns the (possibly rewritten) decision. */ validateCompletion(decision: GrubDecision): { decision: GrubDecision; downgraded: boolean; reason?: string; }; finishTurn(decision: GrubDecision): { action: "continue" | "stop"; task?: GrubTaskState; snapshot?: GrubTaskSnapshot; }; recordFailure(message: string): { action: "continue" | "stop"; task?: GrubTaskState; snapshot?: GrubTaskSnapshot; }; private generateTaskId; private validateInitializerStructure; private cloneFeatureList; private safePersist; }