/** * A {@link ReviewAnswer} that runs shell commands and hands the failure back. * * `reviewAnswer` was the seam for exactly this — judge the answer at the point * the model stops calling tools, and return it with feedback instead of * settling — and nothing shipped supplied one, so an operator who wanted * "don't finish until the build passes" had to write TypeScript. This is the * supplier. With it, `--gate 'pnpm test'` is the whole unattended story: the * model works, stops, the tests run, and a failure comes back as the next user * turn rather than as a green run somebody discovers in CI. * * The kernel already bounds it. The reviewer is consulted only when the model * stopped calling tools, never on the forced-final turn, and a rejection * budget stops the turn with `answer_rejected` — a stop reason that names the * reviewer rather than blaming a token budget. None of that is re-implemented * here. * * ## The part that is not just "run a command" * * **Before re-running a command that already failed, the workspace is * fingerprinted, and an identical fingerprint means the command is NOT run.** * * This is the difference between a bounded loop and one that spends its whole * budget. A model that has run out of ideas answers again without editing * anything; re-running the suite then costs a full execution — often the most * expensive thing in the loop — to produce a failure already known character * for character. Worse, the feedback is identical, so the model is handed the * same prompt that just failed to help it. Saying instead "the workspace has * not changed since that failure; edit something before trying to finish" * is both cheaper and a different instruction. * * The attempt still advances. Skipping the command is a saving, not a pardon: * an answer that changed nothing has been rejected, and the turn's budget must * see that or a stuck model loops forever for free. * * And it fails open on the cheap side. No fingerprint — a git invocation that * errored, a timeout, output past the cap, a tree with no commits — means the * command runs. See {@link fingerprintWorkspace}: the cost of re-running * unnecessarily is one execution; the cost of wrongly skipping is a * verification that silently did not happen. */ import type { CommandOptions, CommandResult } from '../types/execution/index.js'; import type { ReviewAnswer } from '../types/session/answer-review.js'; /** How the gate runs a command. Injected so a test needs no shell. */ export type GateExec = (command: string, args: string[], options?: CommandOptions) => Promise; /** Default per-command deadline. A test suite is allowed to be slow. */ export declare const DEFAULT_GATE_TIMEOUT_MS = 600000; /** How many attempts the gate will EXECUTE its commands for, by default. */ export declare const DEFAULT_GATE_MAX_RETRIES = 3; /** * Model-visible characters of a failing command's output. * * Head and tail, not head alone: a compiler names the file at the top and a * test runner names the failure at the bottom, and a gate that only ever kept * one end would be useless for one of them. */ export declare const DEFAULT_GATE_OUTPUT_CHARS = 4000; export interface CommandGateOptions { /** * Shell command lines, run in order, stopping at the first failure. * * In order and short-circuiting because that is what a person means by * "typecheck then test": a type error makes the test output noise about * the same cause, and handing the model both invites it to fix the * symptom. */ readonly commands: readonly string[]; /** Directory the commands run in, and the tree that is fingerprinted. */ readonly cwd: string; /** * How many attempts will actually EXECUTE the commands. * * Past it the gate rejects without running anything, naming the * exhaustion. It does not accept: an answer that never passed the gate * has not passed the gate, and a reviewer that gave up by accepting would * hand back a green run over a red build — the exact outcome the gate * exists to prevent. What ENDS the turn is the kernel's rejection budget, * so set that to the same number (the CLI does). */ readonly maxRetries?: number; /** Per-command deadline. See {@link DEFAULT_GATE_TIMEOUT_MS}. */ readonly timeoutMs?: number; /** Override the executor. Defaults to a local shell in `cwd`. */ readonly exec?: GateExec; /** See {@link DEFAULT_GATE_OUTPUT_CHARS}. */ readonly maxOutputChars?: number; /** * Override the change detector. Defaults to * {@link fingerprintWorkspace} over `cwd`. * * Returning `null` means "cannot tell", and the gate then runs its * commands. A detector that returned a constant would silence the gate * after its first failure, so this seam exists for tests and for a host * whose workspace is not a git tree — not as a way to turn the check off. */ readonly fingerprint?: () => Promise; } /** Head and tail of a command's output, with the middle marked as dropped. */ export declare function clipOutput(text: string, max: number): string; /** * Build a reviewer that accepts an answer only when every command passes. * * Stateful across calls within one turn, deliberately: the whole point is that * attempt N+1 can be compared with attempt N. Build one gate per turn. */ export declare function createCommandGate(options: CommandGateOptions): ReviewAnswer; //# sourceMappingURL=command-gate.d.ts.map