/** * A tool stub that answers PER INVOCATION — the pure core. * * A stub shadows a binary on PATH (`gh`, `git`, `psql`) so a run that calls it * gets a recorded answer instead of the real service. The earlier stub printed * ONE answer for every argv. That is a test double answering by default, and a * default answer is a fabricated observation: a stub set up to answer * `gh issue create` with an issue URL also answered a script's * `gh api repos/o/r/issues?…` with that URL, the script failed to parse it as * JSON, and a model that read the stub's source stopped trusting it. * * So a stub is a non-empty list of RULES over the argv TOKEN ARRAY. Each rule has * a positional pattern (an exact string or a RegExp per token, and an explicit * trailing `rest` for "any further tokens"), an optional order-free `contains` * for flags, and a reply. The first matching rule answers. An argv no rule * matches is UNANSWERED: the stub exits non-zero, the call is logged, and the * run fails — the same choice `script-overrun.ts` made for a scripted model that * runs past its script. * * Argv is a list, not a line: joining it loses token boundaries (`--title "a b"` * is one token), and a RegExp over the joined line matches across them. * * Everything here is pure and JSON-serialisable, because the same rules are * read by the stub process (one per invocation), folded into the eval cache key * and the lock, and printed in failure messages. No zod here on purpose: this * module is on the stub process's path, which runs once per `gh` call. */ /** "Any number of further tokens, including none." Only valid as the LAST token. */ export interface ArgvRest { readonly kind: "rest"; } /** The `rest` marker. One frozen value, so a pattern carries no free-form object. */ export declare const ARGV_REST: ArgvRest; /** One positional token: exact string, or a RegExp tested against that one token. */ export type ArgvToken = string | Readonly; /** * A positional pattern: token i matches argv token i. Without a trailing * {@link ARGV_REST} the argv must have EXACTLY this many tokens, so a read rule * `["api", /^repos\//]` does not also answer `api repos/… -f title=x`. */ export type ArgvPattern = readonly ArgvToken[] | readonly [...ArgvToken[], ArgvRest]; /** What the fake binary does when a rule answers. */ export interface StubAnswer { /** Printed to stdout. Default "". */ readonly stdout?: string; /** Printed to stderr. Default none. */ readonly stderr?: string; /** Exit code, 0–255. Default 0. */ readonly exitCode?: number; } /** * `always`: every matching call gets the answer. `inOrder`: the k-th matching * call gets `answers[k-1]`, and a call past the end is UNANSWERED — repetition * is something a rule says, never a default. */ export type StubReply = ({ readonly kind: "always"; } & StubAnswer) | { readonly kind: "inOrder"; readonly answers: readonly [StubAnswer, ...StubAnswer[]]; }; /** One rule: an argv pattern, optional order-free tokens, and a reply. */ export interface StubRule { readonly argv: ArgvPattern; /** * Tokens that must each match some argv token AFTER the positional prefix, in * any order — for flags a model writes in any order (`--repo o/r` or `-R o/r`). * Needs a trailing `rest` in `argv` (without it nothing follows the prefix). */ readonly contains?: readonly ArgvToken[]; readonly reply: StubReply; } /** A stub for one binary: its name on PATH and its rules, first match wins. */ export interface ToolStub { readonly name: string; readonly rules: readonly [StubRule, ...StubRule[]]; } /** * What happened to one invocation. Three cases, because the two ways a call goes * unanswered need different fixes: no rule matched (add a rule), or a rule * matched but its `inOrder` answers ran out (add an answer). */ export type StubOutcome = { readonly kind: "answered"; /** 0-based rule index. */ readonly rule: number; /** 0-based answer index (always 0 for an `always` reply). */ readonly answer: number; } | { readonly kind: "no-rule"; } | { readonly kind: "exhausted"; readonly rule: number; } /** * The stub printed its neutral miss line but left no log line (its directory * was not writable from the agent's shell). Never read from a log: the eval * derives it from the agent's tool results. */ | { readonly kind: "unlogged"; }; /** One recorded invocation of a stub. */ export interface StubCall { readonly tool: string; readonly argv: readonly string[]; readonly outcome: StubOutcome; } /** Does `rule` match `argv`? Positional prefix, then `contains` over the tail. Pure, total. */ export declare function matchArgv(rule: Pick, argv: readonly string[]): boolean; /** How many times each rule of `tool` has already answered, from the log so far. */ export declare function priorAnswers(calls: readonly StubCall[], tool: string): ReadonlyMap; /** The decision for one invocation: (rules, answers so far, argv) → outcome. Pure, total. */ export declare function decideInvocation(stub: ToolStub, prior: ReadonlyMap, argv: readonly string[]): StubOutcome; /** Whether an outcome left the call without an answer. */ export declare function isUnanswered(o: StubOutcome): boolean; /** The answer an `answered` outcome refers to; `undefined` for an unanswered one. */ export declare function answerFor(stub: ToolStub, o: StubOutcome): StubAnswer | undefined; /** * Serialise a stub to JSON. A RegExp becomes `{kind:"re",source,flags}` — * `JSON.stringify(/x/)` is `{}`, which would make two different rules hash the * same in the cache key and the lock. */ export declare function encodeStub(stub: ToolStub): string; /** A typed {@link ToolStub} from a value already known to have its shape. */ export declare function toToolStub(v: unknown): ToolStub; /** * Read back what {@link encodeStub} wrote. The input is vigiles' own output, * written after the spec was parsed, so a bad shape is an internal error. */ export declare function decodeStub(json: string): ToolStub; /** One log line for one call (no trailing newline). */ export declare function stubLogLine(call: StubCall): string; /** Read the JSONL log the stub processes appended. Blank lines are skipped. */ export declare function parseStubLog(jsonl: string): readonly StubCall[]; /** A pattern the way a rule is written: `["issue", "create", experimental_stub.rest]`. */ export declare function describePattern(p: readonly (ArgvToken | ArgvRest)[]): string; /** * The failure message for a run with unanswered calls, addressed to the AUTHOR * (the model only ever sees a neutral one-liner). Each distinct argv once, with * a count; the rules it was tried against; and a rule for that exact command to * add. It never suggests a catch-all: answering everything is the defect this * exists to stop. `undefined` when every call was answered. */ export declare function unansweredMessage(calls: readonly StubCall[], stubs: readonly ToolStub[]): string | undefined; /** * The exit code of an unanswered call. Not 127: that tells the caller "not * installed", and a model changes plans for the wrong reason. */ export declare const UNANSWERED_EXIT_CODE = 97; /** * What an unanswered call prints to stderr — the only thing the MODEL sees. One * fixed line: no argv, no rules, no "add a rule". Instructions here would tell * the model it is in a test and what the author will do, and the rest of the * trial would measure a reaction to that. The diagnostic goes to the author, * from the log, through {@link unansweredMessage}. */ export declare function unsupportedLine(name: string): string; /** * The misses a stub could not log, found from the agent's side: every * occurrence of a stubbed tool's neutral miss line ({@link unsupportedLine}) in * `texts` (the agent's tool results) beyond the unanswered calls the log already * holds for that tool. The line is vigiles' own marker, not a guess about what * the agent ran, so this is a count of a thing we printed. A miss the log has * is counted once — by the log. Pure. */ export declare function unloggedMisses(texts: readonly string[], logged: readonly StubCall[], names: readonly string[]): readonly StubCall[]; /** Where a stub root keeps its files (`name` picks the per-stub ones). Pure path arithmetic. */ export declare function stubPaths(root: string, name?: string): { readonly binDir: string; readonly rulesDir: string; readonly rules: string; readonly log: string; readonly bin: string; }; /** The two file operations one invocation needs — injected, so the decision stays testable. */ export interface StubIo { /** File contents, or null when the file does not exist. */ readonly readFile: (path: string) => string | null; /** Append text; one call per log line (`O_APPEND`, one write). */ readonly appendFile: (path: string, text: string) => void; } /** What the stub process prints and how it exits. */ export interface StubProcessResult { readonly stdout: string; readonly stderr: string; readonly exitCode: number; } /** * One invocation: read the rules and the log so far, decide, append one log * line, return what to print. It never reads stdin: a caller that leaves the * pipe open (Node's `execFile` does) would block until its own timeout. * * Concurrency, said plainly: two simultaneous calls of the same `inOrder` rule * can both read "answered k times" and both get answer k. One line per write * keeps the log itself intact. */ export declare function runStub(root: string, name: string, argv: readonly string[], io: StubIo): StubProcessResult; //# sourceMappingURL=stub-rules.d.ts.map