/**
 * The advertised-writable-directory seat — ONE rule for every directory the prompt tells the model
 * it may write with the file tools:
 *
 *   a directory the prompt advertises as writable is a directory the root fence ADMITS; the same
 *   admission decides the hole and the teaching, so a directory the fence cannot admit is neither
 *   opened nor taught, and the skip is announced.
 *
 * Two advertisers share it: the deployment's scratchpad (`TaskSpec.envFacts.scratchpadDir`, spoken
 * by the `# Environment` block) and the memory engine's write root (spoken by the `# Memory`
 * instruction, "write to it directly with the Write tool"). Before this seat each advertiser judged
 * on its own: the scratchpad opened its hole in the hands phase, the memory instruction checked only
 * that a `Write` tool was mounted — and the fence refused the very path the instruction named
 * (`path_not_in_root`), a contradictory gate that taught the model to route around the tools.
 *
 * Admission = fail-closed canonicalization through the SAME ExecutionEnv the fence judges with (a
 * raw or dangling entry is never admitted — the ancestor-canonicalize fallback would let a symlinked
 * prefix carry writes outside every root). An advertiser that OWNS its directory's creation may ask
 * for materialization on a DEFINITIVE `not_found` (the exclusive-create marker, so a concurrently
 * planted path is never written through); an advertiser whose directory is created elsewhere passes
 * no marker and an absent directory narrows.
 *
 * A directory the fence already holds is never admitted as a HOLE in it: a candidate that resolves onto
 * a root — or above one — would turn that whole root into the advertised, write-gate-exempt domain, so
 * an ordinary source write inside the workspace would stop asking. One predicate
 * ({@link swallowedFenceRoot}) states that, and the write gate's configured domains apply the same one.
 *
 * Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
 * caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so no
 * directory reaches the ROOTS FENCE through this seat without the prompt advertising it, and none
 * the prompt advertises is refused by that fence.
 *
 * The invariant is about this seat, not about the whole write surface: under an open write face
 * ({@link import("../../tools/fs/containment-face.js").ContainmentFace}) the roots fence judges
 * nothing, so what the prompt advertises is a floor rather than the whole list — the `# Environment`
 * block says so in its own line, and the advertisement stays true (these directories ARE writable).
 */
import type { ExecutionEnv } from "../../internal/harness.js";
export interface AdvertisedWritableDir {
    /** The directory as the advertisement spells it. */
    path: string;
    /** Who advertises it — names the skip line and the trace. */
    advertiser: "scratchpad" | "memory";
    /**
     * How the directory comes to exist when the env reports it absent. The advertiser that owns the
     * directory's creation supplies the marker file name for the atomic exclusive-create
     * materialization (parent dirs created like writeFile; a marker that already exists — planted
     * concurrently, symlink or not — is never written through; an env without an exclusive primitive
     * narrows instead of emulating one with an overwriting write). An advertiser whose directory is
     * created elsewhere (the memory engine materializes its own root) passes nothing — absent narrows.
     */
    materializeMarker?: string;
    /**
     * The canonical spelling the advertiser's OWN gates key on, when it has one (the memory engine keys its
     * write gate and its harvest boundary on `MemorySessionHandle.writableRoot`). A fence that resolves the
     * directory to a different canonical path would admit writes those gates never see — such a mismatch is
     * a skip, announced with both spellings, never an admission.
     */
    expectCanonical?: string;
}
/** The skip announcement — the one loud outlet for a directory that was advertised but could not be admitted. */
export type AdvertisedWritableDirSkipped = (dir: AdvertisedWritableDir, reason: string) => void;
/**
 * ONE rule for both halves of the auto-approved write surface: a directory offered as a WRITABLE DOMAIN
 * (an advertised writable directory here; a configured `exemptDirs`/`acceptDirs` entry at the write
 * gate) must not SWALLOW a root the fence already holds. Returns the first root the candidate equals or
 * is an ancestor of — the one to name in the refusal — else `undefined`.
 *
 * Why a domain that contains a root is not a domain: the point of the domain is to exempt a small area
 * the prompt (or the deployment) named. A candidate resolving onto the task root — a symlinked
 * scratchpad or memory home is all it takes, and the resolution is what makes it invisible to the
 * spelling — exempts the ENTIRE workspace instead, turning every ordinary source write from `ask` into
 * `allow`. Refusing the admission is the fix; a special case inside the gate ("ignore this domain when
 * it contains the root") would be the same rule written twice, in the place where it is easiest to
 * forget. Both arguments must already be CANONICAL — this compares spellings, it does not resolve them.
 */
export declare function swallowedFenceRoot(domainCanonical: string, fenceRootsCanonical: readonly string[]): string | undefined;
export declare class AdvertisedWritableDirs {
    private readonly env;
    private readonly onSkipped;
    /** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
     *  spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
     *  admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
     *  entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
     *  answer and skips it too, announcing it there), so there is nothing here for it to guard. */
    private readonly declaredRootSpellings;
    private readonly admitted;
    private readonly verdicts;
    /** The CANONICAL roots the fence holds: the task root at construction, plus every root the spec
     *  DECLARES, resolved by this seat before it judges anything (see {@link resolveDeclaredRoots}) —
     *  so which advertiser runs first cannot decide what the rule sees. */
    private readonly fenceRoots;
    /** Latched after the declared spellings have been resolved once (the resolution is per seat, not per call). */
    private declaredRootsResolved;
    /** ONE answer per declared spelling, for this seat AND for the hands phase that folds the same entries
     *  into the allowlist ({@link canonicalizeDeclaredRoot}): two independent lookups of one path can
     *  DISAGREE, and a root outside this rule's list while inside the fence is exactly the gap. */
    private readonly declaredRootAnswers;
    constructor(env: ExecutionEnv, onSkipped: AdvertisedWritableDirSkipped, 
    /** The run's task root, CANONICAL (the driver's one resolution — see `task-root-canonical.ts`). */
    taskRootCanonical: string, 
    /** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
     *  spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
     *  admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
     *  entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
     *  answer and skips it too, announcing it there), so there is nothing here for it to guard. */
    declaredRootSpellings?: readonly string[]);
    /** Declare one more CANONICAL root the fence holds (an `additionalDirectories` /
     *  `additionalReadDirectories` entry, once the hands phase has resolved it). Declaring is
     *  idempotent and order-free; a root declared after an advertiser was judged does not re-open it. */
    declareFenceRoot(canonical: string): void;
    /**
     * The canonical form of a DECLARED fence-root spelling — resolved at most once per spelling and
     * served from the memo thereafter. The hands phase calls this instead of canonicalizing the same
     * entries again, so the list this rule judges against and the allowlist the fence ends up holding
     * are built from ONE answer per path: a lookup that fails for this seat fails for that phase too,
     * and a root cannot be outside the rule's view while being inside the fence.
     */
    canonicalizeDeclaredRoot(spelled: string): Promise<Awaited<ReturnType<ExecutionEnv["canonicalPath"]>>>;
    /** Resolve the spec-declared root spellings into {@link fenceRoots}, exactly once per seat. */
    private resolveDeclaredRoots;
    /** The admitted roots, CANONICAL, in admission order, deduplicated. The hands phase folds this list into the
     *  write-capable containment allowlist before the toolkit compiles; it is not a live view after that. */
    get roots(): readonly string[];
    /** What became of an advertiser's directory: admitted, skipped (announced), or never asked (the advertiser
     *  had nothing to advertise, or decided not to). The prompt side reads this to withhold an advertisement. */
    verdictOf(advertiser: AdvertisedWritableDir["advertiser"]): "admitted" | "skipped" | "not-asked";
    /**
     * Admit one advertised directory. TRUE ⇔ the fence will admit writes under it (its canonical form is on
     * {@link roots}); FALSE ⇔ it was skipped (announced through the seat's outlet) and the advertiser must not
     * advertise it. The env's answers are Result-shaped, so the judgment itself never throws; the skip outlet is
     * the caller's own sink (`deps.onError` in the driver) and is deliberately not guarded here — every phase
     * lets that sink's own exceptions propagate, and a seat that swallowed them would hide the deployment's bug.
     */
    admit(dir: AdvertisedWritableDir): Promise<boolean>;
    private judge;
}
