import type { Exposure, Skill, StepAttestDecl } from "./skill.js";
import { type Observation, type ObservationRef } from "./assert.js";
import { builtinTools, type Tool, type ToolContext } from "./tools.js";
export { builtinTools };
import type { LlmClient } from "./llm.js";
import type { PolicyClaim } from "./policy.js";
export type StepOutcome = "passed" | "failed" | "unchecked" | "skipped";
export interface StepWhy {
    /** Why this step diverged — the load-bearing failure, verbatim from the real assertion/tool result. Present on a failed step. Never fabricated. */
    trigger?: string;
    /** What an escalation changed to heal this step — from the real L1/L2 patch reason. Present on a healed step. */
    change?: string;
}
/**
 * The write receipt: present iff a write-effect (idempotent-write/destructive)
 * step actually dispatched its tool call — never for a refused/mocked step.
 * Spec: docs/specs/flight-recorder-v2.md §2.
 */
export interface StepWrite {
    /** computeIdempotencyKey(tool, tool.server ?? null, filledArgs) — per-run identity of this exact write. */
    idempotencyKey: string;
    /** Opaque identity of this individual dispatch, including repeated identical calls. */
    dispatchId?: string;
    /** true = executed via a matching Step.approve hash; false = executed via the legacy --allow-writes/--yes flags. */
    approved: boolean;
    /**
     * WHICH approval authorized this write — the step's stamped `approve:`
     * hash, verified equal to the recomputed hash before dispatch (the same
     * equality `approved` is read off). Absent exactly when `approved` is
     * false: a `--allow-writes`/`--yes` dispatch has no authorization to name.
     *
     * FOUNDATION's ten-year asset #1 is "what an agent declared it would
     * change (scope, approval hash) joined to what actually changed" — this is
     * the join key. `approved` alone says only THAT a write was authorized,
     * never by which authorization, so pairs could not be grouped after the
     * fact. Additive (I-11).
     *
     * Disclosure, stated precisely (review finding — the first version of this
     * comment justified it with "the hash is already in the committed skill
     * file", which is a non-sequitur: a receipt is publishable and the skill
     * file may be private). This is an UNSALTED sha256 over the operation
     * shape, so it is a stable correlator across runs and across tenants —
     * that is the point — and any third party holding a candidate skill file
     * can recompute it, making it a confirmation oracle for "did this receipt
     * run THIS operation". The real argument is that it adds no new exposure
     * CLASS: `idempotencyKey`, already in this same block, is an unsalted hash
     * over the FILLED args and is strictly more revealing. This is the
     * OPPOSITE property from `attest`'s salted commitments, whose design makes
     * cross-run joins impossible — do not generalize that guarantee here.
     */
    approvalHash?: string;
    /** Best-effort, honestly-labeled extraction from the tool's JSON response body — absent when nothing was found. */
    resource?: {
        id?: string;
        version?: string;
    };
    /** Set when an earlier step in THIS run wrote with the identical idempotencyKey — the step number of that earlier step. */
    duplicateOf?: number;
    /**
     * The instant the write dispatch was issued (state-conditioned approval
     * §2.3, additive): with `stateCheck.observedAt`, every checked receipt
     * carries its own measured probe→dispatch window. Present ONLY on
     * expect-bearing steps — a skill with no `expect:` produces byte-identical
     * records (invariant I-2).
     */
    dispatchedAt?: string;
}
/**
 * The execute-time state check of a state-conditioned approval (wave2 spec
 * §2.3, additive-optional on the wire — the pinned wire-contract fixture is
 * untouched, I-11). Present iff the step carried `expect:` AND the runner
 * reached the check; a step refused earlier (approval mismatch, unknown
 * tool, write gate) carries none. Outcomes and timestamps only — NO keyed
 * MAC values ever land in a record (I-7). Three-valued honesty is
 * load-bearing: `unevaluated` is its own state, never a pass (never-list
 * #1), never a recorder block (never-list #5).
 */
export interface StepStateCheck {
    outcome: "match" | "mismatch" | "unevaluated";
    /** proceeded = match/unevaluated, write dispatched; stamped = mismatch recorded, write still dispatched (recorder mode); refused = gate mode (S8, §5.5): the repo's policy.yml opted into fail-closed and the write was refused BEFORE dispatch — no write block, no attest. */
    action: "proceeded" | "stamped" | "refused";
    /** expect.at — when the approved observation was made. Informational: time is never an input to the comparison (I-14). */
    expectedAt: string;
    /** When the execute-time observation resolved; absent iff unevaluated before any observation. */
    observedAt?: string;
    /**
     * Present iff outcome === "unevaluated" — a CLOSED registry (§8.6). All
     * eight values, in the order they are decided:
     *   approval-expired          (W5-T3 — the TTL elapsed; no probe dispatched)
     *   probe-args-mismatch       (W3-S4 — filled probe args are not the approved ones; no probe dispatched)
     *   key-unavailable           (deletion IS revocation; indistinguishable from never-present)
     *   probe-timeout / probe-failed / probe-tool-unknown
     *   probe-substrate-mismatch  (W3-S5 — status.code through a wrapped MCP tool)
     *   empty-projection
     * This comment listed five for two slices while the code emitted more;
     * it is normative documentation, so it is kept COMPLETE, not appended to.
     * Adding a value requires a spec amendment (SPEC.md §4.1).
     */
    reason?: string;
    /**
     * Present only when outcome === "mismatch" and declared projection fields
     * were absent at execute (B5/A9): names only (`delta.fields` precedent,
     * A1 wording — never an approve-time presence claim), capped.
     */
    absentFields?: string[];
    /**
     * P1.5 (wave2 §3.5): present only when outcome === "mismatch" AND the
     * binding carried per-field commitments — the declared fields whose
     * recomputed field MAC differs from the approve-time one. Unlike
     * absentFields, this IS an approve-time claim, and an earned one: under
     * the held key, per-field MAC inequality proves the committed value
     * differs. Names only, capped like absentFields.
     */
    changedFields?: string[];
}
export interface AttestState {
    hash: string;
    at: string;
}
export interface StepAttest {
    method: "response-derived" | "declared-probe";
    /** The probe TOOL NAME only (e.g. "github.get_comment") — identifies which tool observed
     * the state. Never the args template: a record is a publishable artifact and the record
     * format carries no other tool args. */
    selector?: string;
    pre?: AttestState;
    post?: AttestState;
    delta?: {
        changed: number;
        fields?: string[];
    };
    confidence: "exact" | "partial" | "pending" | "absent";
    /**
     * Deferred probe (§8): the absolute instant, resolved against DISPATCH,
     * by which the provider record was expected to appear. Present iff
     * `confidence` is `"pending"`. A consumer MUST NOT read a pending
     * attestation as a pass (never-list #1) — it is a state, not a result.
     */
    deferredUntil?: string;
    reason?: string;
}
/**
 * Pre-dispatch artifact attestation (docs/specs/artifact-attestation-v1.md
 * §6) — the commitment to what LEFT, for the class of write with no
 * post-state a probe could read back.
 *
 * Present iff the step declared `emit:` AND the call actually dispatched. A
 * refused, skipped or mocked step never carries one: a commitment on a call
 * that never went out would assert an emission that did not happen.
 */
export interface StepEmitRecord {
    /**
     * UNSALTED `sha256` over the type-tagged projection of the FILLED action
     * args (§5.1). Deliberately recomputable by a third party holding the
     * artifact — that is its purpose, as the join key of an
     * emission-to-authorization pair. It adds no new exposure CLASS only
     * because `idempotencyKey` in the `write` block beside it already hashes
     * ALL the filled args, which is strictly more revealing. That argument does
     * not generalize; it holds because that field is already there.
     */
    artifactDigest: string;
    /** The DECLARED coverage list, verbatim and in declared order (RFC 9421 §2.3's ordered covered-components idea). */
    projection: string[];
    /** The declared entries the filled args actually carried. `resolved` and `unresolved` always partition `projection` exactly. */
    resolved: string[];
    /**
     * Declared entries the filled args did not carry, or carried non-scalar —
     * names only. Omitted when empty, NEVER `[]`. This is what keeps a
     * server-side-rendered payload, a reference-valued field, or a two-call
     * draft/send composition visible instead of publishing a digest that
     * quietly covers less than the step declares (§3.1, §5.3).
     */
    unresolved?: string[];
    /**
     * WHICH approval authorized this emission — absent exactly when
     * `write.approved` is `false`, because a flag dispatch has no
     * authorization to point at. Duplicated from `write.approvalHash` so the
     * emission claim is readable without joining across blocks.
     */
    approvalHash?: string;
    /** When the commitment was computed — after the args were filled, before the call was dispatched. */
    at: string;
}
/** Immutable join from a deferred-resolution record back to the dispatch it answers. */
export interface StepResolutionOf {
    approvalHash: string;
    artifactDigest: string;
    deferredUntil: string;
    dispatchId: string;
}
export interface StepRecord {
    n: number;
    title: string;
    /** 0 = ran deterministically (or wasn't attempted); 1/2 = healed at that escalation level. */
    level: 0 | 1 | 2;
    outcome: StepOutcome;
    ms: number;
    failures: string[];
    /**
     * The step's declared `exposure` (SPEC §3.7), copied verbatim from the
     * skill — whether an actor OUTSIDE the system may already have acted on
     * this step's result. Absent exactly when the step declared nothing (which
     * reads as `internal`), so a skill that does not use the key produces a
     * byte-identical record. Orthogonal to the step's `effect`, and gating-inert
     * in this version: a consumer MUST NOT infer that an `external-visible` step
     * was blocked, reviewed, or approved differently.
     */
    exposure?: Exposure;
    /**
     * LLM token usage summed across every escalation attempt on this step
     * (incl. failed ones) — 0 attempts means this is absent, not zero.
     * `model` (added for the $ meter, src/cost.ts) is the model of the
     * HIGHEST escalation level actually invoked on this step (L2's model if
     * L2 ran, else L1's) — a known simplification: if a step tried L1 then
     * L2 with two different models, the summed tokens above are priced
     * entirely at the L2 rate by the cost meter. Rare in practice (most
     * steps resolve at whichever level they first reach) and never silently
     * wrong — just less precise than per-attempt model tracking would be.
     */
    llm?: {
        inputTokens: number;
        outputTokens: number;
        model?: string;
    };
    /**
     * Highest escalation ladder level TRIED for this step — present whenever
     * escalation ran at all (success or failure), absent when it never ran
     * (either the step didn't diverge, or maxLevel was 0). Distinct from
     * `level`, which records only the level that HEALED it (0 if it never
     * healed, even after an escalation attempt).
     */
    escalationAttempted?: 0 | 1 | 2;
    /** Present only when this step drifted (trigger) or healed (change); absent for an unchanged step. Never fabricated — see docs/specs/receipt-why.md. */
    why?: StepWhy;
    /** Present iff this step's tool actually dispatched a write-effect call — see StepWrite. */
    write?: StepWrite;
    /** Pre-dispatch artifact attestation — what LEFT, for writes with no probe-able post-state. See StepEmitRecord. */
    emit?: StepEmitRecord;
    /** Present only on a deferred-resolution record; carries both normative cross-record join keys. */
    resolutionOf?: StepResolutionOf;
    /** State attestation (consequence-layer §1) — present iff a write-effect step actually dispatched. Hashes over a field projection, never raw values. absent/pending are never a pass. */
    attest?: StepAttest;
    /** The execute-time state check (state-conditioned approval) — present iff the step carried `expect:` and the runner reached the check. See StepStateCheck. */
    stateCheck?: StepStateCheck;
    /**
     * Provider-issued request-id refs captured from this step's Observation
     * (trust-ladder spec §3) — extends the write receipt's honesty discipline
     * to EVERY executed step, not just writes: a read step's response can
     * carry a cross-checkable reference too. Omitted when the tool call
     * captured none (allowlist-only; never fabricated). Absent for a mocked
     * step (`--fail N`) — no real dispatch happened, nothing to capture.
     */
    refs?: ObservationRef[];
    /**
     * Set (true) iff this step's observation was a synthetic injected failure
     * (`--fail N[=status]`, docs/specs/flight-recorder-v2.md §3) rather than a
     * real tool dispatch. A mocked step never gets a `write` block — no tool
     * call happened, so there's nothing to receipt.
     */
    mocked?: true;
}
export interface RunRecord {
    skill: string;
    startedAt: string;
    finishedAt: string;
    passed: boolean;
    /**
     * Set only on the second record emitted by `reelier resolve`. Such a record
     * observes no action and MUST NOT be presented as a passing or failing run;
     * its per-step attestation confidence is the result.
     */
    deferredResolution?: true;
    /**
     * sha256 (64 lowercase hex chars) of the exact skill-file bytes that
     * produced this run — stamped at RUN time by the caller (cmdRun/
     * compileReplayAndReceipt/runReplayTool all already hold the source they
     * just read to parse the skill), the most truthful moment to capture it.
     * Optional: absent when the caller didn't pass `skillContentSha256` (e.g.
     * a caller with no file on disk to hash). See push.ts's pushSkill for the
     * push-time fallback used on older records that predate this field.
     */
    skillContentSha256?: string;
    /**
     * Set (true) only when this run's manifest preflight was explicitly
     * bypassed via `--ignore-manifest` (docs/specs/flight-recorder-v2.md §1) —
     * the break-glass path. Absent on every run that had no manifest to check,
     * or whose manifest preflight ran normally, so pre-v2 records (and normal
     * v2 ones) stay byte-identical.
     */
    manifestIgnored?: true;
    /**
     * Set only when this run declared a manifest and its preflight ran and
     * passed. Mutually exclusive with `manifestIgnored`; absent when there was
     * no manifest to check. A failed preflight writes no record.
     */
    manifestChecked?: true;
    /**
     * The policy file in force for THIS RUN (docs/specs/policy-attestation-v1.md).
     * Never the one that governed the recording this skill was compiled from —
     * a RunRecord is evidence about one execution, and inheriting the
     * recording-time policy would fabricate a claim about the present out of
     * the past (§3). The skill file carries no policy field at all, so there
     * is nothing to inherit even by accident.
     *
     * Carries NO `rules`/`unmatchedRules`: replay evaluates no deny or dry_run
     * rule (flight-recorder-v2 non-goal), so a consumer MUST NOT read
     * `status: "verified"` here as evidence that any rule blocked, intercepted
     * or evaluated anything during this replay. On this path the file governs
     * the state gate alone. The absence of the counts IS that statement (§2.4).
     *
     * Optional and additive: absent on every record written before the field
     * existed and on any caller that reported nothing — which is NOT the same
     * as `absent`, the positive finding that a lookup happened and found no
     * file. Verification never requires it.
     */
    policy?: PolicyClaim;
    /**
     * Sorted step numbers that had an injected failure this run (`--fail
     * N[=status]`, docs/specs/flight-recorder-v2.md §3) — present only when
     * `RunOptions.mockFailures` was non-empty. A mock run is a local recovery
     * test, never a real receipt: `reelier push` refuses to push a record that
     * carries this field (src/push.ts).
     */
    mockFailures?: number[];
    steps: StepRecord[];
    totals: {
        steps: number;
        /** Steps whose outcome is exactly "passed" — never includes "unchecked". */
        passed: number;
        /** Steps that ran with zero assertions (honest-success rule: never counted as "passed"). */
        unchecked: number;
        /** Steps skipped because an earlier step diverged and didn't heal. */
        skipped: number;
        failed: number;
        ms: number;
        /** 0 for a pure-L0 run (no escalation ever attempted). */
        llmInputTokens: number;
        llmOutputTokens: number;
    };
}
/** Ordinary execution records only; deferred follow-ups are evidence about earlier runs. */
export declare function executionRecords(records: readonly RunRecord[]): RunRecord[];
export interface RunOptions {
    vars?: Record<string, string>;
    allowDestructive?: boolean;
    /** Permit `idempotent-write` steps to execute. Default false — replay is read-only. `allowDestructive` implies this. */
    allowWrites?: boolean;
    tools?: Record<string, Tool>;
    /** Directory under which .reelier/runs/<skill>.jsonl is written. Defaults to cwd. */
    cwd?: string;
    /**
     * When true, `runSkill` still executes every step's tool call normally
     * (including a destructive step, subject to the usual `allowDestructive`
     * gate) — this only skips the final append to
     * `.reelier/runs/<skill>.jsonl`. It is NOT "no execution, no side
     * effects" — for that, use `dryRunSkill` instead, a separate function
     * that never calls a tool at all. (The CLI's `--dry-run` flag uses
     * `dryRunSkill`, not this option — see SPEC.md §6.1's "dryRun" note.)
     */
    dryRun?: boolean;
    onStep?: (record: StepRecord, filledAction: {
        tool: string;
        args: unknown;
    }) => void;
    /** 0 (default) = pure deterministic replay, LLM never constructed or called. 1 = L1 only. 2 = L1 then L2. */
    maxLevel?: 0 | 1 | 2;
    /** Required (and only ever touched) when maxLevel >= 1. Constructing this is the caller's job — the runner never builds one itself. */
    llm?: LlmClient;
    llmModel?: string;
    llmL2Model?: string;
    /** Path to the skill's source file, required for write-back on a successful heal. Without it, a heal still passes this run but a stderr warning is printed (nothing to persist to). */
    skillPath?: string;
    /** sha256 of the skill-file bytes the caller read to produce `skill` — stamped verbatim onto the resulting RunRecord. See RunRecord.skillContentSha256. */
    skillContentSha256?: string;
    /** Threaded verbatim onto RunRecord.manifestIgnored — set by the caller (cmdRun) when `--ignore-manifest` bypassed the manifest preflight. The runner itself never evaluates a manifest; this is purely a receipt annotation. */
    manifestIgnored?: boolean;
    /** Threaded onto RunRecord.manifestChecked by callers after a declared manifest preflight passes. */
    manifestChecked?: boolean;
    /**
     * `--fail N[=status]` (docs/specs/flight-recorder-v2.md §3): step number ->
     * HTTP status to inject as a synthetic Observation instead of dispatching
     * that step's real tool call. The synthetic failure flows into the SAME
     * assert/bind evaluation and, on divergence, the SAME escalation ladder a
     * real failure would hit. Absent/empty = no injection, today's behavior.
     */
    mockFailures?: Record<number, number>;
    /** Declared-probe timeout in ms (consequence-layer §1.6). Default 2000. A probe that exceeds it degrades the attestation, never the step. */
    probeTimeoutMs?: number;
    /**
     * Expect keystore file for state-conditioned approvals (wave2 §3.4).
     * Default: `REELIER_EXPECT_KEYS` env var, else `~/.reelier/expect-keys.json`.
     * Read lazily, only when a step actually carries `expect:` — the read path
     * and expect-less skills gain zero I/O (I-2).
     */
    expectKeystorePath?: string;
    /**
     * State gate (wave2 §5.5, S8): "refuse" = the repo's policy.yml opted
     * into fail-closed — a write step whose pre-state check lands mismatch
     * OR unevaluated is REFUSED before dispatch: outcome `failed`, the
     * spec's refusal string in failures[], `stateCheck.action: "refused"`,
     * no write block, no attest — dispatch provably never issued. Loaded
     * from .reelier/policy.yml by cmdRun; the runner itself never reads a
     * policy file (A3 holds at the library boundary). Absent = recorder
     * mode, byte-identical to pre-S8 behavior.
     */
    stateGate?: "refuse";
    /** The four-state policy claim for THIS run, resolved by the caller from the same read that decided the gate. Omitted -> the record carries no `policy` key. */
    policy?: PolicyClaim;
    /**
     * W5-T3: the run's clock, epoch ms. Defaults to `Date.now()`, following the
     * `dryRunSkill(skill, vars, now = Date.now())` precedent. This is the SAME
     * snapshot `{{today}}`/`{{today±Nd}}` already resolve against (one snapshot
     * per run, so no fill can straddle a UTC midnight), now injectable so an
     * approval TTL can be tested at an exact instant.
     *
     * Deliberately NOT threaded through the `Date.now()` calls that produce
     * recorded `ms` durations or the wall-clock `observedAt`/`dispatchedAt`
     * stamps: those measure the RUN, not the approval, and rewriting them would
     * be a regression risk with no test to justify it.
     */
    now?: number;
}
/**
 * §5.5 refusal strings — stable API (they become gate-event labels),
 * test-pinned verbatim against the spec. No flag overrides a state-gate
 * refusal (I-10); the strings say so because the operator reading them
 * will reach for the flag next.
 */
export declare const STATE_GATE_REFUSAL_MISMATCH: string;
/**
 * Coverage-gate refusal (artifact-attestation-v1 §7). Names the declared
 * fields that did not resolve — names only, never values, and capped exactly
 * as every other field list in this package is, so a wide projection cannot
 * turn a refusal into a wall of text.
 *
 * It claims ONLY that the declared coverage did not resolve. It does not
 * claim the write would have been wrong, and the wording must never imply it.
 */
export declare function emitGateRefusal(unresolved: string[]): string;
export declare function stateGateRefusalUnevaluated(reason: string): string;
export interface DryRunStep {
    n: number;
    title: string;
    tool: string;
    args: unknown;
    effect: string;
}
/**
 * Recursively fill {{var}} placeholders inside string values of a JSON-like
 * structure. `{{today}}` / `{{today-Nd}}` / `{{today+Nd}}` (N = 1-365) are
 * computed deterministically from `now` (default `Date.now()`) rather than
 * looked up in `bindings` — see the module comment above. This is the one
 * place reelier deliberately introduces a run-time-dependent value; callers
 * that need reproducible fills across an entire run (dryRunSkill, runSkill)
 * pass a single `now` snapshot through every fillTemplate call so a run
 * never straddles a UTC midnight boundary mid-execution.
 */
export declare function fillTemplate(value: unknown, bindings: Record<string, unknown>, now?: number): unknown;
/** Produce the filled action for every step without executing anything. */
export declare function dryRunSkill(skill: Skill, vars?: Record<string, string>, now?: number): DryRunStep[];
/**
 * Read a skill's `.reelier/runs/<name>.jsonl` run-record file, one
 * `RunRecord` per non-blank line, in file order. Shared by `reelier bench`
 * and `reelier push` (src/cli.ts, src/push.ts) so both read the exact same
 * way — a promoted-out duplicate would risk drifting silently.
 */
export declare function readRunRecords(filePath: string): Promise<RunRecord[]>;
/** Default projection field allowlists for response-derived attestation — identity/version class only, never content. Exported so tests fuzz the REAL lists instead of a copy that silently decays. */
export declare const ATTEST_BODY_FIELDS: readonly ["id", "_id", "version", "etag", "revision", "sha", "updated_at", "node_id"];
export declare const ATTEST_HEADER_FIELDS: readonly ["etag", "last-modified"];
/**
 * Project an Observation down to the fields that identify/version its
 * resource. With an explicit projection: those top-level body keys only.
 * Without: the conservative default allowlists above (body + headers).
 * Values are stringified for hashing and NEVER stored in any record.
 */
export declare function projectObservation(obs: Observation, projection?: string[]): Record<string, string>;
/** Consequence-layer §1.3 `response-derived`: state derived from the write's own response. Ceiling `partial`; `absent` + reason when nothing derivable. Never fabricated. Hash is a salted commitment (see newAttestSalt). */
export declare function buildResponseDerivedAttest(obs: Observation): StepAttest;
export declare const DEFAULT_PROBE_TIMEOUT_MS = 2000;
export type ProbeResult = {
    ok: true;
    obs: Observation;
    projected: Record<string, string>;
} | {
    ok: false;
    reason: string;
};
/**
 * Run the declared paired read with a hard timeout. Failure DEGRADES
 * (returns a reason) — it must never fail or delay-fail the step
 * (consequence-layer §1.6). Exported for `reelier approve --probe`
 * (state-conditioned approval §4.2): probes dispatch in exactly two
 * contexts — at run time under a matched approval, and at approve time
 * interactively with literal args (I-13); this is the single code path
 * for both. The raw `obs` rides along so the state-conditioned paths can
 * compute their TYPE-TAGGED projection (src/expect-mac.ts) from the same
 * single observation that feeds the salted attest projection (I-4).
 */
export declare function runProbe(decl: StepAttestDecl, tools: Record<string, Tool>, bindings: Record<string, unknown>, ctx: ToolContext, now: number, timeoutMs: number): Promise<ProbeResult>;
/** Run a skill's steps in order. Stops (marks remaining steps "skipped") on the first divergence. */
export declare function runSkill(skill: Skill, options?: RunOptions): Promise<RunRecord>;
