/** * The command-outcome oracle (FR-14, Story 4.4). * * **Why this file exists.** Measured against the running host on 2026-08-03 by * dumping raw hook stdin: the Bash `PostToolUse` payload contains no exit code * in any form — `tool_response` is exactly `stdout`, `stderr`, `interrupted`, * `isImage`, `noOutputExpected`, and no key anywhere in the payload matches * /exit|code|status/i. Worse, **a command the host deems FAILED fires no * `PostToolUse` at all** (`exit 3`, `exit 7`, `process.exit(2)` and a bare * failing `npx vitest run` each produced zero payloads), so the capture hook is * structurally blind to failure. "The hook fired" is not a usable success * signal either: `grep -c ` exits 1 and the host still reports * success. * * **This was not a latent gap — it was a live, silent outage.** The store held * **4,881 `command_runs` of which 2 carried an exit code** (both fixtures), * **0** stdout tails, and `writeCommandEpisodes`' two outcome-gated writers * (`command_failure`, `test_cycle`) had **never fired in production**. Both are * gated on `exitCode !== undefined`, so they failed safe — the AD-12 shape: * wired, running, and dead, with nothing anywhere saying so. * * **What this reads instead.** The host transcript (`transcript_path`, present * in every hook payload) is JSONL in the Anthropic messages shape: a Bash call * appears as a `tool_use` block (`name: "Bash"`, `id`, `input.command`) and its * result as a `tool_result` block carrying the same id and a structured * **`is_error` boolean**. * * **`is_error: true` is not the same question as "did this command fail".** It * also covers calls the host never ran at all. Measured over all 45 transcripts * on this machine (6,456 paired Bash calls, 130 failures): **5 of the 130 carry * no `Exit code N` line**, and every one of those five never executed — three * `Blocked:` refusals, one `InputValidationError`, and one the **user * explicitly denied**. Recording those as failed runs would fabricate an * execution that never happened, and the user-denial case would record a * command the user refused. So the exit line is required as the witness that a * process actually ran and returned a status; the remaining 125 all carry it, * so the gate costs no real failure. What is *never* branched on is the exit * value itself — only its presence gates, and its value is stored as recorded. * * **What this deliberately does NOT do.** It makes no claim about the present. * An earlier draft of this story used the same oracle to answer "would this * command still pass?", which requires knowing a command's complete input set * and that nothing has changed it. Three review layers over two rounds found * six reachable ways to answer that wrongly — the failure the PRD names as the * worst this product can produce — and every fix narrowed the answerable set * further, to a measured **4 eligible commands out of 2,051**, all four of them * the developer's own probes. That half was withdrawn by ruling (ShuromiU, * 2026-08-03). Recording *"the build failed at 14:32"* carries none of that * risk, because it asserts nothing about now. * * **Cost.** Runs only in the cold-path flush (AD-2/N-4: never a Node spawn per * tool call), and reads only a bounded TAIL of the file — never the whole * transcript, which reached 11 MB in the session that specified this story. * * **Failure direction.** Every unreadable, missing, malformed or truncated case * yields *less* evidence, and less evidence means fewer outcomes attached. An * outcome that cannot be established is simply not attached. Nothing here can * manufacture one (AD-6). */ export declare const TRANSCRIPT_DEFAULT_MAX_BYTES: number; export declare const TRANSCRIPT_DEFAULT_MAX_LINES = 20000; export interface TranscriptLimits { maxBytes: number; maxLines: number; } export interface TranscriptOutcome { toolUseId: string; /** Raw command text, exactly as the host recorded it. Redaction is the caller's. */ command: string; /** True = the host reported this call as failed AND it demonstrably executed. */ failed: boolean; /** * Non-null exactly when `failed` is true: a failure with no `Exit code N` * line never executed and yields no outcome at all (see the file header). * Its presence is the gate; its value is stored, never branched on. */ exitCode: number | null; /** ISO timestamp of the tool_use line, when the host recorded one. */ ts: string | null; } export type TranscriptScan = { status: 'ok'; outcomes: Map; /** The byte cap was hit before the file start; older calls are absent. */ truncated: boolean; } | { status: 'unavailable'; reason: TranscriptUnavailableReason; }; export type TranscriptUnavailableReason = 'no-path' | 'missing' | 'unreadable' | 'unparseable'; /** * Scan the transcript tail and pair Bash calls with their outcome. * * `toolName` is a parameter so a future host that names the shell tool * differently degrades to "no outcomes" rather than to a wrong pairing. */ export declare function scanTranscriptTail(transcriptPath: string | null | undefined, limits?: Partial, toolName?: string): TranscriptScan; /** * Index outcomes by their command TEXT, for attaching to spool lines. * * The spool line carries no `tool_use_id` (adding one is a hook-template change * and a machine-wide reinstall), so the pairing is by exact command text. When * one text appears in the window with **conflicting** outcomes the entry is * `'ambiguous'` and nothing is attached — guessing which run a spool line * belongs to is exactly the kind of inference this file exists to avoid. * Identical outcomes for identical text collapse harmlessly. */ export type CommandOutcomeMap = Map; export declare function outcomesByCommand(scan: TranscriptScan): CommandOutcomeMap; /** * The exit status to record, or `null` when there is nothing evidenced to record. * * A failure always carries its status (one with no exit line never executed and * never becomes an outcome at all), so the `null` arm is unreachable through * `scanTranscriptTail`. It exists so that callers **skip** rather than * substitute a plausible-looking `1` if that ever stops holding — inventing a * status is precisely the fabrication this module is built to refuse (AD-6). */ export declare function outcomeExitCode(outcome: TranscriptOutcome): number | null; /** * Failures in transcript order, keyed by their own `tool_use_id`. * * The synthesis path uses THIS rather than the text-keyed map, and the * distinction is load-bearing. Attaching to a spool line must go by text, * because the spool line carries no id — so two runs of one text collapse, and * a conflicting pair has to be dropped. Synthesis has no spool line to match * against, so it can use the id the host already assigned: three genuine * failures of `npm test` in one window are three records, not one, and each is * written exactly once ever because its id is remembered. */ export declare function failedOutcomes(scan: TranscriptScan): TranscriptOutcome[]; /** * One compact line describing what the last scan actually saw. * * Recorded in meta and surfaced by `cortex doctor`. Without it this feature has * the shape of the outage it exists to fix: a host that renames * `transcript_path`, stops emitting `is_error`, or moves the file produces * silence indistinguishable from "nothing failed". `truncated` is reported here * rather than computed and discarded — a tail that dropped most of the file * must not look like a complete one. */ export declare function describeScan(scan: TranscriptScan): string; //# sourceMappingURL=transcript.d.ts.map