/** * feature-adr durable checkpoints — the PURE half (backlog 49e4a95b). * * Problem: .claude/workflows/feature-adr.js restarts an L/XL run from scratch when its session dies * (the exact failure that forced usage-adaptive routing — one run cost 623k subagent tokens), and the * STANDARD L/XL two-phase flow (stop-after-plan → re-invoke) re-runs router+design+plan wholesale. * The Workflow harness's own resumeFromRunId is same-session only, so it cannot cover either case. * * Design: the heavyweight state is ALREADY durable — the 00–09 artifacts in features//. The * checkpoint layer is deliberately THIN: after each expensive stage the workflow appends one JSONL * line { stage, inputHash, result } to features//.fa-state/checkpoints.jsonl (via a cheap * effort-low agent — the workflow sandbox has no fs). On the next run with the same slug, a stage is * SKIPPED only when its recorded inputHash matches the freshly computed one AND its expected artifact * is still on disk. Granularity is per-STAGE, not per-agent-call: a death mid-Step-7 re-runs Step 7, * never Steps 0–6. * * Provenance: concept (checkpoint keyed by input hash + call cache) from ADR-157 * darwin-checkpoints-durable-execution (status PROPOSED) in agent-harness-generator. Its "~39% resume * saving" figure is from a SYNTHETIC deterministic simulation — deliberately NOT quoted as expected * field saving anywhere in this feature. * * Everything here is pure and deterministic (no Date/random — the workflow sandbox forbids them); * the workflow script mirrors these functions inline (it is self-contained and cannot import), and * the wiring test asserts the mirror stays present. */ /** Blob version stamp read by scripts/gen-loop-blobs.mjs (feature loop-designer, ADR-004) — the * ONLY loop-designer change to this canonical file; bump when any blob-exported semantic changes. */ export declare const BLOB_VERSION = "1.1.0"; /** Stages the workflow checkpoints, in pipeline order. Cheap side-channel agents (usage probes, * fa-record, auto-cost selects) are never checkpointed; the opt-in Delivery gate re-runs by design * (advisory verdicts should reflect the CURRENT tree). */ export declare const CHECKPOINT_STAGES: readonly ["router", "design", "plan", "code", "qe", "fleet"]; export type CheckpointStage = (typeof CHECKPOINT_STAGES)[number]; /** The artifact(s) (relative to features//) whose PRESENCE a resume additionally requires in * 'auto' mode — EVERY listed path must exist. null = result-only stage (hash match suffices). * Tier-dependent stages (design) take extra artifacts at the call site via `extraArtifacts` — * an M+ design must probe its ADR/ideation/architecture files too, not just requirements * (Codex QE #2: a one-file probe accepted a materially incomplete design). */ export declare const STAGE_ARTIFACTS: Record; /** A checkpoint line as persisted (one JSON object per line). */ export interface CheckpointEntry { stage: string; inputHash: string; result: unknown; } /** Oversize guard: a result JSON above this is NOT checkpointed (the stage simply re-runs on resume). * Keeps the read-back prompt bounded; artifacts on disk carry the heavy state anyway. */ export declare const CHECKPOINT_MAX_RESULT_CHARS = 12000; /** Checkpoint format/logic version — SALTED into every input hash. Bump it whenever the workflow's * stage semantics, prompts, or composite result shapes change: every pre-existing checkpoint then * hashes stale and re-runs, instead of an old-format entry resuming into new logic (Codex QE #5). */ /** * Bumped 'fa-ckpt-2' → 'fa-ckpt-3' on 2026-08-20: the `design` entry changed meaning. It used to be * the ONE record for the whole parallel design fan; it is now a completeness marker sitting above * four per-sibling records. A stale-format entry must hash stale rather than resume into new logic, * so every in-flight `.fa-state/` re-runs router+design+plan once. That is the intended one-time * price of the change, not an accident. */ export declare const CKPT_SCHEMA_VERSION = "fa-ckpt-3"; /** * Router-stage hash token. Step 0's CONTRACT changed on 2026-08-21 — it must now WRITE * `00_complexity_assessment.md` with an acid table — and a contract change that leaves the hash * alone lets a pre-change router entry resume: the resume gate only asks whether the artifact is * PRESENT, and one of the 66 features that already had a (tableless) file satisfies it. Step 0 then * never re-runs, the acid table is never written, and C4 goes on skipping — the exact defect this * change exists to close, resurrected through the resume path. * * Scoped like `LANDING_HASH_TOKEN` (003-10) rather than bumping CKPT_SCHEMA_VERSION: only the router * stage re-runs once, while design/plan/code/qe checkpoints stay valid. A global bump would re-spend * every in-flight stage to fix one. */ export declare const ROUTER_CONTRACT_TOKEN = "router-writes-00-v1"; /** FNV-1a 32-bit over UTF-16 code units, hex-encoded (one pass; building block for the 64-bit form). */ export declare function fnv1a(str: string): string; /** 64 bits from two independent FNV-1a passes (plain + salted). A single 32-bit hash admits * findable collisions (Codex QE #9 produced a real pair at `11a08b58`); two passes make the * single-pair collision odds ~2^-64 — adequate for one slug's checkpoint file. */ export declare function fnv1a64(str: string): string; /** The stage's input fingerprint: a JSON-tuple (delimiter-ambiguity class — never a separator join) * of the schema version + stage name + every input that would change the stage's output, hashed. * Upstream stage RESULTS are included as their serialized form, so a stale upstream auto-invalidates * downstream. HONEST SCOPE: the hash proves the run INPUTS are unchanged — it does NOT fingerprint * the working tree (a crash-resume legitimately sees the dead run's uncommitted writes, so a tree * hash would invalidate every real resume). Tree-level staleness is out of the checkpoint contract: * use resume:'never' (or delete .fa-state/) after manual edits, and re-QE independently — the ADR * names this as the accepted limitation (Codex QE #1). */ export declare function checkpointInputHash(stage: string, parts: readonly unknown[]): string; export type ResumeMode = 'auto' | 'never' | 'force'; /** Normalize args.resume: anything but the two explicit strings means the default 'auto'. */ export declare function resumeMode(raw: unknown): ResumeMode; export interface ResumeDecision { resume: boolean; reason: 'resumed' | 'resumed-force' | 'mode-never' | 'no-checkpoint' | 'stale-input' | 'artifact-missing'; } /** The pure resume decision. 'auto' resumes only on (hash match AND every required artifact * present); 'force' trusts the hash alone; 'never' always runs live. A STALE-INPUT hash NEVER * resumes in any mode — force skips only the artifact probe, never the input check (a checkpoint * for different inputs is a different feature). A malformed/null recorded result is treated as * no-checkpoint (Codex QE #8 — a null result must not resume as a real one). */ /** The tier-active design sub-stages, in fan order. Namespaced keys are plain strings, so the * checkpoint parser's last-wins-per-stage rule and its malformed-line erase rule work unchanged. */ export declare const DESIGN_SUBSTAGES: readonly ["requirements", "adr", "qcsd", "architecture"]; export type DesignSubstage = (typeof DESIGN_SUBSTAGES)[number]; /** `design:requirements`, … — the checkpoint stage key for one sibling. */ export declare function designStageKey(sub: DesignSubstage | string): string; export interface DesignFanVerdict { complete: boolean; missingSubstages: string[]; missingArtifacts: string[]; /** Why the fan is not complete — 'ok' when it is. Never collapse 'probe-not-established' into * 'ok': a gate that could not read its inputs is INCONCLUSIVE, and inconclusive is not a pass. */ reason: 'ok' | 'substage-missing' | 'artifact-missing' | 'probe-not-established'; } /** * SP-1 (completeness). The `design` stage may hand a value to Step 6 ONLY when every tier-required * sibling produced a result AND every tier-required design artifact is on disk. * * This is the property the old all-or-nothing WRITE gate was implementing by accident, and the * reason it must survive the split: a one-file probe once accepted a design missing its ADR and * architecture (Codex QE #2). Splitting the write gate without keeping this would reopen that hole, * so the two are landed together and tested together. * * It is deliberately a READ-side predicate. What may be WRITTEN (one sibling's completed work) and * what may be CONSUMED (a whole design) are different questions; the previous code answered the * second by crippling the first, which is how one dead agent discarded three finished artifacts. */ export declare function decideDesignFanResume(opts: { /** * The LIVE result of each required sibling, in `required` order — what the fan just returned. * * Round 1 of cross-family review graded this D and was right: the first version read the * START-OF-RUN checkpoint snapshot instead. That snapshot is wrong in both directions. A sibling * that succeeded THIS run is absent from it, and — the dangerous half — a stale non-null entry * from a PREVIOUS run stays in it even when this run's retry returned null, so an incomplete * design could be declared complete. That is the exact hole the old all-or-nothing write gate * existed to prevent, reopened by reading the wrong source. * * The live result is authoritative and free: a sibling that returns non-null wrote its artifact, * and a sibling that died returns null. Nothing needs to be re-probed to know that. */ results: readonly unknown[]; required: readonly string[]; /** Every artifact the tier requires from this fan. Empty ⇒ the artifact half is not checked. */ artifacts: readonly string[]; /** * A listing taken AFTER the fan ran — never the start-of-run one, which is why the parameter is * named for that and not just `listing`. Rounds 2 and 3 pinned both halves of this: * • round 2 — fed the START-OF-RUN listing, the probe called every fresh M+ fan incomplete, * because a listing taken before the fan cannot contain what the fan is about to write; * • round 3 — with the probe removed entirely, a sibling that returns non-null having written * only its primary artifact (L-tier requirements writes 01_requirements.md and skips * 02_research.md) was accepted, and Step 6 planned with no research behind it. A non-null * result is the agent's own word; it is not evidence that a file exists. * `null` means the probe could not be read. That is INCONCLUSIVE, not clean. */ postRunListing: ReadonlySet | null; }): DesignFanVerdict; export declare function decideCheckpointResume(opts: { mode: ResumeMode; entry: CheckpointEntry | undefined; inputHash: string; artifactRel: string | readonly string[] | null; listing: ReadonlySet; }): ResumeDecision; /** Serialize one checkpoint line, or null when the result is null/oversize/unserializable — * the caller logs the skip loudly; a missing checkpoint only costs a re-run, never corrupts. * A null result is never persisted (Codex QE #8: it would later parse as a resumable entry). */ export declare function serializeCheckpoint(stage: string, inputHash: string, result: unknown): string | null; export interface ParsedCheckpointRead { entries: Record; listing: Set; malformedLines: number; } /** Sentinel separating the checkpoint file body from the artifact listing in the single read-back * command's stdout. */ export declare const CHECKPOINT_LS_SENTINEL = "---FA-CKPT-LS---"; /** Parse the read-back agent's stdout: JSONL entries (LAST occurrence of a stage wins — a re-run * overwrites by append), then the sentinel ON ITS OWN LINE, then one artifact path per line * (relative to the feature dir). The sentinel match is LINE-ANCHORED: JSON.stringify never emits * literal newlines, so a sentinel string INSIDE a recorded result shares its line with JSON syntax * and can never split the stream (Codex QE #10). Malformed JSONL lines are COUNTED, never silently * ignored (corruption is named); entries with a null result are malformed, not resumable. */ export declare function parseCheckpointRead(text: string): ParsedCheckpointRead; /** Single-quote shell escaping (the workflow's shq twin). */ export declare function shellQuote(s: string): string; /** The one Bash command the read-back agent runs: checkpoint file body (absent file = empty), * the sentinel, then the artifact listing as feature-dir-relative paths (find prints them with a * leading ./ that sed strips). Never fails: every leg is || true. */ export declare function checkpointReadCmd(fdirAbs: string): string; /** The one Bash command the write agent runs: mkdir the state dir, then append ONE line. The line * is single-quote-escaped as a whole — JSON.stringify output never contains literal newlines, so * printf '%s\n' emits exactly one record. */ export declare function checkpointAppendCmd(fdirAbs: string, line: string): string; export type CaptureMode = 'capture' | 'backfill' | 'skip-disabled' | 'skip-empty'; /** Decide whether this completion is captured. A resumed stage is backfilled rather than * skipped: its input (stage template + args) and checkpointed output are both in scope at the * capture site, so the pair is deterministically reconstructible. trainingPairBackfillCmd's * persistent atomic mark makes that write at-most-once, so concurrent invocations and later * runIds cannot double-append the same pair. */ export declare function decideCaptureMode(opts: { enabled: boolean; resumed: boolean; recordCount: number; }): CaptureMode; export type CaptureFailureReason = 'threw' | 'unserializable' | 'unverified' | 'backfill-unverified' | 'empty-output'; export interface CaptureFailureRecord { stage: string; mode: CaptureMode | null; reason: CaptureFailureReason; detail: string | null; } /** Normalize capture failures for collection by the caller. This recorder must never throw: * replacing the original capture failure with a reporting failure would hide the real cause. */ export declare function captureFailureRecord(stage: unknown, mode: unknown, reason: unknown, detail: unknown): CaptureFailureRecord; /** Training-pair record format version. Bump on any field-shape change. */ export declare const TRAINPAIR_SCHEMA_VERSION = "fa-trainpair-2"; /** Oversize guard cap over input+output combined (same posture as * CHECKPOINT_MAX_RESULT_CHARS, sized for full stage prompts): an over-cap pair is * TRUNCATED with a named marker + a hash of the full text — never silently dropped * (a lost pair is a lost training sample), never unbounded (a 10MB line would make * the JSONL unusable and the write-agent prompt explode). */ export declare const TRAINPAIR_MAX_IO_CHARS = 48000; export type TrainingPairFamily = 'claude' | 'codex'; /** The family a model spec/label/runner-name belongs to. Family ∈ {claude, codex} — * the field the cross-model dataset rule stands on. Anything naming codex/gpt/openai * is 'codex' (incl. 'codex-fallback' — codex ACTUALLY produced that stage); everything * else (opus/sonnet/fable/haiku, role agentTypes, 'claude-fallback') is 'claude'. */ export declare function trainingPairFamily(spec: unknown): TrainingPairFamily; export interface TrainingPairEvaluation { /** The QE grade for this pair, or null when the stage honestly has none (router). */ grade: string | null; /** Who graded it (runner + model label), or null when ungraded. */ gradedBy: string | null; /** Lesson texts/ids recalled into THIS stage's context (Step-0 recall). */ lessonsInjected: string[]; } export interface TrainingPairProvenance { /** The model label that produced the stage output. */ model: string; /** The model FAMILY — load-bearing for the cross-model dataset rule. */ family: TrainingPairFamily; /** The stage role: 'router' | 'design:*' | 'planner' | 'coder' | 'reviewer' | 'fleet-qe'. */ role: string; tokens: number | null; minutes: number | null; } export interface TrainingPairTruncation { /** Original (pre-truncation) char counts + full-text hashes — what was cut is NAMED. */ inputChars: number; outputChars: number; inputHash: string; outputHash: string; } /** One SFT-ready record: prompt → completion → evaluation, one JSON object per line. */ export interface TrainingPair { schema: string; slug: string; stage: string; /** ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint. */ ts: number | string | null; input: string; output: string; evaluation: TrainingPairEvaluation; provenance: TrainingPairProvenance; truncated: TrainingPairTruncation | null; captureMode: 'capture' | 'backfill'; resumed: boolean; } /** Per-stage JSONL path, relative to the repo root. ONE file per stage. */ export declare function trainingPairPath(slug: string, stage: string): string; /** README dropped once into the capture dir. The caveat is documented ON DISK because the * directory is deliberately not gitignored (explicit owner decision, 2026-08). */ export declare const TRAINPAIR_PRIVACY_NOTE = "feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time \u2014 the original stage's timing lives in that run's .fa-state checkpoint."; /** Assemble one SFT-ready training pair. Deterministic (ts passed in). Applies the oversize * guard: when input+output exceed TRAINPAIR_MAX_IO_CHARS combined, each over-budget side is * truncated with a marker naming the cut char count + the fnv1a64 of its FULL text (the * budget flows to the smaller side, so a small prompt next to a huge output stays verbatim). * Evaluation honesty: an empty/whitespace grade normalizes to null — a stub never reads as * a real evaluation. Provenance: an explicit valid family wins; otherwise it is DERIVED from * the model spec via modelFamily (an invalid family never leaks into the dataset). */ export declare function buildTrainingPair(opts: { slug: string; stage: string; ts: number | string | null; input: unknown; output: unknown; evaluation?: Partial | null; provenance?: Partial | null; captureMode?: unknown; resumed?: unknown; }): TrainingPair; /** Serialize one training pair to a JSONL line. The oversize guard already bounds the pair, * so this only fails on the impossible (all fields are plain data) — null on that, never a throw. */ export declare function serializeTrainingPair(pair: TrainingPair): string | null; /** The one Bash command the write agent runs: mkdir the slug dir, drop the privacy README * once (if-absent guard), then append ONE line (single-quote-escaped byte-faithfully, same * idiom as checkpointAppendCmd). */ export declare function trainingPairAppendCmd(repoAbs: string, slug: string, stage: string, line: string): string; /** Readback sentinels for the caller to distinguish an at-most-once write from an existing pair. */ export declare const TP_BACKFILL_OK = "TP-BACKFILL-OK"; export declare const TP_BACKFILL_SKIP = "TP-BACKFILL-SKIP"; export declare const TP_BACKFILL_DUP = "TP-BACKFILL-DUP"; /** Build the deterministic resume-backfill command. The persistent mkdir mark is the atomic * at-most-once primitive; the inner file-absence guard also protects pair files created before * marks existed. The mark is RELEASED when — and only when — the append fails, so a failed backfill * stays retryable. The `[ -f ]` path keeps the mark because the pair genuinely exists. * KNOWN RESIDUAL: a process killed (SIGKILL, sandbox timeout) between the `mkdir` claim and the end * of the append still leaves a poisoned mark. That window is strictly narrower than "any append * failure" and is the same externally-killed class this feature already names for the ledger row. * `TP_BACKFILL_SKIP` means "the per-stage pair file already existed"; `TP_BACKFILL_DUP` means * "another run already owns this content". The two strings are deliberately NON-PREFIXING because * the two producers parse the readback differently — the generated loop compares `===` after * `trim`, while the `feature-adr.js` twin tests an UNANCHORED regex; a prefixed name would be `DUP` * to one parser and `SKIP` to the other from the same bytes. The default `markKey` is per-CONTENT * only; a caller whose line embeds a per-run identifier must pass a run-independent `markKey`. * Marks are deliberately never pruned. */ export declare function trainingPairBackfillCmd(repoAbs: string, slug: string, stage: string, lines: readonly string[], markKey?: string): string | null; /** * May the code stage's result be checkpointed? * * Pre-epoch this was a DENYLIST inlined in the workflow — `!/genuinely-not-landed/.test(landedNote)` * — which is fail-OPEN by construction: every state that is not that one string persists, including * a dead probe. MEASURED pre-fix: `node -e "console.log(!/genuinely-not-landed/.test('(landed-probe * failed)'))"` prints `true`, i.e. a run whose barrier never answered was checkpointed as landed. * * The replacement is an ALLOWLIST with exactly two admitted states, and `barrierRequired` is what * makes it non-forgeable: a codex run cannot LABEL itself `'synchronous'` past the gate, and a * Claude run cannot claim a barrier verdict it never ran. `barrierRequired` arrives as a BOOLEAN * (`needsCodeLandedBarrier(coderUsed)` at the call site) purely to avoid a routing↔checkpoints * module cycle — H6. */ export declare function codeCheckpointPersistAllowed(landingStatus: unknown, barrierRequired: boolean): boolean; /** * Composite-shape validity for a code-stage checkpoint entry (consumer #3): the pre-existing shape * checks PLUS the landing fields. An entry written before this protocol carries no `landingStatus` * and no `landingProtocol`, so it reads as NO CHECKPOINT and the stage re-runs — the belt to R6's * hash-token invalidation, in case a hash somehow matches. */ export declare function codeStageResultShapeValid(r: unknown): boolean; /** * Parse the design artifact probe's stdout into "which required artifacts exist". * * The probe is relayed by a MODEL, not read from a pipe: the workflow sandbox cannot run a shell, so * an agent runs the command and hands back what it saw. That makes the transcript forgeable in * principle, and cross-family review round 7 showed it forgeable in PRACTICE by accident — an agent * that narrates ("Expected output when present: HAVE:01_requirements.md … Actual stdout: …") emits a * line identical to the real token, and a permissive parser took it for evidence. The gate then passed a * design whose artifact did not exist, which is the one thing this gate exists to prevent. * * So the transcript is validated STRICTLY, not scanned: after trimming blank lines, the output must be * exactly some subset of the known HAVE tokens followed by exactly one sentinel, and nothing else. * Anything unexpected — narration, a second sentinel, a shell error, a code fence — makes the probe * NOT ESTABLISHED (null), which is a refusal, never a pass. * * What this does NOT do, named plainly: it cannot stop an agent that deliberately emits precisely the * expected transcript and nothing else. That residual is the same trust the whole pipeline places in * a relaying agent (the checkpoint reader and the Step-7.5 landing barrier share it). What it does do * is make ACCIDENTAL forgery — the kind that actually happened — impossible rather than likely. * * @returns a Set of the required artifacts that exist, or `null` when the transcript is not trustworthy. */ export declare function parseArtifactProbe(opts: { stdout: string | null | undefined; sentinel: string; required: readonly string[]; }): Set | null; /** Why a checkpoint write was refused, or `ok` when it may proceed. */ export type CheckpointWriteVerdict = { readonly ok: true; readonly line: string; readonly witnessed: readonly string[]; } | { readonly ok: false; readonly reason: string; }; /** * Decide whether a stage may be recorded — the WITNESS half of `dz feature-adr checkpoint`. * * Why this exists (2026-08-21). The workflow script runs sandboxed with no filesystem, so it * delegated checkpoint writes to a subagent by handing it a FINISHED JSON line and saying "append * this". The subagent was a courier: it verified nothing. Read from outside, that shape is one party * instructing another to declare a verification gate complete — which is what a safety classifier saw, * blocking NINE such writes in one run (router, four design substages, plan, code, qe, and the cost * ledger). MEASURED: `.fa-state/checkpoints.jsonl` was never created, while every stage had in fact * run and left its artifact on disk. So resume was silently dead and the run still reported success. * * The classifier's premise was wrong for those writes, but its instinct was not: nothing in the old * mechanism could tell a real completion from a fabricated one. A stage whose artifact does not exist * could be recorded as complete, and yesterday's cross-family reviewer flagged exactly that for the * `fleet` stage. So the fix is not a better-worded prompt — it is to stop hand-writing state at all. * The subagent now RUNS A COMMAND; this function is the check that command performs first. * * @param artifacts repo-relative paths the stage must have produced. EMPTY IS REFUSED: a stage that * claims nothing verifiable has nothing to witness, and recording it would restore the very * hole this replaces. * @param present the subset of `artifacts` the caller MEASURED on disk (never what it planned). */ export declare function decideCheckpointWrite(opts: { stage: string; inputHash: string; result: unknown; artifacts: readonly string[]; present: readonly string[]; }): CheckpointWriteVerdict; //# sourceMappingURL=feature-adr-checkpoints.d.ts.map