UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,821 lines • 138 kB
import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
import type { GateOutcome } from "./gate-outcome.js";
import { type PreToolUseScreening } from "./runner/gate-exit.js";
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js";
export { cloneObserverInput } from "./runner/gate-exit.js";
import { type AskClass } from "./ask-class.js";
import { type PermissionMode } from "./ask-origin.js";
import type { AutoModeDenialTracker } from "./auto-mode.js";
export { formatHookFeedback } from "./reminder-mint.js";
import type { WiringLegKind } from "./wiring-manifest.js";
import { type SafeNotifier } from "./safe-notify.js";
import type { PermissionSeatInputs } from "./mode-policy.js";
import type { SkillGateSeats } from "./skill-grant.js";
import type { AllowEntryGateSeats } from "./allow-entry.js";
/**
 * In-process hook seam (design/37) — a provider-agnostic interception layer modeled on CC's hooks,
 * kept process-internal (no shell/HTTP executors, no settings files). The original tool-call trio:
 *
 *  - {@link Hooks.preToolUse} runs before a tool executes. It may **rewrite** the args (an `allow` with
 *    `updatedInput`), **restrict** the call (`deny`/`ask`), and/or inject `additionalContext`. It is a
 *    pre-filter only: a hook's `allow` does NOT circumvent the tool-policy gate — the policy still runs on
 *    the (possibly rewritten) args and has the final say (load-bearing invariant; see {@link runToolGate}).
 *  - {@link Hooks.postToolUse} runs after a tool executes. It may replace the result content
 *    (`updatedOutput`) and/or append `additionalContext`.
 *  - {@link Hooks.userPromptSubmit} runs before the objective becomes a user message. It may `block`
 *    submission (the task fails with a model-readable reason) or inject `additionalContext` ahead of it.
 *
 * The seam has since grown well past the trio — each later member carries its own contract below:
 * {@link Hooks.stop} / {@link Hooks.stopFailure} (run-end pushback / API-error terminal observer),
 * {@link Hooks.postToolUseFailure} and {@link Hooks.postToolBatch} (failure + batch-boundary taps),
 * {@link Hooks.preCompact} / {@link Hooks.postCompact} (compaction gate + observer),
 * {@link Hooks.permissionDenied} (deny observer), and the {@link Hooks.preToolUseObservational}
 * declaration flag.
 *
 * All feedback the model should see (a block reason, injected context) is wrapped as a
 * `<system-reminder>` via {@link formatHookFeedback} so the model can read and self-correct.
 *
 * **FAILURE POSTURE, and a correction to a widely-quoted anchor (recorded 2026-08-23).** Three repos
 * have reasoned from the sentence "CC fails open on hook failures, exit code 2 excepted". That sentence
 * is TRUE OF ONE LANE and false as a general statement, and the difference lands exactly on this seam.
 * CC runs hooks of seven kinds; the `command` kind is a shell script, and for it a spawn failure, a
 * non-zero exit and a per-hook timeout are all non-blocking — the call proceeds. But CC ALSO has an
 * in-process CALLBACK lane, which is the one structurally equivalent to this interface, and there a
 * PreToolUse callback that throws or times out is FAIL-CLOSED, with its own message stating that the
 * tool call was not executed. (Two further details the short sentence hides: the decision channel is
 * stdout JSON, with the exit code only a fallback — so exit 0 can block and a non-zero non-2 exit with
 * valid JSON does not; and the callback lane's UserPromptSubmit timeout BLOCKS the prompt.)
 *
 * So this seam's fail-closed postures are not a sema invention to be justified against CC — they are
 * the same posture CC takes on the same kind of lane: a `preToolUse` that throws or does not answer
 * refuses the call, a `userPromptSubmit` that throws or does not answer refuses the prompt, and the
 * OBSERVATION seats swallow and disclose because an observer has nothing to fail closed about. Anyone
 * quoting the short sentence to argue this layer "should" fail open is quoting the script lane.
 */
export interface Hooks {
    preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise<PreToolUseResult | undefined>;
    /**
     * Deployment SELF-DECLARATION: this deployment's {@link preToolUse} is a pure OBSERVER — a tracer, an
     * audit sink, a metrics counter — that never judges a call. Declaring it changes exactly one thing:
     * the face stops being folded into the DELEGATION chain (`InheritedGate.parentConstraints`), so a
     * delegated child inherits no screening entry for it.
     *
     * Why the declaration exists. The delegation fold (issue #33) keys on PRESENCE — "a preToolUse is
     * wired" — because a callback cannot be inspected for whether it will ever return a verdict. That
     * reading is right for a screening face and wrong for an observer, and the difference is not free:
     * every inherited entry is one more OPAQUE constraint on the child, recorded on a durable park as
     * `parentConstraintCount`. A deployment that flips on a default-OFF diagnostic tracer therefore also
     * changes the shape every parked child's resume has to re-supply. The declaration is how a deployment
     * says which of the two it wired, on the same footing as every other thing the library trusts a
     * deployment to state about its own wiring.
     *
     * What does NOT change: the callback still runs in THIS task's own tool gate, on every call, with the
     * same context and the same crash posture (a throw is still the fail-closed deny). Observation is
     * untouched; only the inherited constraint goes away.
     *
     * The face also receives a DETACHED copy of the arguments rather than the live object, because the
     * gate reads a rewrite off reference identity and an in-place edit would otherwise be a second channel
     * that outlives the dropped return value. That detachment is BEST-EFFORT, in the same sense (and by the
     * same helper) as every other observe-only payload here: a graph `structuredClone` cannot take — a
     * function, a live handle — falls back to a shallow copy whose nested objects still alias, and a
     * `SharedArrayBuffer` shares its memory even when the clone succeeds. Phase-1 arguments are the model's
     * JSON on the ordinary path, which clones whole; the residual is reachable only through values a
     * DEPLOYMENT put there (its own rewrite, or driving `runToolGate` directly), and the face and the
     * deployment are one trust principal — this seam helps a deployment keep its own declaration, it is not
     * a boundary against an untrusted party.
     *
     * If a declared-observational face RETURNS SOMETHING ANYWAY, the return is REFUSED, not obeyed: the
     * call proceeds as if the hook had no opinion (`undefined`), and the deployment's `onError` face is
     * told (`phase:"hook"`, classification `observational-hook-verdict-ignored`). That covers the whole
     * result object — an `additionalContext` injection is a run-affecting capability too, so a face that
     * declared it only watches does not get to write into the model's transcript either. The refusal is
     * loud on purpose: a declaration that could be quietly contradicted would be a way to keep a
     * screening face's verdicts while shedding the delegation constraint they belong to.
     */
    preToolUseObservational?: true;
    postToolUse?(toolName: string, input: unknown, output: HookToolOutput, ctx: HookToolContext): PostToolUseResult | undefined | Promise<PostToolUseResult | undefined>;
    /**
     * design/373 §4.3 (D2) ⚠️ WIDENED INVOCATION SET: this seat now screens EVERY user-lane entrance,
     * not just the objective — a live `TaskStream.steer`, a wake resume's message (pre-CAS, at the
     * verb), and a parked steer's resume redelivery (any draining resume kind) all pass through it, discriminated by
     * {@link UserPromptSubmitContext.source}. A hook that blocks unconditionally therefore starts
     * refusing steers too — branch on `ctx.source` (+ `ctx.actor`) to scope a verdict. The screen's
     * domain is caller/operator PROSE only: engine-authored frames (task notifications, diagnostics,
     * recall) never pass here — external `notify()` text takes the sanitized DATA-frame lane instead
     * (ruled: notification frames are not prompts; see the notify contract). The same input can
     * legally arrive twice under two sources (live + parked redelivery) — idempotency is the hook's,
     * keyed on `ctx.inputId`. Block semantics per entrance: objective ⇒ the task ends `blocked`;
     * live steer ⇒ typed `steering.blocked_by_hook` to the steer caller; resume message ⇒ typed
     * pre-CAS refusal (the checkpoint stays pending); parked redelivery ⇒ the frame is withheld
     * with a `steering.parked_input_blocked` notice + a `delivery:"blocked"` human_input account,
     * and the resume itself proceeds (a blocked instruction must never wedge a wake).
     *
     * ⚠️ RE-ENTRANT within one leg: the steer verb goes live the moment the run's handle publishes —
     * BEFORE the objective seat runs — so a steer screen can overlap the objective screen (and a
     * resume-message screen, which fires pre-CAS at the verb) in flight. A stateful implementation
     * (per-call temp files, a single in-flight latch, a shared accumulator) must tolerate concurrent
     * entry; only steer-vs-steer is serialized. And that serialization has a LATENCY face: queued
     * steers screen strictly FIFO, so each waits behind its predecessor's ENTIRE hook bound (the
     * resolved `timeoutMs`, default 600s) — a slow or wedged screen delays every steer behind it by
     * up to the full bound, which is a reason to declare a short `timeoutMs` on deployments that
     * screen steers.
     */
    userPromptSubmit?(prompt: string, 
    /** #281 件A (additive): the run/leg identity envelope — an implementation declaring only
     *  `(prompt)` keeps working. Always supplied on the engine's emission. design/373: carries the
     *  entrance discriminator (`source`/`inputId`/`actor`) — see {@link UserPromptSubmitContext}. */
    ctx?: UserPromptSubmitContext): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
    /**
     * roadmap #5 (CC 198 Stop hook, :473831-): runs when the agent WOULD OTHERWISE END its run (no
     * more tool calls, steering and follow-up queues dry). Return `{ block: reason }` to PUSH BACK —
     * the reason is injected as a model-readable message and the run continues another turn (external
     * adjudication: "the tests still fail", "the deliverable is missing X"). Return `undefined` to let
     * the run end. Guardrails (CC-exact): consecutive blocks are capped (default 8 — the loop then
     * overrides and ends with a warning); `ctx.stopHookActive` is true when the run is ALREADY
     * continuing due to a previous block — a hook that ignores it and blocks unconditionally loops
     * until the cap. `maxTurns` still outranks everything.
     */
    stop?(ctx: StopHookContext): StopHookResult | undefined | Promise<StopHookResult | undefined>;
    /**
     * design/134 (CC PostToolUseFailure parity, SDK 0.3.202): runs after a tool EXECUTION fails —
     * mutually exclusive with {@link postToolUse} (success → postToolUse; failure → this). Fires ONLY
     * for genuine execution failures: a gate/policy block, a plan-mode deny, a pre-execution abort or a
     * durable suspend triggers NEITHER callback (CC scopes those to the PermissionDenied event domain,
     * not implemented here). Capability is additionalContext-only (CC-exact): the text is appended to
     * the error tool result as a `<system-reminder>`; the failure itself cannot be rewritten.
     */
    postToolUseFailure?(toolName: string, input: unknown, failure: HookToolFailure, ctx: HookToolContext): PostToolUseFailureResult | undefined | Promise<PostToolUseFailureResult | undefined>;
    /**
     * design/134 (CC PostToolBatch parity, SDK 0.3.202): fires ONCE after every tool call in a batch
     * (one assistant turn's calls, design/120 stream-inline included) has resolved, before the next
     * model request. Per-tool postToolUse/postToolUseFailure fire first; the batch callback fires after,
     * at the turn boundary. A zero-tool turn does not fire; an aborted/steered-away boundary does not
     * fire (same gate family as design/133 attachments). additionalContext is injected once for the
     * whole batch as a boundary `<system-reminder>` (sanitized, shares the attachment byte cap).
     *
     * RB-467 (test-AI black-box repro, board 2305: ONE injection ate a whole subsequent batch): a batch
     * landing on a boundary where the engine itself injected (final-verification nudge / finalize
     * write-out) is now DELIVERED with `meta.injectedThisTurn` set, instead of being suppressed — for a
     * deployment observer, any suppressed boundary is lost audit data; attribution belongs to the
     * observer. On such a boundary the hook's `additionalContext` is IGNORED (the boundary already
     * carries an engine injection; stacking observer text there is the double-inject this gate
     * originally guarded against — that half of the old behavior stands).
     */
    postToolBatch?(batch: PostToolBatchCall[], meta?: {
        injectedThisTurn: "final_verification" | "finalize";
    }, 
    /** #281 件A (additive): the run/leg identity envelope, on its OWN parameter — `meta`'s presence
     *  is an existing signal ("the engine injected at this boundary") that must not become
     *  always-true just because identity rides along. Always supplied on the engine's emission. */
    ctx?: PostToolBatchContext): PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>;
    /**
     * design/134 (CC PreCompact parity): runs before each compaction, AFTER the trigger gate and a
     * valid cut point are confirmed (so every preCompact corresponds to a compaction that would
     * actually happen) and before the summary LLM call. `trigger`: "auto" (threshold) | "manual"
     * (/compact) | "forced" (promptTooLong recovery and trim-pressure — compaction is not optional
     * there). Return `{ block }` to skip this compaction (honored on auto/manual ONLY; ignored with a
     * trace on forced — blocking a compaction the provider already demanded would kill the run).
     * `additionalInstructions` is merged (appended) into the summarization instructions (CC
     * mergeHookInstructions shape). A THROWING preCompact is swallowed on every path (traced, treated
     * as no-block): an observer bug must never feed the compaction breaker or kill PTL recovery.
     */
    preCompact?(ctx: PreCompactContext): PreCompactResult | undefined | Promise<PreCompactResult | undefined>;
    /**
     * design/134 (CC PostCompact parity): observe-only, after the compaction landed (summary appended
     * to the session). No return capability (CC-exact); a throw is swallowed and traced.
     */
    postCompact?(ctx: PostCompactContext): void | Promise<void>;
    /**
     * StopFailure (the last engine-owned event of the registry-core 0.1.51 hooks contract; CC semantics:
     * "the turn ended because of an API error"). Fires ONCE, after the result is assembled, when the task
     * terminates `failed` BECAUSE the model/API layer errored (a brain error terminal —
     * auth / network / rate_limit / server_error / …). It does NOT fire for engine-owned failure classes:
     * budget, timeout/abort, max-turns, degenerate/walltime cutoffs (deliberate engine cuts), storage
     * conflict, blocked, or a durable suspend — those are not API errors. Observe-only (CC-exact: the
     * contract defines no StopFailure output capability); a throwing callback is swallowed + traced
     * (design/134 discipline — an observer bug must never rewrite a terminal state).
     */
    stopFailure?(ctx: StopFailureContext): void | Promise<void>;
    /**
     * design/134 R5 (CC PermissionDenied parity, SDK 0.3.202): runs when the tool gate's
     * adjudicate chain DENY-SHORT-CIRCUITS a tool call — a ToolPolicy deny, a gate tighten (egress /
     * irreversibility / coarse shellGate) whose `ask` resolved to deny (headless auto-deny included), or the
     * plan-mode write-deny. Closes the observation blind spot where a blocked call fires NEITHER postToolUse
     * NOR postToolUseFailure (R3 scoping). Does NOT fire for:
     *  - a **durable suspend** (the ask path — CC surfaces that via a `can_use_tool` control_request);
     *  - a **PreToolUse hook deny** (CC-exact: "PreToolUse hook denies circumvention canUseTool and are not
     *    covered here").
     * DOES fire for a **PreToolUse hook CRASH** (RB-442, `source:"hook"`): a throwing hook is intercepted by
     * the ENGINE, which then makes its OWN fail-closed deny decision — likewise a hook result the decision
     * receipt REFUSES (out of contract, undetachable). The R5 exclusion above covers the hook's own DENY
     * DECISION — not an engine decision that merely happened near a hook.
     * Observe-only v1 (CC-exact payload `{toolName, input, toolCallId, reason}` + our `source` enum standing
     * in for CC `decision_reason_type`): no `retry` capability (recorded — retry semantics vs our adjudicate
     * chain need their own design), no additionalContext. A throwing callback is swallowed + traced via
     * `onError(phase:"hook")` — an observer bug must never alter the deny outcome (design/134 discipline).
     *
     * **HOW WIDE THIS SEAT IS, stated honestly (anchoring re-check 2026-08-23).** The exclusion sentence
     * above is quoted from the SDK's TYPE-LEVEL documentation, and the whole R5 scoping argument was
     * built on it without ever being checked against a shipped bundle. Against CC 2.1.223's runtime the
     * picture is different in a way that matters for how this comment may be USED: CC emits its
     * PermissionDenied event from exactly ONE site, guarded to the auto-mode CLASSIFIER's refusals —
     * a rule deny, a mode deny and a headless auto-deny emit nothing at all. So "a hook's own deny is
     * excluded" is TRIVIALLY true there (almost everything is excluded), and it must not be read as
     * evidence that this seat's TRIGGER SURFACE matches CC's. It does not: sema fires on four engine
     * emissions with seven `source` values, deliberately WIDER. The width is the useful direction for an
     * observation seat — the blind spot R3 named is real and a narrower seat would not close it — and it
     * is a divergence rather than a parity claim. The exclusion itself stays, on its own merit: a hook's
     * deny is the HOOK's decision, and this seat exists to attribute the ENGINE's.
     */
    permissionDenied?(payload: PermissionDeniedPayload): void | Promise<void>;
    /**
     * The TIME BOUND every seat on this object runs under, in milliseconds. Absent ⇒
     * {@link DEFAULT_HOOK_TIMEOUT_MS}. It is the analog of the CC hook record's own `timeout` field —
     * declared once per hook record there, once per hook OBJECT here, because this seam's record IS the
     * object. The unit is ms rather than CC's seconds: every other deadline this library takes ends in
     * `Ms`, and one unit per codebase beats one unit per upstream.
     *
     * **Why the seam has a bound at all.** Every callback above is DEPLOYMENT code awaited on the
     * engine's critical path, and one that never returns used to stop the whole run — the tool gate
     * awaited `preToolUse` bare, so a hung screening face left the call unsettled and the task's own
     * timeout/cancel could not end it. The diagnosis was an internal asymmetry rather than a guess: the
     * SAME callback installed as an inherited constraint on a delegated child was already bounded, because
     * that path folds it into a `ToolPolicy` and the policy wait is abort-raced — and the sibling seam in
     * this very file (`reversibilityProbe`) had already been given a default deadline for the identical
     * reason. Policies were bounded, probes were bounded, hooks were not.
     *
     * **What a bound does NOT change.** A seat that answers keeps its answer, byte for byte; a seat that
     * throws keeps its existing crash posture (this member changes nothing about a throw). The bound is
     * consulted only when a seat has not answered yet, and what happens then is per-seat and stated on
     * each member's own contract — decision seats fail CLOSED (`preToolUse` refuses the call the way a
     * crash does; `userPromptSubmit` refuses the prompt), observation seats are ABANDONED and the engine
     * proceeds as if the seat had returned nothing. Either way the deployment is told on its `onError`
     * lane, because a silently-dropped seat is the failure mode this member exists to make visible.
     *
     * **The seat also gets a signal.** An expired wait would otherwise leave the callback running with
     * nobody reading its answer — an orphan of the deployment's own making. So every context carries an
     * optional `signal` that aborts when this bound elapses (or the task aborts), and an implementation
     * that honors it can cancel the read it was waiting on. Honoring it is optional; a seat that ignores
     * it is exactly as bounded as before, the engine simply stops waiting.
     *
     * **WHAT THE BOUND IS ON, stated narrowly because the difference is not academic.** It bounds the
     * engine's WAIT, not the callback's CPU. A hook that blocks the thread SYNCHRONOUSLY — a spin loop, a
     * synchronous exec, a synchronous parse of something enormous — cannot be preempted by this bound or
     * by any other in-process mechanism, because the timer that would fire is queued behind the code that
     * is not yielding; the ordering of the timer's installation makes no difference to that. In-process
     * is the seam's whole premise (it is what makes a hook able to rewrite args and return a verdict),
     * and preemption is what it trades away. A deployment that must survive a hook it does not trust to
     * yield runs that hook somewhere with its own thread — a worker, a process, a service — and awaits
     * the result from here. What this member DOES bound is every asynchronous wait, which is where the
     * failure this was written for lives: a callback that awaits an unreachable host, a lock, a queue.
     *
     * **Value discipline** (bad values are loud, never silently reinterpreted): a non-finite, negative, or
     * above-`setTimeout`-ceiling (2147483647) value is REFUSED to the default and reported on the
     * deployment's error lane — the same treatment, and the same ceiling, the gate's `approvalTimeoutMs`
     * seat gives its own garbage. `0` is honored as written (every seat expires immediately), because a
     * deployment that writes zero has said something, and guessing that it meant "unbounded" would be the
     * one reading this member cannot support.
     */
    timeoutMs?: number;
}
/**
 * The bound a {@link Hooks} seat runs under when the deployment declares none ({@link Hooks.timeoutMs}).
 *
 * The value is CC's own hook default (600s) rather than a number invented here. It is deliberately
 * GENEROUS: the seat bound's job is to make a wedge terminal, not to police how long a legitimate hook
 * may think — the task's own abort signal is what ends a hung DECISION seat promptly on a run that has a
 * deadline, and this ceiling is what ends it on a run that does not. A deployment whose hooks should
 * answer in a second says so with `timeoutMs`.
 */
export declare const DEFAULT_HOOK_TIMEOUT_MS = 600000;
/** `setTimeout`'s own ceiling: above it the platform silently clamps to ~1ms, which would turn a
 *  deployment's "very long bound" into an instant expiry it never asked for. Refused loudly instead
 *  (same ceiling, same posture as the gate's `approvalTimeoutMs`). */
export declare const MAX_HOOK_TIMEOUT_MS = 2147483647;
/**
 * How long an observation seat still gets when the task is cancelled `elapsed` ms into its wait — the
 * grace, capped by what is LEFT of the declared deadline.
 *
 * The `remaining` argument, not the whole deadline, is the correction: arming a fresh
 * `min(GRACE, timeoutMs)` from the abort moment restarts the clock, so an abort landing near the
 * deadline pushes the seat's absolute end PAST the number the deployment declared — up to double it for
 * a sub-second bound — on precisely the cancelled-task settlement path the grace exists to shorten. The
 * two contract sentences this file already carries ("never an extension", "a deployment that declared a
 * SHORTER `timeoutMs` keeps its own number") were true only in the aborted-on-entry case, where elapsed
 * is zero and the two formulas coincide. Now they are true everywhere.
 *
 * Clamped at zero: a deadline already spent is no grace at all, not a negative timer.
 */
export declare function postAbortGraceMs(remainingMs: number): number;
/**
 * Read {@link Hooks.timeoutMs} as a usable deadline: the declared value when it is one, the default
 * when it is absent, and the default LOUDLY when it is garbage (bad-value loudness — a knob's bad value
 * must never resolve silently to the default).
 *
 * `owner` is the record the value was read off; passing it bounds the disclosure to once per record
 * (see {@link badHookTimeoutReported}). Omit it where the caller already bounds its own channel.
 */
export declare function resolveHookTimeoutMs(supplied: number | undefined, report?: (err: unknown) => void, owner?: object): number;
/** The ten {@link Hooks} callback seats, named so a bound wait can say WHICH one did not answer. */
export type HookSeatName = "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "stop" | "stopFailure" | "preCompact" | "postCompact" | "permissionDenied"
/** design/380 O2 — not a {@link Hooks} record seat: the `RunnerDeps.resumePreflight` deployment
 *  callback, run through the same bounded-wait home so the pre-CAS resume ladder cannot grow its
 *  own dialect of "a deployment callback gets a deadline". */
 | "resumePreflight";
/** What one bounded seat invocation produced: the seat's own value, or the fact that the wait ended
 *  first. A seat that THREW is neither — the throw propagates out of {@link runHookSeat} unchanged, so
 *  each site's existing crash arm keeps owning it. */
export type HookSeatOutcome<T> = {
    readonly expired: false;
    readonly value: T;
} | {
    readonly expired: true;
    readonly cause: "timeout" | "aborted";
};
/** How one seat invocation is bounded. */
export interface HookSeatBound {
    /** The deployment's declared bound ({@link Hooks.timeoutMs}); re-validated here, so a caller that
     *  already validated pays nothing and a caller that did not cannot pass garbage through. */
    timeoutMs?: number;
    /** The task abort signal. It is ALWAYS forwarded to the seat (so an implementation can cancel its own
     *  underlying wait) and it ALWAYS shortens the wait — the two seat kinds differ only in HOW MUCH.
     *  Supply it wherever the caller has one: a seat with no signal is bounded by its deadline alone, and
     *  a ten-minute default deadline is not a cancellation story. */
    signal?: AbortSignal;
    /** True for a seat whose answer is MOOT once the task is ending — the DECISION seats. Those settle as
     *  `aborted` the instant the task is cancelled. An observation seat leaves this false on purpose:
     *  `postToolUseFailure` fires precisely BECAUSE the task aborted (`isInterrupt`), and cancelling it
     *  outright would delete the observation the seat exists to deliver — it gets
     *  {@link POST_ABORT_GRACE_MS} instead of the full deadline. */
    abortEnds?: boolean;
    /** Where a bad {@link timeoutMs} is disclosed. The EXPIRY itself is not reported here — each site
     *  words its own, because the sentence a deployment needs includes what the engine then did. */
    onBadTimeout?: (err: unknown) => void;
    /** The hook record the bound was read off, for once-per-record bad-value disclosure. */
    owner?: object;
}
/**
 * Run ONE {@link Hooks} seat invocation under its bound — the single home of "a deployment callback
 * gets a deadline", so the ten seats cannot grow ten dialects of one wait.
 *
 * Shape notes, each one load-bearing:
 *  · the seat is invoked SYNCHRONOUSLY, in this turn, exactly where the bare `await` used to be — a
 *    callback that reads its own mutable state before its first `await` must see the turn it always saw;
 *  · a SYNCHRONOUS throw propagates unchanged (it never reached a deadline, and the site's crash arm is
 *    the contract for it), and so does an asynchronous rejection;
 *  · the seat's signal fires when — and ONLY when — this function stops waiting, and its promise is then
 *    abandoned with a rejection sink attached (an abandoned seat that fails later must not surface as an
 *    unhandled rejection somewhere else). The task's own signal is never forwarded raw: for an
 *    OBSERVATION seat the task being cancelled is not a reason to stop, it is the reason the seat is
 *    firing at all;
 *  · the TASK signal ends the wait on both kinds of seat, differently. A DECISION seat
 *    ({@link HookSeatBound.abortEnds}) settles as `aborted` the moment the task is cancelled, INCLUDING
 *    when it was already cancelled on entry — a verdict about a call that will not run decides nothing,
 *    and waiting the full deadline for one holds a cancelled task open for as long as the deadline says.
 *    On the entry case the callback is still INVOKED (an audit face keeps seeing the attempt) and its
 *    answer is discarded: settling on the cancellation rather than on whichever of the two happened to
 *    land first is what makes the arm a rule instead of a race.
 *    An OBSERVATION seat is not cancelled by the abort — it fires precisely BECAUSE the task aborted
 *    (`isInterrupt`), and racing it against that same abort would delete the observation it exists to
 *    deliver — but it does not get the full deadline either: the abort collapses its wait to
 *    {@link POST_ABORT_GRACE_MS}. Prompt observers still land; a hung one can no longer hold a cancelled
 *    task's settlement for the length of the whole bound.
 */
export declare function runHookSeat<T>(seat: HookSeatName, bound: HookSeatBound, call: (signal: AbortSignal) => T | Promise<T>): Promise<HookSeatOutcome<T>>;
/**
 * The ONE wording home for "the engine stopped waiting for a hook seat", so the ten sites cannot drift
 * into ten sentences about one fact. `consequence` is the half only the site knows: what the engine did
 * instead.
 *
 * The CAUSE is named, not glossed. A deadline that elapsed and a task that was cancelled are different
 * facts to whoever reads this — the first says a hook is too slow or stuck and wants fixing, the second
 * says nothing about the hook at all — and one sentence covering both would have to be false about one
 * of them. (It was: the first version of this said "did not answer within its Nms bound" on every
 * expiry, including the ones where the bound was never reached.)
 */
export declare function hookSeatExpiredError(seat: HookSeatName, timeoutMs: number, cause: "timeout" | "aborted", consequence: string): Error;
/** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + the gate outcome). */
export interface PermissionDeniedPayload {
    /** #281 件A — the run/leg identity envelope. Present on every ENGINE emission (the gate's single
     *  deny exit, the hook-crash intercept, the plan-mode and compliance denies); absent only when a
     *  host drives {@link runToolGate} without `ToolGateInput.identity`. */
    identity?: HookInvocationIdentity;
    toolName: string;
    /** The FINAL (post-hook-rewrite / post-policy-rewrite) args the chain adjudicated — what would have
     *  executed; not necessarily the model's original args. */
    input: unknown;
    toolCallId: string;
    /** Human/model-readable deny reason (CC `reason`) — the RAW decision text, NOT `<system-reminder>`-wrapped. */
    reason: string;
    /**
     * The gate outcome this deny is — the SAME object the call's `tool_end` frame carries as `gate` and
     * the engine's per-call sideband records (one mint, three faces). `disposition.deniedBy` says which
     * LAYER refused (our `decision_reason_type` analog for the refusing half); when the refusal came through
     * an ask resolution, `settlement` says how the wait ended and `origin` who asked. Nothing is read out of
     * an absence: a direct policy / hook / plan-mode / compliance deny carries no settlement because it
     * settled none.
     */
    gate: GateOutcome;
    /** {@link HookToolContext.permissionMode}, read when the deny is observed. */
    permissionMode?: PermissionMode;
    /** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
     *  already happened and nothing this callback does can change it, so the signal says exactly one
     *  thing: stop reading, nobody is waiting for your answer any more. */
    signal?: AbortSignal;
}
/** Context for {@link Hooks.stopFailure} — aligned with the TaskResult error face (observe-only). */
export interface StopFailureContext {
    /** #281 件A — the run/leg identity envelope (always present on the engine's emission; the seat is
     *  optional only because the envelope's whole contract is). See {@link HookInvocationIdentity}. */
    identity?: HookInvocationIdentity;
    /** Human-readable error message (the assembled `errorMessage`; any `[code]` prefix already stripped). */
    error: string;
    /** Machine-readable kind — the assembled `errorCode` lifted from the brain's `[code]` prefix
     *  (auth / network / rate_limit / server_error / …); absent when the provider stamped no code. */
    errorKind?: string;
    /** Turns completed when the failure ended the run. */
    turns: number;
    /** {@link HookSeatSignal} — this invocation's own abort signal. */
    signal?: AbortSignal;
}
/**
 * The abort signal every hook seat's context carries (`ctx.signal`), documented once here and
 * referenced from each seat.
 *
 * It carries exactly one meaning: **the engine has stopped waiting for this invocation.** It is not the
 * task's abort signal and it never fires early on the task's behalf — a DECISION seat's wait ends when
 * the task is cancelled, so its signal fires then; an OBSERVATION seat's wait ends at its deadline or
 * after the post-abort grace, so its signal fires there and NOT at the cancellation itself. The
 * difference is deliberate: a `postToolUseFailure` fires precisely BECAUSE the task aborted, and a
 * signal that went off at the cancellation would tell a cooperative observer to abandon the very
 * observation it was invoked to deliver.
 *
 * Honoring it is optional and it changes nothing about the engine's own behavior — the engine stops
 * waiting either way. What it is for is the deployment's side of that: a seat that was awaiting a
 * network read, a lock or a subprocess can cancel it instead of leaving an orphan running with nobody
 * left to read its answer. Absent when a host drives a seat outside the engine's own invocation (no
 * bound, nothing to signal).
 */
export type HookSeatSignal = AbortSignal;
/** The failed tool execution a PostToolUseFailure callback inspects (design/134). */
export interface HookToolFailure {
    /** Model-facing error text of the failed call (the error tool result's text content, joined). */
    error: string;
    /** Wide any-abort semantics (⊇ CC `is_interrupt`, which is user-interrupt specifically): true when
     *  the task abort signal fired — timeout, budget, cancel or suspend — not only a human interrupt. */
    isInterrupt: boolean;
    /** Tool execution time in ms when the engine has it (CC 0.3.202 `duration_ms` parity); else absent. */
    durationMs?: number;
    /** Element-level clone of the error result's content blocks (mutating them cannot rewrite the
     *  transcript — the failure is observe-only). */
    content: Array<TextContent | ImageContent | DocumentContent>;
    /** BEST-EFFORT deep clone (structuredClone) of the tool's raw details. When the details graph is
     *  not structured-cloneable (functions/handles), this is the LIVE reference — treat it as
     *  READ-ONLY: a mutation would corrupt the result the loop is about to commit. */
    details: unknown;
}
/** A PostToolUseFailure result: append model-readable context to the error result (CC-exact, no rewrite). */
export interface PostToolUseFailureResult {
    additionalContext?: string;
}
/** One resolved call in a postToolBatch payload (CC PostToolBatchToolCall shape, thin projection —
 *  `response` is a bounded text digest of the result, never the full details object). */
export interface PostToolBatchCall {
    /** The name the model spelled on the tool_use block (an alias stays an alias) — the reference's PostToolBatch payload
     *  names each block the same way, while the per-call post hooks receive the resolved tool's wire name. Correlate
     *  the two faces by `toolCallId`. */
    toolName: string;
    input: unknown;
    toolCallId: string;
    response?: string;
    isError: boolean;
}
/** A PostToolBatch result: inject context once for the whole batch at the turn boundary. */
export interface PostToolBatchResult {
    additionalContext?: string;
}
/** Context for {@link Hooks.preCompact} (design/134). */
export interface PreCompactContext {
    /** #281 件A — the run/leg identity envelope. Present when the Runner drives the compaction (its
     *  wrapper injects it); honestly absent when a deployment drives the compaction module directly
     *  (presence law arm 3 on {@link HookInvocationIdentity}). */
    identity?: HookInvocationIdentity;
    /** "auto" = threshold-triggered; "manual" = /compact; "forced" = promptTooLong recovery or
     *  trim-pressure propagation (block is ignored on forced — the compaction is not optional). */
    trigger: "auto" | "manual" | "forced";
    /** The summarization instructions in effect (spec/deployment-level), when set. */
    customInstructions?: string;
    /** {@link HookSeatSignal} — this invocation's own abort signal. */
    signal?: AbortSignal;
}
/** A PreCompact result: skip this compaction (auto/manual only) and/or extend the summary instructions. */
export interface PreCompactResult {
    /** When set (and trigger is not "forced"), this compaction is skipped; the reason is traced. */
    block?: string;
    /** Appended to the summarization instructions for this compaction (never replaces them).
     *  LLM-summary path only: a compaction served from a `summaryProvider` reuse (Seam C) never runs
     *  the summarization call, so these instructions are not consumed there (recorded design gap —
     *  extending the provider contract is out of scope for design/134). */
    additionalInstructions?: string;
}
/** Context for {@link Hooks.postCompact} (design/134, observe-only). */
export interface PostCompactContext {
    /** #281 件A — same presence law as {@link PreCompactContext.identity}. */
    identity?: HookInvocationIdentity;
    trigger: "auto" | "manual" | "forced";
    /** The conversation summary the compaction produced (CC `compact_summary` parity). */
    summary: string;
    /** DIAGNOSTIC, NOT subtractable: prepareCompaction's coordinate, LACKING the overhead term
     *  {@link tokensAfter} carries. Use {@link freedTokens}. */
    tokensBefore?: number;
    /** DIAGNOSTIC, NOT subtractable: always structural + prompt overhead, a different coordinate from
     *  {@link tokensBefore}, so the difference can go negative. Absent when the post measurement failed. */
    tokensAfter?: number;
    /** The engine's measured delta (clamped ≥ 0) — the ONE subtractable number here, and the same value
     *  this pass's `compacted` frame carries. Absent ⇒ the post measurement failed. */
    freedTokens?: number;
    /** {@link HookSeatSignal} — this invocation's own abort signal. */
    signal?: AbortSignal;
}
/** Context for the {@link Hooks.stop} hook (CC `stop_hook_active` parity). */
export interface StopHookContext {
    /** #281 件A — the run/leg identity envelope. The load-bearing member for THIS seat: a process-level
     *  `deps.hooks.stop` fires in root, delegated-child and resume legs alike, and previously could not
     *  tell which run it was being asked to end (matrix row 10/13 residual). Always present on the
     *  engine's stop-gate emission. */
    identity?: HookInvocationIdentity;
    /** True when this run is already continuing because a previous stop() blocked — check it and
     *  return success (undefined) once your condition can't be improved, or you will loop to the cap. */
    stopHookActive: boolean;
    /** Consecutive blocks so far in this run (resets when a stop() call lets the run proceed). */
    consecutiveBlocks: number;
    /**
     * A view of the conversation this hook is being asked to judge — the counters alone cannot serve a
     * hook that asks anything ABOUT THE RUN (the prompt-shaped hooks a settings file declares).
     *
     * LAZY on purpose: a hook that only reads the counters pays nothing, and a hook that needs the
     * conversation gets it in full rather than through a truncation policy the engine would have to invent
     * on its behalf. Callers decide what to send to a model and how much — that decision has a budget and a
     * trust tier attached, and both belong to the caller.
     *
     * Absent when the runner cannot supply one (a stop gate wired without a session).
     */
    getBranch?: () => Promise<readonly SessionTreeEntry[]>;
    /** The run's background tasks still in flight (pending / running / parked under this run's owner triple — the
     *  post-compaction reminder's projection; CC `SubagentStop.background_tasks`). LAZY like {@link getBranch};
     *  `[]` = none in that projection, ABSENT = no view. A parked child here waits on an approval out of band. */
    backgroundTasks?: () => ReadonlyArray<{
        id: string;
        description?: string;
        status: string;
    }>;
    /** {@link HookToolContext.permissionMode}. */
    permissionMode?: PermissionMode;
    /** {@link HookSeatSignal} — this invocation's own abort signal. */
    signal?: AbortSignal;
}
/** A Stop hook result: block the run from ending, with a model-readable reason. */
export interface StopHookResult {
    /** When set, the run does NOT end: this reason is injected (as a `<system-reminder>`) and the
     *  model gets another turn to address it. */
    block?: string;
    /**
     * Extra model-readable context injected at the stop point — DECOUPLED from `block`. When present it
     * is injected (as a `<system-reminder>`, neutral framing: "Stop hook additional context: …") whether
     * or not the hook blocked, and an `additionalContext` WITHOUT `block` still continues the run one
     * more turn.
     *
     * **It DOES count toward the consecutive-block cap, and it never resets it.** This sentence used to
     * say the opposite ("the cap is driven by `block` alone", cited to CC 2.1.201), and it was wrong on
     * both halves. Re-read against CC: the `additionalContext` branch pushes into the SAME array as the
     * blocking-error branch, the loop counts that array's length against the cap, and CC never resets
     * mid-run. The engine was fixed to match; the sentence here was not, so the module that DEFINES this
     * seat and the module that IMPLEMENTS it stated opposite behaviors for years. What the cap bounds is
     * "the run did not end when it otherwise would have", and a context-only push-back is exactly that —
     * a hook whose most common failure mode is "no parseable verdict, here is some text" would otherwise
     * run to `maxTurns` looking busy rather than stuck.
     */
    additionalContext?: string;
}
/**
 * A READ-ONLY path-resolution capability face over the task's {@link ExecutionEnv}, handed to tool hooks
 * on {@link HookToolContext.env} (the hook-env capability seam).
 *
 * **Why it exists.** A hook that judges a path — "is this write inside the workspace", "does this
 * innocent-looking name resolve onto a guarded file" — must resolve it in the filesystem the HANDS
 * actually write to. For a sandboxed/remote deployment that filesystem is NOT the host process's `fs`:
 * the env is minted by `RunnerDeps.executionEnvFactory` DURING prepare, i.e. after the spec (and any
 * policy/hook the caller built with it) already exists, so a hook closed over a spec-time env resolves
 * against the wrong disk and answers `exists`/symlink questions about a different machine. Reading the
 * env off the CALL CONTEXT removes the ordering problem structurally: the context is built after the env
 * is, and a resumed/revived run's rebuilt env is picked up with no re-instantiation protocol.
 *
 * **Capabilities, not the object.** Hooks are an extension boundary the deployment fills with code whose
 * job is to DECIDE; the whole `ExecutionEnv` is a face that can ACT (`exec`, `writeFile`, `remove`,
 * `cleanup`). So this is four INDEPENDENTLY BOUND read primitives and nothing else: no `env`
 * back-reference, no prototype of the env object, and the face itself is frozen (a hook cannot re-point a
 * member for a later hook in the same run). Widening it later is additive and cheap; taking a wide object
 * back is not.
 *
 * The line this draws, stated so nobody reads more into it: it separates what a hook is HANDED from what
 * the environment can do. It is NOT an isolation boundary against code that can already reach the env's own
 * class — anything that can redefine `SomeExecutionEnv.prototype.exec` intercepts every call the ENGINE
 * makes into that env, receiver included, and needs nothing from this face to do it. Deployments that must
 * withstand that run their hooks somewhere else, not behind a narrower argument.
 *
 * **Names are the env's own** ({@link ExecutionEnv} / `FileSystem`), deliberately: a hook author reads one
 * contract, not a translation of it. The seam sketch's `canonicalize`/`root` are `canonicalPath`/`cwd`
 * here — `canonicalPath` IS the realpath-semantics primitive, and `ExecutionEnv` declares no "root" at all
 * (the engine's task root is a RUNNER notion — `deps.rootPath`/worktree/`requestedCwd` — not something the
 * environment reports about itself), so the honest anchor is the env's own working directory. `cwd` is a
 * FUNCTION, not the snapshot string, so a hook reading it late sees where the env is NOW. Read {@link
 * HookEnvCapabilities.cwd} before using it as a base for RELATIVE paths — the task's tracked cwd is a
 * different value, and this face carries the env's, on purpose.
 *
 * **Every member is optional, and absence is the signal.** A member is present iff the underlying env
 * exposes that primitive (`readLink` is optional in `FileSystem`; a transport env may implement less than
 * it declares). A missing member is never a stub that throws or an emulation that guesses — the hook is
 * told, in the only way that cannot be misread, that this environment cannot answer, and takes its own
 * conservative branch. The face carries a NULL PROTOTYPE so that reading is a question about this object
 * alone (see {@link createHookEnvCapabilities}). Errors that the env CAN report stay in the `Result`
 * (these methods never throw, inheriting the `FileSystem` contract verbatim).
 */
export interface HookEnvCapabilities {
    /** {@link ExecutionEnv.canonicalPath} — the canonical path of an EXISTING path, symlinks resolved
     *  (realpath semantics), in the env's own namespace. */
    canonicalPath?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
    /** {@link ExecutionEnv.exists} — `false` for a missing path; other failures (permission, transport)
     *  come back as a `FileError`, which is NOT an answer of "absent". */
    exists?(path: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
    /** {@link ExecutionEnv.readLink} — a symlink's raw target, ONE hop, not followed. Absent whenever the
     *  env omits the primitive: a DANGLING symlink's real target is then unknowable to the hook. */
    readLink?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
    /**
     * {@link ExecutionEnv.cwd} — the ENVIRONMENT's own working directory, read live: the base the env's own
     * primitives resolve a relative path against, and the only anchor it reports about itself.
     *
     * NOT the task's tracked cwd, and the difference is load-bearing for anyone judging a RELATIVE path.
     * The engine keeps a separate per-task cwd — it starts at the resolved task root (which is
     * `deps.rootPath` when that is set, not necessarily the env's cwd) and moves with the shell's `cd` and
     * with worktree entry — and the TOOLS' relative paths resolve against THAT, while the environment is
     * never told it moved. So a hook that must decide a relative target the way the tool will interpret it
     * cannot take the base from here: judge absolute targets, or have the deployment hand the tracked cwd
     * to the hook through its own channel. What this member is good for is the env-side anchor — which
     * filesystem/mount the answers below are about.
     */
    cwd?(): string;
}
/**
 * Build the {@link HookEnvCapabilities} face for `env`. One wrapper per primitive (never a bound method of
 * the env, never the env itself), presence probed at build time so the face's shape reports what this env
 * can actually do, and frozen so it cannot be re-pointed by whoever receives it.
 *
 * Two details the "absent means cannot" contract rests on, both cheap and both load-bearing:
 *  · **null prototype, both ways.** Absence must be a fact about THIS object — on the way OUT (a face
 *    behind `Object.prototype` would answer `ctx.env.readLink` from there, turning "this env cannot read
 *    links" into "here is some function"; freezing the face does not freeze what it inherits) and on the
 *    way IN ({@link findEnvSlot} stops before `Object.prototype`, so an ambient property of that name is
 *    never adopted as a capability and never gets called with the env as its receiver).
 *  · **capture, do not re-read.** Each primitive is read ONCE, checked, and that captured callable is what
 *    the wrapper invokes — through module-load-captured intrinsics, never a build-time or call-time global
 *    lookup. Re-reading the property, or the machinery that does the reading, would let something other
 *    than what was validated end up running, or the receiver end up somewhere it was never handed.
 *    {@link createCwdReader} carries the one live read under the same rules.
 */
export declare function createHookEnvCapabilities(env: ExecutionEnv): HookEnvCapabilities;
/**
 * #281 件A — the UNIFIED IDENTITY ENVELOPE every hook invocation can carry (`ctx.identity`): which
 * run, which leg, and where in the delegation tree the hook is firing. The CC analog is
 * `createBaseHookInput`'s per-event base fields; the members here are sema's OWN identity facts
 * (session/leg/delegation axes), not a transcription of CC's field names.
 *
 * WHY: `deps.hooks` is PROCESS-level — one `stop`/`preToolUse`/`permissionDenied` seat serves the
 * root run, every delegated child and every resume leg at once, and before this envelope none of
 * those invocations could say which leg they belonged to. Additive: every seat is
 * `identity?: HookInvocationIdentity`, so a consumer that never reads it sees nothing new.
 *
 * PRESENCE LAW (which invocations carry it — stated here once, referenced per seat):
 *  · PRESENT on every ENGINE-driven invocation of every hook seat: the tool gate's PreToolUse
 *    screenings (phase 1 + the approval-edit re-screen), postToolUse / postToolUseFailure,
 *    postToolBatch, userPromptSubmit, stop, stopFailure, preCompact / postCompact (the Runner's
 *    wrapper injects it), and every engine `permissionDenied` emission (gate deny exit, hook-crash
 *    intercept, plan-mode deny, compliance deny). The engine mints ONE frozen envelope per prepared
 *    leg ({@link mintHookInvocationIdentity} — single mint home) and every station reuses it.
 *  · HONESTLY ABSENT (no fabrication) at exactly four places, each structurally unable to know:
 *     1. a HOST driving the exported {@link runToolGate} directly without supplying
 *        `ToolGateInput.identity` — the engine cannot know a leg it is not running;
 *     2. an inherited PreToolUse screening face riding a DESCENDANT's constraint fold
 *        ({@link createPreToolUseConstraintPolicy}) — the fold consumes it as a `ToolPolicy`, whose
 *        request shape carries no leg identity, and stamping the INSTALL-site leg would name the
 *        wrong run;
 *     3. a deployment driving the compaction module directly with its own `preCompact`/`postCompact`
 *        options — those contexts are built below the runner, which is where the envelope lives;
 *     4. the PRE-CAS durable-resume observation of a declared-observational PreToolUse face
 *        (resumeStream's edited-args leg) — it runs BEFORE the resume leg prepares, so the leg's
 *        envelope does not exist yet, and minting a partial one off the checkpoint would guess at
 *        the delegation axes (the row persists `isDelegatedChild` alone; `insideFork`/`agentName`/
 *        `parentToolCallId` are internals-only, and a wrong guess is worse than an honest
 *        absence). The SAME face is re-consulted with full identity by the resumed leg's own gate.
 *
 * CONSUMER NOTE — the minted object is FROZEN and carries a NULL PROTOTYPE (same rule and same
 * trade as {@link HookEnvCapabilities}: absence must be a fact about this object, not about what
 * `Object.prototype` happens to hold). Read members directly (`identity.agentName`), test presence
 * with `in` or `!== undefined`; do NOT call inherited methods on it — `identity.hasOwnProperty(...)`
 * or implicit string coercion would throw, exactly as on the env capability face.
 */
export interface HookInvocationIdentity {
    /** The run's session id (uuid domain) — the same key `TaskResult.sessionId` reports. */
    readonly sessionId: string;
    /** The run's unified task id (`spec.taskId ?? sessionId` — the value TaskEvent's `sourceTaskId`
     *  and `task_progress` frames key on). Equal to {@link sessionId} when no task id was declared. */
    readonly taskId: string;
    /** Which prepared leg this invocation belongs to — the wiring manifest's own `leg.kind`
     *  derivation, verbatim ({@link WiringLegKind}: `"root" | "child" | "resume"`). NOTE the fork
     *  lane's legs read `"child"` here like any other delegation; {@link insideFork} is the
     *  fork-lane discriminator. */
    readonly legKind: WiringLegKind;
    /** RB-204 delegation fact, verbatim: `true` iff this leg runs as a delegated child (every core
     *  spawn lane — sync/steer/background/fork/workflow — sets it; a top-level run reads `false`). */
    readonly isDelegatedChild: boolean;
    /** Present (`true`) iff this leg IS a forked child (`Agent(subagent_type:"fork")` — the
     *  design/110 trusted internals fact). Absent everywhere else. */
    readonly insideFork?: true;
    /** The delegated child's display name, when one was threaded at spawn (taskName / agent-type).
     *  UNTRUSTED display text (model-chosen) — already single-line-bounded at mint (same
     *  `inlineUntrusted` treatment as the `task_progress` name lane); never an identity key. */
    readonly agentName?: string;
    /** The EXPLICIT `Agent({name})` spawn identity, when the spawn carried one — sema's teammate key
     *  (validated at spawn: `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`, never `main`). The same string the
     *  child's roster row, its uplink `from` label and its `task_progress.spawnName` carry, so a hook
     *  seat binds "which teammate" by string equality. ABSENT = an unnamed delegation (not a
     *  teammate); {@link agentName} above stays the DISPLAY label and is never an identity key. */
    readonly spawnName?: string;
    /** The RESOLVED subagent type the leg was spawned as (`"general-purpose"` / `"fork"` / a roster
     *  definition's name — the fleet row's `agentType` word, never the description). Absent on a
     *  top-level run and on a leg whose spawner threaded no type (a workflow-lane agent). */
    readonly agentType?: string;
    /** The spawning Agent tool call's own id, when this leg runs under one — the delegation-chain
     *  position anchor ({@link import("./types.js").TaskEvent}'s `parentToolCallId` twin). Absent on
     *  a top-level run and on a directly-started workflow child (no launching tool call — no id is
     *  fabricated). */
    readonly parentToolCallId?: string;
}
/**
 * #281 件A — the ONE construction home of {@link HookInvocationIdentity} (domain-lexicon: one mint
 * site; the runner calls it once per prepared leg and every hook station reuses the same object).
 * Frozen AND null-prototype: the envelope is handed to arbitrarily many deployment callbacks, and a
 * hook mutating `ctx.identity` must not rewrite what a later hook (or the delegation-lifecycle
 * observer) reads. The null prototype is the {@link createHookEnvCapabilities} rule applied to the
 * other absence-signaling face this module mints — "absent member" must be a fact about THIS object,
 * and a frozen object with an ordinary prototype still answers `insideFork`/`agentName` reads (and
 * `in` probes) from `Object.prototype`, so a prototype write elsewhere in the process could hand
 * every leg a delegation axis the mint never stamped. `agentName` is sanitized here — it
 * is the only member whose value a model influences.
 */
export declare function mintHookInvocationIdentity(facts: HookInvocationIdentity): HookInvocationIdentity;
/** Identifying context passed to tool hooks. */
export interface HookToolContext {
    toolCallId: string;
    toolName: string;
    /** #281 件A — the run/leg identity envelope; see {@link HookInvocationIdentity} for the presence
     *  law (present on engine-driven gates; absent on a host-driven gate without
     *  `ToolGateInput.identity` and on the delegation-fold twin). */
    identity?: HookInvocationIdentity;
    /**
     * The read-only path-resolution face over the env the hands run against (see
     * {@link HookEnvCapabilities} for the why, and for why it is capabilities rather than the env object).
     *
     * ABSENT when the deployment wired no execution environment at all (no `executionEnv` /
     * `executionEnvFactory`: the run has no hands, and the engine's internal no-I/O placeholder is not an
     * environment to report). Present ⇒ inspect the individual members: each one is itself optional.
     */
    env?: HookEnvCapabilities;
    /**
     * The TASK's tracked working directory at this call — the base the hand tools will resolve a RELATIVE
     * path against, read live off the same ref the tool policy reads (so a hook and a policy judging the
     * same call judge it against the same base).
     *
     * It is NOT {@link HookEnvCapabilities.cwd}, and the difference is the reason this member exists: that
     * one is the ENVIRONMENT's own working directory, which never moves when the shell `cd`s or the run
     * enters a worktree. A hook is documented — on the capability face itself — as the seat that judges
     * "is this write inside the workspace", and it was the only seat in the gate that could not resolve a
     * relative target the way the tool would. CC ships its own `cwd` to every hook event for the same
     * reason; this is that field, named for the value it actually carries.
     *
     * ABSENT when the run has no tracked cwd to report — no real write hands (read-only or no shell), a
     * host driving {@link runToolGate} without one, or the delegation-fold twin (which runs inside a
     * DESCENDANT's policy fold and would otherwise report the install site's directory, i.e. the wrong
     * machine's answer). Absence means "this engine cannot tell you", never "the root".
     */
    cwd?: string;
    /** The leg's EFFECTIVE permission mode when the callback is invoked — the mode seat's `observed` read (it arms
     *  no classifier on the leg or an ancestor, so it moves no decision); a steer, a plan-mode entry or exit and a
     *  resumed leg's restored word show on the next callback. Stamped by the Runner on its own legs' contexts (this
     *  one, {@link PermissionDeniedPayload}, {@link UserPromptSubmitContext}, {@link PostToolBatchContext},
     *  {@link StopHookContext}); ABSENT ⇔ no leg seat: a host-driven {@link runToolGate} (its
     *  `ToolGateInput.permissionMode` is the arbitration's reader, never called for this), the delegation-fold twin,
     *  the `"resume_message"` screen (before the resumed leg exists). Not a version signal. The compaction seats and
     *  {@link StopFailureContext} carry none (the reference's hook input omits it there). */
    permissionMode?: PermissionMode;
    /** {@link HookSeatSignal} — this invocation's own abort signal. A FRESH one per screening pass, like
     *  the context object it rides: phase 1 and the approval-edit re-screen are two bounded waits, and a
     *  shared signal would let the first pass's expiry cancel the second. */
    signal?: AbortSignal;
}
/** The executed tool result a PostToolUse hook inspects. */
export interface HookToolOutput {
    content: Array<TextContent | ImageContent | DocumentContent>;
    details: unknown;
    isError: boolean;
}
/**
 * A PreToolUse hook result: a {@link PermissionResult} (so a hook can `deny`/`ask`, or `allow` with an
 * `updatedInput` rewrite) plus optional `additionalContext` injected into the eventual tool result.
 *
 * **The provenance members are not hook-settable inputs** (#174). `decisionReason` is the ENGINE's account
 * of which layer produced a verdict, and `allowRuleShadowed` is the allow layer's own disclosure that a
 * member matched without clearing the ask. The gate stamps both itself — a `decisionReason` on a hook
 * result is DISCARDED (a hook ask always reads `"hook"` downstream), and an `allowRuleShadowed` is
 * dropped rather than rendered on an approval card as an entry no member matched.
 *
 * Which of a hook's OTHER members reach the surviving decision depends on which fold arm ran, so read
 * the arm, not a single list: an ask promoted from a policy `allow` is rebuilt from the hook's `message`,
 * `requiresRealApproval` and `updatedInput`, while a hook ask folded BESIDE a policy ask contributes its
 * message and its mandate to the policy's decision and leaves the policy's own rewrite in place (the
 * hook's rewrite still reaches execution — through the gate's captured rewrite, not through the decision
 * object). Both arms are at the `hookAsk` fold in {@link runToolGate}.
 *
 * `additionalContext` is on neither list and never was consumed as a decision member: it rides its own
 * channel — collected into the gate's `preToolContext` as soon as the hook answers, whichever verdict it
 * returned, and drained onto the tool result by the runner. DELIVERY is narrower than collection: the
 * runner stashes the context only for a call that will EXECUTE, because a blocked or suspended call never
 * reaches the result step that drains it and carries its own model-facing reason instead. The context
 * formerly APPEARED on the promoted ask object as an undeclared property, because that fold spread the
 * whole hook result; the concurrent arm never carried it, so the two arms disagreed on a member neither
 * of them reads. Nothing in the engine reads it off a decision.
 */
export type PreToolUseResult = PermissionResult & {
    additionalContext?: string;
};
/** A PostToolUse hook result: replace the tool output and/or append context (both optional). */
export interface PostToolUseResult {
    /** If provided, replaces the tool result content array in full. */
    updatedOutput?: Array<TextContent | ImageContent | DocumentContent>;
    /** Appended to the result as a `<system-reminder>` the model can read. */
    additionalContext?: string;
}
/** #281 件A — the context of a {@link Hooks.userPromptSubmit} invocation (second parameter,
 *  additive). An object so later additions stay additive; design/373 §4.3 (D2) grew the
 *  entrance-discrimination trio below. */
export interface UserPromptSubmitContext {
    /** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
    identity?: HookInvocationIdentity;
    /** {@link HookToolContext.permissionMode} (absent on `"resume_message"`, screened before the leg exists). */
    permissionMode?: PermissionMode;
    /** {@link HookSeatSignal} — this invocation's own abort signal. */
    signal?: AbortSignal;
    /**
     * design/373 §4.3 (D2) — WHICH user-lane entrance this text is entering through. The screen now
     * sits at every user-lane entrance, so a hook that used to see only the objective can (and, if it
     * blocks unconditionally, MUST) branch on this: `"objective"` = the task's opening prompt (the
     * pre-373 only caller); `"steer"` = a live mid-run TaskStream.steer; `"resume_message"` = the
     * message riding a wake resume (screened at the verb, pre-CAS); `"parked_redelivery"` = a parked
     * steer being re-screened on the CURRENT process's hook when a resume redelivers it — ANY resume
     * kind that drains parked steers (policy_ask / dry_run_review / plan_review / resource_limit,
     * wake included), because a park's screening cannot be carried across processes (hooks are
     * process-local config). ⚠️ DOUBLE ARRIVAL IS CONTRACTUAL:
     * the same input can reach a hook twice with different sources (screened live as `"steer"`,
     * parked, re-screened as `"parked_redelivery"`); idempotency is the hook's to hold — a stateful
     * hook keys on {@link inputId}. `"followUp"` is a RESERVED spelling (spec seat only): the Runner
     * face has no user-lane followUp verb today, so no invocation carries it yet — if/when that face
     * lands it arrives screened under this name (never a silent widening).
     * Absent ⇒ an emission from a pre-373 engine (old engines stamped nothing); every current engine
     * seat stamps its name, so treat absence as "version unknown", never as a sixth entrance.
     */
    source?: "objective" | "steer" | "resume_message" | "followUp" | "parked_redelivery";
    /**
     * design/373 §4.3 (D2) — the screened input's correlation id. Present on EVERY entrance except
     * the objective seat (no id exists on that face — nothing is fabricated there):
     *  - `"steer"` — the effective `inputId` (caller-supplied or engine-minted — the SAME id the
     *    `human_input` event and any parked copy carry);
     *  - `"parked_redelivery"` — the parked entry's STORED id (stable across redeliveries);
     *  - `"resume_message"` — always present too: the caller's id verbatim when it supplied one,
     *    otherwise MINTED PER CALL by the wake validator. ⚠️ Per call, not per message: a retried wake
     *    that supplies no caller id and REACHES this seat re-screens identical text under a FRESH id.
     *    A dedup/quota hook keying its ledger here gets cross-attempt stability only when the CALLER
     *    supplies the id — a host that retries wakes should mint its own. What the engine guarantees
     *    instead (#472) is that the seat is not reached needlessly: it sits BELOW every retryable
     *    pre-CAS refusal on the resume ladder, so an attempt the engine was going to refuse anyway
     *    (a full governance window most of all) costs ZERO invocations — the retry loop that refusal
     *    invites cannot bill this hook per attempt.
     * This is the dedup key for the double-arrival contract on {@link source}: a quota-charging hook
     * that must not double-charge one input keys its ledger here.
     */
    inputId?: string;
    /**
     * design/373 §4.3 (D2) — the API-boundary actor snapshot attributed to this input (the same
     * snapshot the `human_input` event reports), when the caller supplied one. Attribution only,
     * never authority — a hook wanting to exempt its own supervisor lane branches on this PLUS
     * {@link source}, and absence means "anonymous caller", exactly as on the event face.
     */
    actor?: ActorAssertion;
}
/** #281 件A — the context of a {@link Hooks.postToolBatch} invocation (third parameter, additive —
 *  deliberately NOT folded into `meta`, whose presence already means "the engine injected here"). */
export interface PostToolBatchContext {
    /** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
    identity?: HookInvocationIdentity;
    /** {@link HookToolContext.permissionMode}. */
    permissionMode?: PermissionMode;
    /** {@link HookSeatSignal} — this invocation's own abort signal. */
    signal?: AbortSignal;
}
/** A UserPromptSubmit hook result: block the submission, or inject context ahead of the prompt. */
export interface UserPromptSubmitResult {
    /** Block submission entirely; the task fails with this model-readable reason. */
    block?: string;
    /** Injected ahead of the user's prompt (wrapped as a `<system-reminder>`). */
    additionalContext?: string;
}
/** The outcome of the two-phase tool gate, mapped onto the harness `tool_call` hook return shape. */
export interface ToolGateResult {
    /** Block execution (the loop emits an error tool result with `reason`). */
    block?: boolean;
    /** Model-readable block reason (already `<system-reminder>`-wrapped). */
    reason?: string;
    /**
     * The record of this pass ({@link GateOutcome}): its final disposition, the ask it settled (if any) and
     * that ask's origin. Present on BOTH terminal exits, the block and the allow — an approved call is a
     * settled approval too, and a caller that saw the record only on refusals would read its absence on an
     * execution as "nobody approved this". Absent only on the durable-suspend exit, where the pass decided
     * nothing yet. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
     * frame, and the deny observer receives the same object: one mint, three faces.
     */
    gate?: GateOutcome;
    /** Rewritten args to execute with (re-validated by the loop); omitted when nothing rewrote. */
    updatedInput?: unknown;
    /** A blocking refusal's structured details, when the refusing station minted any (the shell read station's
     *  `read_path_denied` / `readonly_out_of_root` cards): the loop attaches them to the error result it mints
     *  from `reason`, so the refusal reaches `tool_end.structured` / `errorCode` exactly as the tool's own
     *  refusal of the same finding did. Absent on every other block. */
    details?: unknown;
    /**
     * design/45: the gate routed a policy `ask` to a **durable suspension** (no synchronous approver).
     * `suspendAsk` already persisted the checkpoint and aborted the run; the caller records this so the
     * task assembles as `status:"suspended"` with the token. Suspend lives ONLY here in the gate's `ask`
     * branch — it never becomes a `PermissionResult` action (policy stays pure allow/ask/deny, §15.2).
     */
    suspend?: {
        token: import("./checkpoint-store.js").CheckpointToken;
        gate: import("./checkpoint-store.js").CheckpointGate;
    };
    /** PreToolUse `additionalContext` to append to this call's eventual tool result (correlated by id). */
    preToolContext: string[];
}
/**
 * The gate's content-ask routing outcome — deliberately a channel of its OWN, not a
 * field on the permission verdict. A permission verdict is folded, re-adjudicated and re-presented by
 * layers that know nothing about answers; an answer riding one of those would be dropped by the fold or
 * carried into a second adjudication. Three arms, one per thing that can happen:
 *  · `answered` — a person answered. The gate lets the call execute and the answer is delivered to the
 *    tool through the run's own one-shot binding (the gate never carries the answer itself).
 *  · `unavailable` — nobody was reachable and NOTHING was shown (the {@link
 *    import("./ask-question.js").QuestionUnavailable} contract). Only this arm may be re-routed — the
 *    gate parks the question for a later human, or answers the honest "it was NOT shown" refusal.
 *  · `delivery_failure` — delivery was attempted and failed. NEVER re-routed: the question may already
 *    have been rendered to a person, so parking it would put one question in front of two deciders. It
 *    is reported to the model through the tool's own coded-failure / degrade exits.
 */
export type ContentAskOutcome = {
    kind: "answered";
    /** The EXACT batch that was put in front of the person, retained by the resolver as an isolated
     *  snapshot. The gate executes this and nothing else, so "what was shown" and "what runs" are the
     *  same object graph by construction rather than by two parties agreeing not to mutate one. */
    presentedInput: unknown;
} | {
    kind: "unavailable";
    /** The retained snapshot, when the resolver got far enough to make one (i.e. it actually waited
     *  on a callback). The park persists THIS, not the caller's live object: the wait is a window in
     *  which whoever owns the rewrite can change it, and a checkpoint holding bytes the gate never
     *  adjudicated is one a later human would answer as a different question. Absent when no wait
     *  happened, in which case there is no window and the live object is still the validated one. */
    presentedInput?: unknown;
    /** True iff the park closure took its SYNC-FIRST DECLINE for this call — it has not yet made a
     *  real suspend attempt, so re-entering it now IS that attempt rather than a second one. False
     *  when the park already had its attempt (no live face to prefer, or a durable mandate): the gate
     *  must not re-enter, because a suspend and a checkpoint write are not idempotent, and a backend
     *  that committed before surfacing a transport error would be left an orphan row under a second
     *  token. Exactly one real attempt per call, on every path. */
    parkDeclined: boolean;
} | {
    kind: "delivery_failure";
    code: string;
    /** The retained snapshot, when the resolver got far enough to make one. Executed for the same
     *  reason the answered arm's is: the bound failure names the batch it belongs to, and executing a
     *  different one would drop it and re-ask the person. Absent when the refusal happened before a
     *  snapshot could be taken. */
    presentedInput?: unknown;
};
/**
 * A durable park that was ATTEMPTED for this call and could not be minted, carrying the reason in the
 * words of whatever refused it. Distinct from the `undefined` return, which means "no park attempt
 * happened" (no facility armed, or the closure took its sync-first decline) — that case hides nothing,
 * while this one does: the gate falls back to the synchronous approver chain either way (the
 * compensation contract — an uncommitted checkpoint is a safe fallback — is unchanged), but on a
 * headless deployment that fallback is the auto-deny whose text is "no approver is wired". True as far
 * as it goes, and the whole reason the call did not park lives somewhere else entirely (the host's
 * `onError` face). So the reason travels with the fall-through and the deny quotes it.
 */
export interface ParkAttemptFailed {
    /** Why the park could not be minted — a sentence, already worded for a reader. */
    parkFailed: string;
}
/**
 * design/182 §7 — what the ORG layer answers for ONE call (see `ToolGateInput.orgRules`).
 *
 * The two arms are distinguished by `status`, NEVER by the absence of a verdict: "the org published no
 * rule for this call" and "this deployment cannot see the org's rules" are opposite facts, and reading
 * an empty answer as the first one is exactly the governance fail-open the availability contract exists
 * to prevent.
 */
export type OrgGateVerdict = {
    status: "available";
    /** The org rule that speaks for this call, if any. Deny outranks ask; there is no allow bucket. An
     *  `unreadable` member (the lexer's reason) says the call could not be READ against `rule` — an
     *  org rule that may reach it — so `behavior` is `ask` and the message names the reason. */
    verdict?: {
        behavior: "deny" | "ask";
        rule: string;
        unreadable?: string;
    };
    /** design/252 G-2 (additive): the REVISION of the snapshot this answer was read off — the
     *  published-policy version an auditor reconciles the resulting decision against. Present on the
     *  available arm whether or not a rule spoke: "revision 41 published nothing about this call" is
     *  itself the audit answer, and it is a different fact from "we never looked". ABSENT ⇒ the lane
     *  did not report one (a foreign overlay, or a revision that failed its own domain screen — the
     *  evidence records that as `"not_reported"` rather than inventing a number). Never adjudication
     *  input: the verdict above is the whole decision, and dropping this field would leave every
     *  outcome byte-identical. */
    revision?: number;
} | {
    status: "unavailable";
    /** The overlay's own disclosure lines (engine/deployment-authored), for the operator channel. */
    disclosures: readonly string[];
};
/** One member of a {@link PersistedRuleHit} coverage set: a rule's canonical text plus (optionally)
 *  its add-dot identity. */
export interface PersistedRuleHitRule {
    /** The canonical text of one admitting rule. */
    readonly rule: string;
    /** The rule's LIVE add dots (tombstoned adds already filtered by the store's read). Omitted by a
     *  lane that has the text but not the dots; the evidence then reports `"not_reported"`. */
    readonly dots?: readonly import("./permission-rule-model.js").RuleDot[];
}
/**
 * design/252 G-2, re-formed by design/375 and again by the three-behavior model (BREAKING) — the
 * structured answer a persisted-rule lane gives: WHICH behavior spoke for the call, and the rule set it
 * stands on. For an `allow` the set is the COVERAGE SET that admits the call — a single rule admitting
 * the whole command is a one-element set (the historical case); a compound admitted by the conjunction
 * of per-segment rules (design/375 §5.1) is the set of those rules, in segment order. For a `deny` or an
 * `ask` the set is the one rule of that behavior that reaches the call (the lane's precedence walk —
 * deny > ask > allow — already chose it). `rules` is non-empty by contract — an empty set covers nothing
 * and is not a hit (the normalizer reads it as a clean negative).
 *
 * Why dots and not an id: a persisted rule HAS no scalar id — it is a (behavior, text, scope) group whose
 * identity is the set of add dots that redeemed it, and two concurrently approved records for one text
 * each carry their own. The set is the identity; a joined string would not be one.
 *
 * The pre-375 single-rule object shape (`{ rule, dots? }`) and the pre-behavior `{ rules }` shape are
 * NOT accepted: on this loosening seam an unrecognized answer degrades to a clean negative (more asks),
 * never to a match — the safe failure shape for a lane that missed the same-window upgrade.
 */
export interface PersistedRuleHit {
    /** What the deciding rule(s) say about the call — the closed set's word. */
    readonly behavior: import("./permission-rule-model.js").RuleBehavior;
    /** The rule set the verdict stands on. Non-empty. */
    readonly rules: readonly PersistedRuleHitRule[];
}
/**
 * design/252 G-2 — the answer a lane gives when it could not READ its store (no `reason`; the CALL form is `reason`'s).
 *
 * The store form's DECISION meaning is identical to "no rule": the store's rows — allow, ask and deny alike — take
 * no part in this call's decision (skipped, the way a settings file with errors is skipped whole; the lane announces
 * it, and the rows apply again once a read succeeds). What it changes is the RECORD: without it, an unreadable store
 * and a store that holds no matching rule are the same answer (`undefined`), and the ask's evidence would report one
 * as the other — `"no_match"` ("we looked and found nothing") vs `"unavailable"` ("we could not look").
 */
export interface PersistedRuleUnreadable {
    readonly unreadable: true;
    /** WHY the lane could not read: absent for a store read failure (its rows take no part); for a CALL the lane could
     *  not read against the person's standing deny/ask rows (an expansion where a rule reads a word, an unterminated quote,
     *  a syntax error — `programRunReachOf`'s `unreadable` word), the lexer's reason — the call then fails closed: an allow
     *  tightens to a real-approval ask of origin `rule_store_unavailable`, the reason in its message. */
    readonly reason?: string;
}
/**
 * design/375 §5.2② — the CLEAN NEGATIVE that still carries the per-segment coverage table: no rule
 * admits the whole call, and THESE segments are the ones an eligible rule already covers. Minted by
 * the lane from the SAME store read that adjudicated the call (snapshot = adjudication snapshot, no
 * second read), and consumed only by the suggestion face — which puts on the batch offer exactly the
 * segments this table calls uncovered. Its DECISION meaning is identical to "no rule": the seat is
 * advisory, and a lane that omits it (or an unreadable one) degrades to all-uncovered, which merely
 * over-offers (every offered rule still passes the validator at redemption).
 */
export interface PersistedRuleCoverage {
    readonly segmentCoverage: readonly import("./permission-rule-model.js").SegmentCoverage[];
}
/** Every shape a lane may answer with. The retired bare-string form (a single allow rule's text) is read
 *  by the normalizer as a clean negative, like every other unrecognized shape. */
export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | PersistedRuleUnreadable | undefined;
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
 *  signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
 *  additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
 *  interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, AllowEntryGateSeats {
    /** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
     *  isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
     *  wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
    onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
    /** The call. `toolName` is the name every policy, hook and name-keyed table of this gate reads — the wire name of the
     *  tool the call dispatches to: the Runner's station resolves a call made under an alias through the roster
     *  (`rosterToolName`) before this gate runs; a host driving the gate resolves it the same way. */
    event: {
        toolCallId: string;
        toolName: string;
        input: Record<string, unknown>;
    };
    /** #281 件A — the run/leg identity envelope, put verbatim on every {@link HookToolContext} this
     *  gate call builds and on every {@link PermissionDeniedPayload} it emits. The Runner supplies its
     *  per-leg mint; a host driving the gate directly may omit it, and the contexts then carry no
     *  identity (honest absence — this layer never fabricates a leg). */
    identity?: HookInvocationIdentity;
    /** design/319 (A ticket) — the run's reminder provenance mark: the gate's own model-facing deny
     *  reasons ride `formatHookFeedback`, so their open tags must carry the same mark the run's
     *  system-prompt declaration names. Absent (a host driving the gate directly) ⇒ bare tags. */
    reminderMark?: string;
    preToolUse?: Hooks["preToolUse"];
    /**
     * The bound the {@link preToolUse} screenings run under — {@link Hooks.timeoutMs}, carried in from
     * the hook record the caller resolved it off (the gate receives the callback, not the record). Absent
     * ⇒ {@link DEFAULT_HOOK_TIMEOUT_MS}. A garbage value is refused loudly to that default via
     * {@link onHookError}, exactly as {@link approvalTimeoutMs} is.
     *
     * On elapse the screening FAILS CLOSED — the call does not execute — and the block reason names the
     * hook and the bound. That direction is not a new posture for this seat: a hook that THROWS is
     * already the fail-closed deny here, and a hook that never answers has told the gate strictly less
     * than one that crashed. A face the deployment declared observational is not exempt, for the same
     * reason its throw is not: the declaration says its VERDICTS do not count, and a wait that never
     * ends is not a verdict.
     */
    hookTimeoutMs?: number;
    /** The task's live tracked cwd reader, put on every {@link HookToolContext} this gate call builds
     *  (see {@link HookToolContext.cwd}). A FUNCTION rather than a string for the reason the policy's own
     *  threading gives: capturing the value when the gate's wiring is built would freeze the task's
     *  starting directory and answer for a run that has since `cd`ed. Absent ⇒ the contexts carry no
     *  `cwd`, which is the honest answer for a run with no tracked one. */
    trackedCwd?: () => string | undefined;
    /** The read-only env capability face put on every {@link HookToolContext} this gate call
     *  builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
     *  passed through unchanged; omitted when the deployment wired no execution environment. */
    hookEnv?: HookEnvCapabilities;
    /**
     * design/388 B6 — the leg's live roster lookup: the face of the mounted tool a call dispatches to, stamped
     * on the {@link ToolCallRequest} every policy sees (read at the gate entry, so a run-time roster change is
     * honoured on the next call). Absent ⇒ requests carry no face (a bare producer).
     */
    toolFaceOf?: (toolName: string) => ToolCallFace | undefined;
    /**
     * design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
     * own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
     * gate itself never awaits on it — it hands the signal verbatim to the four consuming seats
     * below: `adjudicate`, `resolveAsk` and `resolveContentAsk` take it as their optional trailing
     * argument; `suspendAsk` (slice 2) reads it as `carry.signal` inside its structural seat
     * ({@link AskCarry}), and its park closure composes it into the halt×park fence. The Runner's
     * closures compose it with the run signal so a turn interrupt (bare halt / steer-now) releases
     * the wait — or, on the durable leg, makes the park concede. Absent (a host driving the gate
     * directly, or an unthreaded emit) ⇒ the three synchronous seats are invoked exactly as before —
     * same arity, no trailing `undefined` (the threading is a conditional invocation) — the carry has
     * no `signal` member, and waits stay bound to the run signal alone: the historical shape.
     */
    callSignal?: AbortSignal;
    /** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
     *  The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
     *  through verbatim; an implementation that ignores it keeps the run-level binding. */
    adjudicate?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>;
    /** Resolve an `ask` to allow/deny via `onAsk` (abort-bound). Required iff a decision can be `ask`.
     *  G1 three-value: the result may carry `approverUnavailable` — the live approver affirmed no
     *  operator is reachable for THIS ask; the gate then re-routes onto the durable park leg (below),
     *  keeping the carried fail-closed deny when no park is armed. The optional trailing `callSignal`
     *  (design/384) is {@link ToolGateInput.callSignal}, passed through verbatim. */
    resolveAsk: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
    /**
     * design/45: route a policy `ask` to a **durable suspension** instead of the synchronous `resolveAsk`
     * (onAsk) path. Called in the `ask` branch with the FINAL post-hook args; if it returns a suspend
     * directive it has already persisted the checkpoint + aborted the run, and the gate short-circuits to
     * `{ suspend }`. Returns `undefined` to fall through to `resolveAsk` (non-durable / declined). A
     * {@link ParkAttemptFailed} ALSO falls through to `resolveAsk` — same fallback, same compensation —
     * but carries why the mint failed so a resulting deny can name it instead of leaving the model and
     * the operator with the fallback's own reason as if it were the cause.
     * Omitted entirely → no durable mode (the 1.63 synchronous path, unchanged).
     *
     * The armed closure itself may also DECLINE (return `undefined` up front) to prefer the live
     * synchronous approver: the Runner's closure does so for a plain AND for a SAFETY ask whenever the
     * resolved seat is a FUNCTION-valued `onAsk` — even under `durableApproval`, which is the FALLBACK for
     * when no live approver is present, not an "always park" mandate — an interactive per-edit ask must
     * resolve in-stream, not cost a park/rebuild round-trip. The gate stays a pure fall-through consumer of
     * that choice. What KEEPS the durable leg is no function seat (absent, or a blanket `"allow"`/`"deny"`
     * string mode — a policy setting is not a live approver), a control-plane `forceDurableGate`, a live
     * face that already answered `"unavailable"` for this ask, or an inherited-unavailable marked call.
     * `safety` no longer participates in the park/sync decision at all — it only selects the gate KIND
     * (`irreversible_ask`) once a park is minted.
     */
    suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, 
    /** design/80 D-2: present (non-undefined) ONLY when this ask is a SAFETY tighten (the egress/
     *  irreversibility gate tightened a surviving `allow` to `ask`, `decisionReason === "safety"`). Drives the
     *  mint-site gate-kind choice — a safety tighten mints `irreversible_ask` (non-budgetable) EVEN WHEN
     *  `durableApproval` is wired. The object records which axis(es) tightened (persisted as the gate's
     *  `safetyAxis`). Absent for a plain policy/hook `ask` (→ the normal `human`/`irreversible_ask` choice). */
    safety?: import("./checkpoint-store.js").SafetyAxis, 
    /** The three-valued live-face verdict, widened by design/174 to cover BOTH live faces: true ⇒ the live face this
     *  ask would have been routed to already answered `"unavailable"` for it — the permission approver for
     *  a permission ask, the question face for a content ask. Either way there is no live leg left to
     *  prefer, so the closure must NOT take its sync-first decline and should park durably. All other
     *  decline/pre-commit-failure paths keep their existing `undefined` fallbacks (the gate then keeps the
     *  fail-closed deny / typed refusal the caller carries). */
    liveFaceUnavailable?: boolean, 
    /** Present ⇒ the surviving ask carries `requiresRealApproval` and the mint must escalate to
     *  the non-budgetable `irreversible_ask` kind carrying this bit (a budget resolver must never
     *  auto-approve what only judgment may clear). `origin` records whether the bit came from an org
     *  ASK rule, from the org-unavailable tighten (whose resume semantics differ — see
     *  {@link import("./checkpoint-store.js").RealApprovalGateBit}), or from a policy/hook. */
    realApproval?: import("./checkpoint-store.js").RealApprovalGateBit, 
    /** The allow-layer members that matched and could not clear (the surviving ask's `allowRuleShadowed`)
     *  — threaded so the park mint's risk descriptor carries the disclosure on the durable route. */
    allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[], 
    /** The surviving ask's `decisionReason` — threaded so the park mint's rule-suggestion door can
     *  read the ask's origin on the durable route exactly as the synchronous mints read it off the
     *  decision (a hook-raised ask is one no persisted rule may clear, so its parked card must not
     *  offer to mint one). */
    askDecisionReason?: DecisionReason, 
    /** The reversibility probe's cause for the tighten (the surviving ask's `probeReason`)
     *  — threaded so the park mint's risk descriptor carries on the durable route the same account of
     *  WHY the call was gated that the synchronous card gets. Already neutralized + capped at the gate. */
    probeReason?: string, 
    /** The STRUCTURED cause (the surviving ask's `probeCause`) — same durable-route errand
     *  as `probeReason`, already validated at the gate. */
    probeCause?: import("./checkpoint-store.js").ProbeCause, 
    /** design/375 §5.2②: the surviving ask's engine-stamped `segmentCoverage` — threaded so the park
     *  mint's offer factory reads the same coverage snapshot on the durable route as the synchronous
     *  mints read off the decision (absent ⇒ all segments uncovered ⇒ over-offer, the safe way). */
    segmentCoverage?: readonly import("./permission-rule-model.js").SegmentCoverage[], 
    /** #457 ④: the surviving ask's `matchedAskRule` — threaded for the SAME errand as
     *  `askDecisionReason`: the park mint's rule-suggestion door must know that an EXPLICIT `ask` rule
     *  matched this call, because the persisted-rule lane refuses to clear such an ask (the
     *  `decision.matchedAskRule === undefined` conjunct on its allow arm), so the parked card must not
     *  offer to mint a rule that would never silence it. APPENDED at the tail deliberately: every
     *  parameter here is positional and `probeReason` is also a `string`, so inserting this beside its
     *  logical neighbour would silently re-bind three existing call sites' arguments with no type
     *  error to catch it. */
    matchedAskRule?: string, 
    /** The surviving ask's engine-stamped `probeMandated` — threaded for the SAME errand as
     *  `askDecisionReason` and `matchedAskRule`: the park mint's rule-suggestion door must know that
     *  this ask is one no persisted rule may clear, or the parked card offers a "stop asking me this"
     *  rule the lane would then refuse to honor. APPENDED at the tail for the reason its neighbour
     *  states — every parameter here is positional — and typed `boolean` so a mis-bind against the two
     *  trailing `string` seats is a compile error rather than a silent re-binding. */
    probeMandated?: boolean, 
    /** The ONE structural seat after the twelve positional ones — everything else the park closure needs
     *  travels inside it ({@link AskCarry}: the per-call signal, the surviving ask's fallback member, its
     *  origin word), so the tuple does not grow a seat per member. ALWAYS passed by the gate (thirteen
     *  arguments, every call) — optional on the TYPE only because the ten seats before it are optional and
     *  a required parameter cannot follow one. The twelve seats above are the historical shape; a host-built
     *  closure that read the signal as the 13th positional argument now reads `carry.signal`. */
    carry?: AskCarry) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
    /**
     * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
     * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
     * post-policy request (the same args that will execute), and it owns everything about delivery:
     * validating the batch before anything is put in front of a person, calling the live face, and binding
     * the answer to this call id for the tool to pick up. It returns only WHICH of the three outcomes
     * happened — never the answer, which travels the run's own binding.
     *
     * CONTRACT: it must not throw (a delivery failure is a returned value, so the gate can tell "failed"
     * from "nobody home" — the two have opposite re-routing rules). Passed UNCONDITIONALLY by the runner,
     * including on runs with no live face: it then answers `unavailable` without contacting anything, which
     * is exactly the pre-174 park/refusal behavior. Absent entirely ⇒ the pre-174 path verbatim.
     * The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
     * through verbatim; an implementation that ignores it keeps the run-level binding.
     */
    resolveContentAsk?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ContentAskOutcome>;
    /**
     * design/70: the called tool is egress-marked (`ToolSpec.egress` — an external write: open a PR,
     * push, send). The gate tightens a surviving `allow` to `ask` so an egress tool is NEVER
     * auto-allowed — execution always passes an explicit ask-resolution (onAsk / durable suspend);
     * headless resolves to deny. Filled by the caller from the tool's spec; policies stay egress-blind.
     */
    egress?: boolean;
    /**
     * design/77 §4 (Gate 4): the called tool's irreversibility tier (`ToolSpec.irreversibility`), filled by
     * the caller from the tool's spec. After the egress tighten, the gate runs a SECOND deterministic tighten:
     * `"always"` tightens a surviving `allow` to `ask`; `"maybe"` calls {@link reversibilityProbe} and tightens
     * UNLESS the probe reports reversible (fail-closed on timeout/throw); `"never"`/undefined → untouched.
     * Policies stay irreversibility-blind; the gate is the single chokepoint.
     */
    irreversibility?: "never" | "maybe" | "always";
    /**
     * design/77 §4: the `"maybe"`-tier probe from the tool's spec (read at prepare-time, captured here — not a
     * tool argument). Called when `irreversibility === "maybe"` && the surviving decision is `allow` (the
     * TIGHTEN arm) — and, since #502, when it is already `ask` (the MANDATE arm: the probe is the only
     * source of `mandated`, and an ask another layer raised needs it too; there the `reversible` verdict is
     * discarded, since a probe may never un-ask another layer's question). Both arms are time-bounded by
     * {@link approvalTimeoutMs} (30s default). Fail-closed: anything other than `{ reversible: true }`
     * (including a timeout or a throw) tightens to `ask`. A probe is never trusted to AUTO-ALLOW past the gate.
     * The invocation set is therefore WIDER than the tighten's: an ask-first policy calls this on every
     * `"maybe"`-tier call, and a throwing/hanging probe costs an `onError(phase:"hook")` and the wait there
     * too — see `ToolSpec.reversibilityProbe` for the deployment-facing statement of that cost.
     *
     * An optional `reason` on a TIGHTENING verdict names the cause. The gate neutralizes + caps it and
     * carries it onto both approval routes (`AskRequest.probeReason`, `RiskDescriptor.probeReason`) — see
     * `ToolSpec.reversibilityProbe`. Display/triage only: `reversible` alone decides the verdict, so a
     * probe cannot widen anything through this member.
     */
    reversibilityProbe?: (args: unknown) => import("./types.js").ReversibilityVerdict | Promise<import("./types.js").ReversibilityVerdict>;
    /** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`.
     *  ABSENT ⇒ a bounded default applies (30s — the probe wait is never unbounded, even with no
     *  {@link abortSignal}); a non-finite/negative value is refused loudly (via {@link onHookError}) to
     *  that same default, never silently reinterpreted. `0` is honored as written (immediate deadline). */
    approvalTimeoutMs?: number;
    /** design/77 §4: the task abort signal — bounds {@link reversibilityProbe} by the task's real deadline
     *  (timeout/cancel) in addition to {@link approvalTimeoutMs}; an abort while probing fails closed to `ask`. */
    abortSignal?: AbortSignal;
    /**
     * design/134 R5: observer for the gate's DENY short-circuit ({@link Hooks.permissionDenied}), pre-wrapped
     * by the caller (swallow + onError) so it never throws. Fired ONCE, at the single deny exit of the
     * adjudicate chain — a policy deny or a resolved-ask deny. NOT fired for a PreToolUse hook deny (the
     * exclusion on {@link Hooks.permissionDenied}, which also records why that exclusion is not a parity
     * claim) or a durable suspend (the ask path). runToolGate still try/catches defensively: an observer
     * must never alter the deny outcome.
     *
     * ALSO fired for the engine's fail-closed interception of a THROWING PreToolUse hook, with
     * `source:"hook"` — the exclusion is the hook's own deny DECISION, not an engine decision.
     */
    permissionDenied?: (payload: PermissionDeniedPayload) => void | Promise<void>;
    /**
     * Trace sink for a PreToolUse hook that THREW — the deployment-facing half of the crash
     * (`onError(phase:"hook")`, wired by the caller). The model-facing half is the fail-closed block reason;
     * this one carries the exception object itself to whoever runs the deployment, because a crashing hook is
     * a bug someone has to fix and the model-facing summary is bounded/sanitized. Never affects the outcome
     * (a throwing sink is swallowed).
     *
     * ALSO fired for a {@link reversibilityProbe} that threw or timed out (same species — a
     * deployment-supplied callback failing while the gate holds the fail-closed line) and for a
     * malformed {@link approvalTimeoutMs} refused to the bounded default. A task-abort rejection
     * mid-probe is NOT reported (normal cancellation, not a defect).
     */
    onHookError?: (err: unknown) => void;
    /**
     * design/134 R5: true when this tool's `irreversibility` tier was installed by the ENGINE'S SHELL SEAT
     * (the read-boundary judge on every shell face, plus the shape tier on every full shell and the
     * `TaskSpec.shellGate: "always"` floor — design/80 D-E; the seat's `shellGatedTools` membership), not an explicit per-tool mark —
     * attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
     */
    shellGated?: boolean;
    /**
     * design/276 (CC 2.1.233 P11 arm 4): true when the called tool is the peer-message verb — a message
     * to another agent (`SendMessage`). ENGINE-FILLED from the wire name at the caller, never a
     * configuration knob; a caller's same-named shadow tool is keyed too (for a tighten, over-asking is
     * the fail-safe direction). Under an armed {@link autoMode} the gate tightens a surviving `allow` to
     * `ask` (the peer-referral tighten below) so the message passes the classifier's eye — its prompt
     * already carries the Multi-Agent Coordination exemption, so the ordinary teammate message resolves
     * allow and the injection-shaped one is what this member exists to catch. Absent/false, or with no
     * armed auto mode, the decision path is byte-identical. Per-run semantic: armed by THIS run's
     * resolved caps, never inherited down the delegation chain (CC parity — the mode predicate reads the
     * SENDING session's own mode).
     */
    peerMessage?: boolean;
    /**
     * The write-protection judge — ENGINE-BUILT from the deployment's table seat by `createWriteProtectionCheck`
     * (write-protect.ts), never a deployment callback (the deployment authors table ROWS, validated loudly at
     * compile; the judge's only I/O is the shared canonicalizer over the run's own environment, so it is trusted
     * here like the `egress` mark). Judges the FINAL args of a path-confinable write tool by the file the tool
     * will write — spelling and resolved real target; a hit demotes a surviving `allow` to `ask`, an unresolvable
     * target is refused (the tighten below). `null` SYNCHRONOUSLY for a call it does not cover (no suspension
     * point there), a promise for a covered write. Absent ⇒ no table (`[]`) and the decision path is byte-identical.
     */
    writeProtectionCheck?: import("./write-protect.js").WriteProtectionCheck;
    /**
     * The shell READ STATION — ENGINE-BUILT by the hands phase (`createShellReadStation`,
     * `src/tools/fs/shell-read-check.ts`) for the shell-running tools of this leg, never a deployment callback.
     * Judges the FINAL args of a shell call: ONE parse of the command into its read candidates, each
     * established against the execution environment (symlinks resolved, patterns expanded, traversal roots
     * enumerated, within a bound) and judged by the leg's read deny seat and its containment roots. A
     * POLICY-class member of the fold: a deny-listed read is REFUSED (`deniedBy: "read_boundary"`, composing
     * under `deny > ask > allow`), an out-of-root or unestablishable read tightens a surviving allow to a
     * MANDATED ask (or marks an existing ask mandated), a read the station declined to judge tightens to an
     * ordinary ask on the full shell face (under `auto` the classifier answers it, shown the read-deny table) and
     * is refused on the read-only face, and a vouched call is untouched.
     * `null` SYNCHRONOUSLY for a call it does not cover (no suspension point there), a promise for a shell
     * call. Absent ⇒ no station (no shell mounted) and the decision path is byte-identical.
     */
    shellReadCheck?: import("../tools/fs/shell-read-check.js").ShellReadCheck;
    /**
     * design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
     * small-model policy CLASSIFIER before any human/durable resolution:
     *  - verdict `allow` → the ask resolves to allow (`decisionReason:"classifier"`) — no suspend, no onAsk;
     *  - verdict `block` → deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`); `parse_error` too;
     *  - `unavailable` → where `unavailable.approvalSeat` (a person, live or parked) the ask goes on, its text saying why;
     *    else CC's refusal (`cause: "unavailable"`). Never counted by the denial limit (`classifierUnavailableArm`).
     * SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
     * irreversibility safety tightens — auto mode is the deployment's explicit choice to let the
     * classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
     * action classes: outbound writes, irreversible operations, and sensitive-value handling). A
     * deployment that wants human-only
     * resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by
     * failing — only an affirmative `<block>no</block>` allows.
     * EXCLUDED from that scope (the ask flows the ORIGINAL chain untouched, exactly as if this field
     * were absent for that one call): every ask whose ORIGIN is not the plain `policy` family — the
     * closed set and the classifier's per-origin table are in `ask-origin.ts` (an org-constrained ask on
     * either org arm, the reserved question tool, a call MARKED unresolvable, a hook-raised or
     * hook-promoted ask, an explicit matched `ask` rule, and the classifier's own denial-limit fallback).
     * The hook and ask-rule rows are the same pair the persisted-rule lane refuses, so the gate's two
     * ask→allow seams answer provenance questions identically.
     */
    autoMode?: {
        decider: import("./auto-mode.js").AutoModeDecider;
        /**
         * The per-run DENIAL-LIMIT tracker beside the decider (CC 2.1.250 `FO`/`Wie`; same owner,
         * same lifetime). Absent ⇒ the pre-#548 chain byte for byte: every block is a deny, without bound.
         * Present ⇒ a block first counts, then judges; the block that reaches a bound becomes the fallback
         * ask (`requiresRealApproval: true` + `denialLimitFallback`), and a classifier allow or a person's
         * allow of that ask resets the consecutive count. The Runner builds it from
         * `RunnerDeps.autoMode.denialLimit`; a host driving the gate directly may hand its own.
         */
        denialTracking?: AutoModeDenialTracker;
        unavailable?: import("./classifier-unavailable-arm.js").ClassifierUnavailableSeat;
    };
    /**
     * The leg's EFFECTIVE permission mode, read at every arbitration (a steer changes the next call's answer). The
     * gate's one mode read point sits after the classifier and the sandbox leg and before the park / resolution:
     * `bypassPermissions` releases every non-immune class (`GateDisposition.released`), `dontAsk` refuses every ask
     * but a hook's own (`deniedBy: "mode"`), the other modes hand the ask on. ABSENT ⇒ no
     * arbitration (a host driving the gate directly keeps the pre-mode chain); the classifier station then reads
     * `autoMode` presence alone as the arming fact.
     */
    permissionMode?: () => import("./ask-origin.js").PermissionMode;
    /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
     *  absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
    toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
    /** The called tool's seat carries its server's declaration that a call must be put to a person
     *  ({@link import("../engine/loop/types.js").AgentTool.mcpRequiresUserInteraction}): a surviving `allow` is tightened to an `ask`, and every ask on the call carries
     *  the class `requires_user_interaction` and the real-approval bit (no rule, grant, classifier, bypass mode, blanket seat or policy-settled approver answers it). Absent ⇒ not declared. */
    requiresUserInteraction?: true;
    /** The deployment's A2A peer-isolation switch: a set switch makes the A2A egress default an operator mandate. */
    a2aIsolatePeers?: boolean;
    /** Observe a mode RELEASE (fired before the released allow leaves the arbitration point; `readUnestablished`: the read station could not establish what the call reads). Observe-only. */
    onModeReleased?: (info: {
        toolName: string;
        toolCallId: string;
        release: import("./gate-outcome.js").ModeRelease;
        readUnestablished?: true;
    }) => void;
    /** An ancestor station's arbitration of this call (the inherited lane arbitrates inside the fold and records
     *  here); the exit reads it onto the disposition when the gate's own point did not arbitrate. */
    ancestorArbitrationOf?: (toolCallId: string) => import("./ask-origin.js").Arbitration | undefined;
    /**
     * The HEADLESS arm of the denial-limit fallback: the fallback ask was resolved by a deny no
     * person made (no approver wired; a blanket `"allow"` refused as no approver; an approver that
     * reported unavailable with no park to take it). The fallback had nowhere to go, so the caller — which
     * owns the run — stops it (`TaskResult.errorCode = "classifier.denial_limit"`, CC's "too many
     * classifier denials in headless mode" abort). The deny itself still stands; this gate only names the
     * fact. A TOP-LEVEL seat, deliberately NOT under `autoMode`: the fallback can arrive from an ANCESTOR's
     * frozen tracker through the fold (a delegated child with no armed classifier of its own), and that
     * child's run is the one that has to stop. Observe-only for the gate: a throwing seat never alters
     * the deny.
     */
    onHeadlessDenialLimit?: (info: {
        toolName: string;
        toolCallId: string;
        fallback: import("./auto-mode.js").DenialLimitFallback;
    }) => void;
    /**
     * The deny exit nobody judged: the call's approval could not be RESOLVED (`cause` — the approver
     * answered `unavailable`, no approver is wired, or an inherited durable mandate has no park facility on
     * this leg) and no durable park took the call, so the fail-closed deny STANDS. Fired once per such call
     * from the gate's single deny exit, after every park re-route has had its attempt, with the facts the
     * `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
     * `parkLaneExisted` = a park lane was armed for this run; `remedy` = the caller-side change that would have let the
     * call run, present for the causes that say nobody could be asked (`approver_unavailable`, `no_approver_wired`).
     * Observe-only: a throwing seat never alters the deny. Absent ⇒ the deny stands silently (a host driving the gate
     * directly, no notice sink).
     */
    onAskUnresolvable?: (info: {
        toolName: string;
        toolCallId: string;
        cause: import("./ask-unresolvable-notice.js").AskUnresolvableCause;
        parkLaneExisted: boolean;
        remedy?: string;
    }) => void;
    /**
     * design/153 §2/§7.4 (件4 复审, MED): true means this call's ask is MARKED — an inherited ancestor
     * constraint already determined "no synchronous layer may resolve this ask" (the ancestor's frozen
     * approver was unreachable, OR an ancestor durable mandate was floated back because THIS task's own
     * park facility can honor it). The marker's whole contract is that the ask reaches EITHER the durable
     * park leg OR the fail-closed `resolveAskBound` intercept, with no synchronous decision-maker in
     * between — the auto-mode classifier is exactly such a decision-maker (`decide()` can `allow`, executing
     * the call) and sat strictly BEFORE both marker consumers, so a marked call under an armed classifier
     * bypassed the marker's contract entirely. When true, the classifier step is skipped (falls through to
     * `suspendAsk`/`resolveAsk` untouched) so a marked ask can only ever be resolved by an operator (durable
     * park) or fail closed — never by the classifier.
     */
    isMarkedUnresolvable?: (toolCallId: string) => boolean;
    /**
     * design/179 — the persisted-rule lane. Present only when a deployment wired a rule store; absent it,
     * this whole step does not exist and the gate's decision path is byte-identical to what it was.
     *
     * The lane answers with the BEHAVIOR that spoke ({@link PersistedRuleHit}), and the gate consumes the
     * three behaviors at two different places — the upstream arbiter's order, deny > ask > allow, across
     * the person's own standing rules:
     *  · a DENY row refuses the call outright, on EVERY decision (an allow from the policy fold included):
     *    it is the person's own "never run this" (their settings' deny list, imported), the personal-store
     *    sibling of an org deny, and it runs right after the org layer — nothing downstream re-opens it;
     *  · an ASK row is the person's standing "ask me each time": it turns a surviving allow into an ask
     *    and stamps `matchedAskRule` on it (the same marker an explicit `ask` permission rule stamps), so
     *    the ask's origin is `ask_rule` — the auto-mode classifier never answers it and a persisted ALLOW
     *    never clears it; on a call that is already an ask it stamps the marker and leaves the rest;
     *  · an ALLOW row is the standing form of ONE recorded human approval, so it resolves the asks a real
     *    person saying "allow" would have resolved — and nothing else. It sits post-fold, at the entry of
     *    the `ask` branch, BEFORE the classifier and AFTER every tighten: earlier and a tighten would
     *    simply put the ask back; later and the classifier would already have spoken.
     *
     * The deny/ask arms tighten and take no conjunct but the two structural ones every lane shares (the
     * engine's own content-ask tool is exempt; the lane must be wired). The ALLOW arm's consumption
     * predicate is its own, not the classifier's blanket flip:
     *  · `requiresRealApproval` asks are never consumed — the two integrity gates are the floor, and the
     *    same flag is what a v2 organization-level ask will carry;
     *  · a PreToolUse hook's ask is never consumed — that is the deployment's own adjudication, and a rule
     *    minted for a command does not overrule it;
     *  · the reserved question tool and a call MARKED unresolvable are excluded for the same reason the
     *    classifier excludes them: both contracts require that no synchronous decision-maker stands between
     *    the ask and the park / content route, and this lane is one.
     *  · #144 (ruled): a MANDATED ask is never consumed — an operator's shellGate:"always" tier and a
     *    tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
     *    ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
     *    but cannot clear, the surviving ask discloses it (`allowRuleShadowed`, member `persisted`).
     *  · #502: an ask the call's own reversibility probe declared STRUCTURAL (`probeMandated`) joins that
     *    family. It is the one PER-CALL member of it, and the reason there has to be one: the built-in
     *    shell probe raises it for a read outside the session's allowed directories, which is a boundary
     *    the deployment declared but which arrives wearing the shape tier's own "maybe" —
     *    the very tier the bullet below keeps as this lane's home turf. Without a per-call member, a
     *    compound was cleared segment-wise by the conjunction arm and an out-of-root read plus a sender
     *    ran on two ordinary standing rules with no question asked.
     *  · #213: an ask carrying `matchedAskRule` (an explicit `ask` permission rule matched this call)
     *    is never consumed — a person's standing "ask me each time" outranks the memory of one past
     *    yes, across lanes exactly as the DSL's own deny > ask > allow order says within one. Same
     *    disclosure as the mandated family when a rule matches anyway.
     * What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
     * don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
     * also skips the park it would have minted; that IS what a standing approval means, and the
     * bullets above are what keep integrity/hook/mandated asks out of that set.
     */
    persistedRules?: {
        /** The lane's verdict for this call — a {@link PersistedRuleHit} naming the behavior that spoke and
         *  the rule set it stands on, a coverage-carrying clean negative, an unreadable report, or
         *  `undefined` (the store was READ and no rule speaks). Must not throw — and a store that cannot be
         *  read answers `{ unreadable: true }`, not `undefined`: the DECISION is the same (its rows take no part, as
         *  for a thrown or timed-out lane) but the evidence records `"unavailable"`, not `"no_match"`; the lane owns
         *  announcing the skip. A call unreadable against the rows answers `{ unreadable: true, reason }` (fails closed).
         *
         *  For an ALLOW the set is the COVERAGE SET — each member's canonical text plus its add DOTS. A
         *  single rule admitting the whole command is a one-element set; a compound admitted by the
         *  per-segment conjunction (design/375 §5.1) is the set of the covering rules. The dots are a rule's
         *  durable identity, and the text is not one (it reaches a card sanitized and capped); a lane that
         *  can name them lets the shadowed-ask evidence say WHICH stored rules matched rather than merely
         *  what they looked like. The retired shapes — a bare rule TEXT, the pre-375 `{ rule, dots? }` object,
         *  a `{ rules }` hit without its behavior — are read as a clean negative by the normalizer: a stale
         *  lane degrades toward asking, never toward an allow. Consulted on EVERY decision (a deny/ask row
         *  must be able to tighten an allow), so the lane pays one store read per call on the tools it
         *  speaks for; the runner's lane hands the org layer's read to it, so a governed deployment still
         *  pays one. */
        adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
        /** Observation sink for an ask a rule coverage resolved — the allow-side disclosure. `rules` is
         *  the coverage set's canonical texts in reporting order (design/375: a conjunction names every
         *  rule the decision stands on). Never affects the outcome. */
        onResolved?: (info: {
            toolName: string;
            toolCallId: string;
            rules: readonly string[];
        }) => void;
    };
    /**
     * Observation sink for a shell call the READ-ONLY reader cleared (`readOnlyShellVerdict`, the
     * allow layer's second member beside the persisted allow rule: consulted for the shell tool only, after
     * the person's allow rules, under the SAME consumption predicate — never over a mandated ask, a hook's
     * ask, an explicit ask rule, governance or a marked call). The allow-side disclosure of "why did this run
     * without asking me?" for that arm; `command` is the FINAL command the gate judged (a policy rewrite
     * included). Never affects the outcome. Absent ⇒ the arm still runs, silently.
     */
    onReadOnlyAllowed?: (info: {
        toolName: string;
        toolCallId: string;
        command: string;
    }) => void;
    /**
     * design/182 §7 — the ORG layer. Present ONLY when the deployment DECLARED org governance (the
     * overlay constructor is the boot gate: a governed declaration with no snapshot provider refuses to
     * boot). Absent ⇒ the whole step does not exist and the decision path is byte-identical.
     *
     * The org layer is the outermost TIGHTENING authority, so it runs after every fold and every engine
     * tighten and before the ask-resolution chain — "org deny > org ask (non-dismissable) > personal
     * allow rule > bare ask". It never loosens: there is structurally no org allow bucket.
     *
     * The `unavailable` arm is the load-bearing one. An org-governed deployment that cannot reach a
     * snapshot fails the WHOLE decision boundary closed — every terminal allow (a deployment-policy
     * allow and the no-policy default allow included) becomes an ask carrying
     * `decisionReason: "org_unavailable"` AND `requiresRealApproval: true`, and both ask→allow seams (the
     * personal-rule lane and the auto-mode classifier) are disarmed. Tightening only the two seams was
     * refuted during design: a call that is ALREADY allow never passes through either seam, so it would
     * have sailed past the org deny that could not be read. The `requiresRealApproval` bit closes the
     * third loosening path — a blanket `onAsk: "allow"` would otherwise clear the synthetic ask.
     */
    orgRules?: {
        /** Adjudicate this call against the current org overlay. MUST NOT throw — a throw is read as
         *  `unavailable` (fail-closed), never as "no org rule spoke". */
        adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
        /** True iff the reserved question-tool NAME on this leg resolves to the ENGINE's own content-ask
         *  tool. It is the exemption's real predicate: the name alone is not, because a caller may mount
         *  its own tool under that name when the first-party one is not mounted, and such a tool has
         *  arbitrary side effects (confirmed MED). Absent ⇒ `false` ⇒ the call is governed
         *  like any other, which is the fail-closed default. */
        contentAskToolMounted?: boolean;
        /** Disclosure sink for the unavailable arm (trace `permission.org_snapshot_unavailable`). Never
         *  affects the outcome. */
        onUnavailable?: (info: {
            toolName: string;
            toolCallId: string;
            message: string;
        }) => void;
    };
    /**
     * F-012 L2 — the SANDBOX ADMISSION leg. Present ONLY when the runner armed the mode: the execution
     * env DECLARES isolation (`RemoteExecutionEnv.capabilities.isolation === true` — a Runner-resolved
     * fact, never caller self-report) AND the park prerequisites hold (durable park configured + a
     * checkpoint store that explicitly declares `redecision.reopen`). Absent ⇒ the whole step does not
     * exist and the decision path is byte-identical to a build without it.
     *
     * Semantics (the ruling): inside a declared sandbox, an engine-authored "confirm a potentially
     * destructive local action" ask is answered by containment itself — auto-allow + durable
     * disclosure (`permission.sandbox_admitted`), never a headless deny that pollutes the model.
     * STRICT dominance governs: the leg admits only when EVERY surviving ask on the call is classified
     * `sandbox_local` in the engine's private envelope (deployment-policy asks are `unknown`, hook
     * asks `hook`, marked calls `mandate`, `requiresRealApproval` asks external — none admit), and
     * the call crosses NO declared boundary (`boundaryCapable`: egress-marked tools and
     * protocol-mounted (MCP/A2A) tools — a server's self-report is always boundary-capable). A
     * boundary-crossing ask under the armed mode takes the ordinary durable-park leg below — "park to
     * a human", never an auto-deny.
     */
    sandboxAdmission?: {
        /** The ask classes recorded for this call's fold-layer asks (with their source layers), or
         *  `undefined` when no fold layer asked — the gate's own safety tighten is then the only asker. */
        askClassesOf: (toolCallId: string) => ReadonlyArray<{
            cls: AskClass;
            layer: string;
        }> | undefined;
        /** This call's tool crosses a DECLARED boundary (egress axis / protocol-mounted) — never admitted. */
        boundaryCapable: boolean;
        /** The durable disclosure sink — fired on every admission BEFORE the allow is returned. */
        onAdmitted: (info: {
            toolCallId: string;
            toolName: string;
            askClasses: ReadonlyArray<{
                cls: AskClass;
                layer: string;
            }>;
            boundary: "sandbox_internal";
        }) => void;
    };
}
/**
 * Fold a {@link Hooks.preToolUse} callback into a {@link ToolPolicy} layer, so a screening face a task
 * installed for ITSELF can also travel down a delegation chain as one more inherited constraint.
 *
 * Why a policy and not a second hook channel: a delegated child already folds each ancestor's caller
 * policy (`InheritedGate.parentConstraints`), and that fold is where `deny > ask > allow`, the rewrite
 * cascade, the ancestor-frozen approver and the durable mandate all already live. Expressing the
 * screening face in the same shape means the child gains no new decision channel — the existing fold
 * consumes it — and the tighten-only property is structural rather than restated.
 *
 * The mapping mirrors the gate's own phase 1 (see {@link runToolGate}) decision for decision:
 *  - no opinion (`undefined`) → `allow`, contributing nothing;
 *  - `deny` → `deny` in the fold's own words (the fold short-circuits, and the gate's deny exit supplies
 *    the same `formatHookFeedback` envelope phase 1 would have);
 *  - `ask` → `ask`, stamped `decisionReason:"hook"` so every consumer that distinguishes a screening ask
 *    from a policy ask reads the same fact one level down — notably the persisted-rule lane, which must
 *    not let a standing allow-rule resolve a screening ask;
 *  - `allow`/`ask` with `updatedInput` → the same rewrite, which the fold cascades to later layers and
 *    re-checks against the narrowing layers;
 *  - a THROW → the fail-closed deny with the RB-442 attribution text (naming the hook and the
 *    non-execution), because a screening face that cannot answer must not pass the call.
 *
 * **The callback inherits the fold's idempotency requirement** ({@link combinePolicies}): a layer's
 * `check` may run more than once for a single tool call — the fold scans past an `ask` looking for a
 * later `deny`, a rewrite emitted by a LATER layer re-runs the prefix that never saw the final args, and
 * an approved operator edit is re-adjudicated. A callback whose consultation has a side effect (an audit
 * row, a counter) will therefore see that call more than once on those paths. Its own gate's phase 1 has
 * always had the second of these (an approved edit is re-screened there too).
 *
 * Two properties are deliberately NOT carried, and neither is reconstructible in this shape:
 *  - `additionalContext` — a policy verdict has no channel for model-facing text. Context a screening
 *    face injects is a property of the run whose gate ran it; a descendant's transcript does not receive
 *    it. (The decision half — the part that restricts — is what travels.)
 *  - the phase-1 observer exclusion — a deny raised here is a fold deny, so the descendant's
 *    `permissionDenied` observer sees it with `source:"policy"`. That exclusion is about a hook deciding
 *    in ITS OWN gate; here the descendant's policy fold is what refused the call.
 *
 * `env` is the capability face of the environment the callback was installed against — captured at the
 * install site, not rebuilt in the descendant, for the same reason an inherited policy closure keeps its
 * own captured environment (the descendant may run against a different root). `onCrash` is the install
 * site's deployment error lane: the gate's phase 1 routes a crashing callback to `onError(phase:"hook")`
 * so a broken screening face is distinguishable from a deliberate refusal, and the folded form has to
 * reach the same lane from inside a descendant's fold.
 */
export declare function createPreToolUseConstraintPolicy(preToolUse: NonNullable<Hooks["preToolUse"]>, env?: HookEnvCapabilities, onCrash?: (err: unknown) => void, 
/** The install site's {@link Hooks.timeoutMs}. The folded form was ALREADY bounded by the descendant's
 *  abort race — that asymmetry (bounded as an inherited constraint, unbounded in its own gate) is what
 *  named the hole in the first place — but an abort bound is not a deadline: on a descendant with no
 *  walltime and no cancel, the race never fires. So the same seat bound rides here too, and the two
 *  installations of one callback are now bounded the same way for the same reason. */
timeoutMs?: number): ToolPolicy;
/**
 * The park closure's ONE structural seat — what the gate hands the durable park beyond the twelve
 * positional seats. Every member is optional and read by name, so a new thing the park must know
 * becomes a member here, never a fourteenth positional argument (each of the last four seats was
 * appended "at the tail so nothing re-binds", which is the growth this seat ends).
 */
export interface AskCarry {
    /** design/384 slice 2 — {@link ToolGateInput.callSignal}, verbatim: the park closure composes it
     *  with the run signal into the halt×park fence's cut signal. Absent ⇒ the run-level binding alone. */
    readonly signal?: AbortSignal;
    /** The surviving ask's `denialLimitFallback` (the classifier denial-limit counts) run through the
     *  window-less route station ({@link import("./auto-mode.js").unarmedWindow}) when the gate builds the
     *  carry: a park is not a hand-out to a live approver, so nothing on this lane counts down. The
     *  decision's member cannot state a window (its type has no such member), and this is the one place
     *  the `0` a park row shows is written. Persisted on the row so a durable card shows the counts the
     *  synchronous card shows. Absent then means: not a fallback ask. */
    readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
    /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
     *  on the row so a durable card is rendered by the same word the synchronous card is. */
    readonly origin?: import("./ask-origin.js").AskOrigin;
    /** The surviving ask's engine-stamped class ({@link import("./tool-policy.js").AskRequest.askClass}), persisted on the
     *  row so a durable approval seat answers by the class the synchronous one reads. Present exactly when
     *  {@link classes} is. */
    readonly askClass?: import("./ask-origin.js").AskClass;
    /** The whole class set beside it ({@link import("./tool-policy.js").AskRequest.classes}), in `ASK_CLASSES` order. */
    readonly classes?: readonly import("./ask-origin.js").AskClass[];
    /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) — the park site's
     *  rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
    readonly mandated?: true;
    /** The table word stamped beside it ({@link import("./tool-policy.js").AskRequest.mandate}), for the row's twin. */
    readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
    /** The deployment's authorship ({@link import("./tool-policy.js").AskRequest.authoredBy}), for the row's twin. */
    readonly authoredBy?: "deployment";
}
/** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
 *  the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
 *  number), the origin word only when it is a member of the closed set, and the class pair only when it is
 *  well-formed, as a fresh copy (the carry is typed, but the row is the boundary a host reads back through —
 *  the same screen the summary applies to the word). */
export declare function askCarryRowMembers(carry: AskCarry | undefined): {
    denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
    origin?: import("./ask-origin.js").AskOrigin;
    askClass?: import("./ask-origin.js").AskClass;
    classes?: readonly import("./ask-origin.js").AskClass[];
};
/**
 * The PreToolUse screening seat ({@link PreToolUseScreening}) for ONE call — seat runner, seat bound, per-screening
 * context and the fail-closed / deny sentences, built in ONE place for every site that screens a call: the gate's
 * phase 1 and approved-edit re-screen (through {@link runToolGate}'s pass) and the resumed leg's re-screen of an
 * approved call. Undefined when no face is wired. `hookSeatMs` is the caller's validated bound.
 */
export declare function createPreToolUseScreening(preToolUse: Hooks["preToolUse"] | undefined, input: Pick<ToolGateInput, "event" | "trackedCwd" | "hookEnv" | "identity" | "onHookError" | "abortSignal">, hookSeatMs: number, notifier: SafeNotifier): PreToolUseScreening | undefined;
/**
 * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
 * structural: a hook's `allow` passes no deny, no ask rule and no mandated question; it is one member of the
 * allow layer and clears only the questions an allow rule may clear (a deployment that wants a question put to a
 * person whatever a hook says marks it `authoredBy: "deployment"` or `requiresRealApproval`):
 *
 *  1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput`
 *     and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook
 *     `ask` is remembered (it does not short-circuit — a later policy `deny` outranks it).
 *  2. **adjudicate** — run the tool policy on the FINAL `currentInput` (never the model's stale args),
 *     fold it with any remembered hook-ask via `deny > ask > allow`, and resolve a surviving `ask`
 *     through `onAsk`. The policy ALWAYS runs regardless of the hook's verdict. A policy `allow` may
 *     itself carry an `updatedInput` rewrite (redact/clamp), applied last over any hook rewrite.
 *
 * Because both phases run in this one linear function, the ordering — and the "policy is final"
 * invariant — is enforced by the call stack, not by registration convention. Returns the gate result
 * for the harness `tool_call` hook plus the PreToolUse context to attach to the tool result.
 */
export declare function runToolGate(input: ToolGateInput): Promise<ToolGateResult>;