import type { CortexStore } from '../db/store.js'; import { computeGitListedCensus } from './census.js'; import { scanTranscriptTail, type TranscriptEvent } from './transcript.js'; /** * Batch-level command-outcome capture (FR-14, Story 4.4). * * **Why this is not in `hooks.ts` with its siblings.** Every `handle*Event` * there replays ONE spool entry. This runs once per flush, over a candidate set * that does not come from the spool at all: a command the host deemed failed * fires no `PostToolUse`, so it has no spool line, and the only place it exists * is the transcript. * * **Everything expensive happens OUTSIDE the write transaction.** Story 4.3 * learned this the hard way: N searches over one root ran N `git rev-parse` * spawns inside the write transaction, so one stalled git (network drive, * antivirus) held the write lock for N×5 s and starved every other writer. The * transcript scan, the census and the head lookup all run first; only the * upserts run in a transaction. * * ## What the review round changed, and why it had to * * Three independent review layers each reached a false `passed-at` — Cortex * reporting a pass about a tree the command never ran on, which the PRD names * as the worst failure this product can produce (SM-C3). Four routes, one root * cause: **the transcript was used only to FIND candidates, never to RULE THEM * OUT**, and nothing bounded how far back a candidate could come from. * * 1. *No window.* `scanTranscriptTail` returns every call in a 2 MiB tail — * dozens of turns. Certification consulted only the CURRENT spool batch, so * any later quiet turn (a read-only turn, or one with no tool calls at all) * re-certified every historical command and re-stamped it with the census * taken *now*. Closed by {@link COMMAND_CAPTURE_WATERMARK_PREFIX}: a command * is considered by exactly one flush, ever. * 2. *Invisible failed siblings.* `npm test` passes, then `npm install` FAILS in * the same turn and rewrites `package-lock.json`. The install fires no hook, * so the spool never saw it, so nothing disqualified the pass. Closed by * disqualifying on the TRANSCRIPT's own event list, which sees both. * 3. *Subtree census.* Fingerprinting only the command's own directory left * everything it imports from elsewhere unwatched. Closed by censusing the * scope root always — strictly wider, and width is the safe direction. * 4. *Empty fingerprint.* A directory with nothing git-listed produced the * sha256 of the empty string, which matches forever. Closed by refusing a * zero-entry census here and answering `unknown` for one at query time. */ export interface CommandCaptureDeps { census: typeof computeGitListedCensus; headOid: (dir: string) => string | null; scanTranscript: typeof scanTranscriptTail; } export declare const DEFAULT_COMMAND_CAPTURE_DEPS: CommandCaptureDeps; /** The subset of a spool entry this pass reads. Kept structural so the spool's * own type does not have to be imported across the layer. */ export interface CommandCaptureEntry { tool?: string; ts?: string; cmd?: string; file?: string; bg?: unknown; } export interface CommandCaptureInput { entries: CommandCaptureEntry[]; /** Entries that arrived after this batch was claimed. */ liveAnyCmd: boolean; liveEditedPaths: string[]; /** The flush could not establish ordering; refuse everything (Story 4.3). */ conservative: boolean; transcriptPath: string | null; /** Worktree root the flush is running for. */ dir: string; scopeKey: string; scopeRoot: string | null; /** * Whether this flush speaks for the capability's health (review round). * * Only the end-of-turn flush is given a transcript path. `inject-header` and * `cortex flush-spool` are not, so when they wrote the status they overwrote a * healthy `ok` with `no-transcript` — and `doctor` then reported a working * installation as broken, observed live. Any flush that cannot speak for the * capability stays silent about it instead. */ reportStatus?: boolean; } export type CommandCaptureReason = 'ok' | 'no-transcript' | 'transcript-missing' | 'transcript-unreadable' | 'transcript-unparseable' | 'initializing' | 'error' | 'conservative'; export interface CommandCaptureResult { /** Distinct records written — not candidates considered. */ recorded: number; /** Why nothing was recorded, when nothing was — for `doctor` and tests. */ reason: CommandCaptureReason; } /** * Meta key holding the last capture attempt's outcome, so `cortex doctor` can * report the capability's real state instead of its intended one (AC #8). * * AD-12 exists because this project keeps getting bitten by capability that is * wired, silent, and dead — the exit-code capture this story replaces sat * broken across 4,881 commands with nothing anywhere saying so. A feature that * cannot say why it did nothing is that bug waiting to happen again. */ export declare const COMMAND_CAPTURE_STATUS_KEY = "command_capture:last"; /** * Per-scope high-water mark: the newest command timestamp any flush has already * considered. A command past it is never a candidate again, so **each command is * judged by exactly one flush — the one covering the turn it ran in.** * * Advanced whether or not anything was recorded: a command the flush could not * certify has had its window, and reconsidering it later is precisely the stale * re-certification two review layers reproduced. * * Per scope, because a store serves several branches and worktrees whose * sessions have different transcripts. Two windows sharing one scope can * suppress each other's candidates; that direction costs a missed record, never * a wrong one. */ export declare const COMMAND_CAPTURE_WATERMARK_PREFIX = "command_capture:watermark:"; export interface CommandCaptureStatus { reason: CommandCaptureReason; recorded: number; at: string; } export declare function readCommandCaptureStatus(store: CortexStore): CommandCaptureStatus | null; /** * Split from the store read because `doctor` opens the database READ-ONLY and * never constructs a `CortexStore` — the rule that keeps a diagnostic from * repairing what it is diagnosing. */ export declare function parseCommandCaptureStatus(raw: string | undefined | null): CommandCaptureStatus | null; /** * Second-resolution UTC, because the two clocks being compared do not agree on * precision: the capture hook stamps `date -u '+%Y-%m-%dT%H:%M:%SZ'` (whole * seconds) while the transcript carries milliseconds. Comparing the raw strings * lexically would make `…:42.123Z` sort AFTER `…:42Z` and silently break every * ordering test. Returns null for anything unparseable — which disqualifies, * never passes. */ export declare function toSecondStamp(ts: string | undefined | null): string | null; /** * AC #4 for one candidate: nothing observable may have happened at-or-after the * moment this command STARTED. * * Start, not finish, deliberately: an edit landing while a suite runs means the * tree the suite read is not the tree the flush is about to fingerprint. * * Two evidence sources, because neither alone is complete. The spool sees * edits, writes and the tools the hook matches. The transcript sees every tool * call including the ones that FAILED — which the spool structurally cannot, * and which is where the reproduced false pass lived. */ export declare function isCommandCertifiable(candidate: { rawCommand: string; startedAt: string; toolUseId?: string; }, input: CommandCaptureInput, transcriptEvents?: readonly TranscriptEvent[]): boolean; /** * Record every command in the flushed window whose outcome is proven and whose * window is clean. Returns how many DISTINCT records were written, and why none * were if none. * * Never throws — and this time the contract is enforced rather than asserted: * the inner pass is wrapped, because a `SQLITE_BUSY` from a concurrent flush * used to escape and take the status write with it, leaving `doctor` reporting * a stale reason forever (review round, reproduced). */ export declare function captureCommandOutcomes(store: CortexStore, input: CommandCaptureInput, deps?: CommandCaptureDeps): CommandCaptureResult; /** Resolve a stored scope-relative directory the way the query does, so capture * and query cannot disagree about which tree a record describes. Exported for * the ledger and pinned by test. */ export declare function resolveRecordDir(dir: string, scopeRoot: string | null, fallback: string): string; //# sourceMappingURL=command-capture.d.ts.map