import { type Action } from "../../../internals/plan.js"; import type { BindingLock, BindingOwnershipEntry, BindingWrite } from "../../lock.js"; /** * The D18 managed-write engine for the Claude project-scope host adapter — the * foundation every binding write flows through. Plan/apply are SEPARATE (D14): * this engine only PLANS. Callers hand its {@link ClaudeManagedPlan.actions} to * the shared `executePlan`, then seal ownership with {@link finalizeClaudeOwnership}. * * D18 ownership model (verbatim): shared JSON files (`.claude/settings.json`, * `.claude/settings.local.json`, `.mcp.json`) are owned by FIELD, never by file; * the CLAUDE.md managed block is owned by a text marker; rules/skills/agents are * owned as FILES with source digests. Every owned slot records the pre-existing * value (or its explicit absence) captured at plan time, so conservative removal * can restore the world. Shared JSON files are ALWAYS merge-written (targeted keys * only) — a whole-file replacement never happens. */ /** The three D18 ownership mechanisms, mirroring the lock's `OWNERSHIP_KINDS`. */ export type ClaudeOwnershipKind = "json-pointer" | "mcp-server" | "file"; /** D18: the pre-existing value AIH observed, or an explicit record of its absence. */ export type ClaudePreExisting = { absent: true; } | { value: unknown; }; /** * A planned ownership record — everything the lock needs EXCEPT the post-apply * digest, which {@link finalizeClaudeOwnership} seals by re-reading after apply. * Carries the structured routing (file + pointer / block marker) the finalizer * uses; the final {@link BindingOwnershipEntry.target} string re-encodes that * routing so conservative removal can re-locate the slot from the lock alone. */ export interface ClaudeOwnershipIntent { kind: ClaudeOwnershipKind; /** Final lock target: file-qualified pointer, MCP server id, file path, or `CLAUDE.md#block:`. */ target: string; /** Repo-relative POSIX file whose content this ownership is read back from. */ file: string; /** JSON pointer segments (json-pointer / mcp-server only). */ pointer?: string[]; /** Block marker id (CLAUDE.md fence ownership only; kind is `file`). */ blockMarker?: string; preExisting: ClaudePreExisting; /** JSON value (pointer/server) or a source digest (file/block). */ applied: unknown; } /** * The plan bundle: actions to execute, the per-slot writes, and the ownership * intents. `writes` has one entry per jsonField/mcpServer/block/file call; * `ownership` may be SHORTER when depth-2 leaves under a freshly-created parent * collapse into a single parent-container entry (D18 "own what you created"). */ export interface ClaudeManagedPlan { actions: Action[]; writes: BindingWrite[]; ownership: ClaudeOwnershipIntent[]; } export declare class ClaudeManagedWriteEngine { private readonly root; private readonly slots; constructor(root: string); /** Read + tolerantly parse a repo-relative JSON file's current state, or `undefined`. */ private readJson; /** * Capture, from ONE read of the current file, both the slot's pre-existing value * and — for a depth-2 pointer — whether the PARENT container is absent on disk. * An absent parent means AIH will IMPLICITLY create the container, so build() * records ownership of the whole container ("own what you created") rather than * just the leaf, and removal can take the empty container with it (D18). */ private captureJsonSlot; /** * Own a single FIELD in a shared JSON file, addressed by a JSON pointer (depth 1 * or 2 — every Claude settings/MCP owned field fits). Merge/targeted-key write: * unrelated siblings survive; the owned leaf is REPLACED (never array-unioned) so * re-binds are deterministic. Refuses any file outside the D18 shared surfaces. */ jsonField(file: string, pointer: string, value: unknown): this; /** * Own an MCP server entry (`mcpServers.`) in `.mcp.json` (D18 kind * `mcp-server`). The server id is the ownership target; the write preserves every * other server already present. */ mcpServer(id: string, config: unknown): this; /** * Own the single marker-delimited CLAUDE.md block (D18: text marker). Whole-block * replace on re-bind, all content OUTSIDE the fence preserved verbatim (EOL style * kept). Ownership kind is `file` with the block-target convention * `CLAUDE.md#block:` so removal STRIPS the fence rather than deleting the * file — the region outside the fence is never AIH's to delete. */ claudeMdBlock(body: string, opts?: { note?: string; preamble?: string; }): this; /** * Own a whole FILE under `.claude/rules/`, `.claude/skills/`, or `.claude/agents/` * (D18: file-level ownership with a source digest). Refuses any path outside those * roots, and a non-POSIX / traversing / drive path at the schema boundary. */ ownedFile(relPath: string, contents: string): this; /** Produce the plan bundle: grouped actions + per-slot writes + ownership intents. */ build(): ClaudeManagedPlan; } /** Set `value` at `segments` in `obj`, creating intermediate plain objects. */ export declare function deepSet(obj: Record, segments: string[], value: unknown): void; /** * Seal ownership after `executePlan` has run the actions: re-read each owned slot * and compute its post-apply digest, producing lock-ready * {@link BindingOwnershipEntry} records (D18's post-apply digest). The engine does * NOT write the lock — the caller does. */ export declare function finalizeClaudeOwnership(root: string, intents: readonly ClaudeOwnershipIntent[]): BindingOwnershipEntry[]; /** * Re-provision pre-existing preservation for a SINGLE-slot managed plan — * shared by every caller that re-binds one `jsonField()` owner across a * re-provision (the Claude host's own `enabledPlugins` field in * `hosts/claude/plugins.ts`'s `bindPlugin`, and the Superpowers adapter's * telemetry env field). Previously duplicated verbatim in both call sites * (`plugins.ts`'s private `preservePriorPreExisting` and * `frameworks/superpowers.ts`'s private `preserveAcrossRebind`); this is the * one shared implementation both now call. * * `managedPlan` must have been built from EXACTLY the one `jsonField()` call * being re-provisioned — `ownership[0]` is the intent mutated in place. * `target` is that field's LEAF target string (`#`, e.g. * `.claude/settings.json#/enabledPlugins/ecc@ecc-mkt`), matching the * convention the original `jsonField()` call used, REGARDLESS of whether * `build()` already collapsed it to parent-container ownership this time. * * Two shapes the prior lock may have recorded for that leaf: * - the prior lock owns the LEAF (the container pre-existed at first bind): * carry its `preExisting` onto the new intent verbatim; * - the prior lock owns the PARENT container (`#/` with * `preExisting` absent — AIH created the container at first bind): * RE-ASSERT parent ownership on the re-bind intent. The engine now sees * the container as present (the first bind wrote it) and would otherwise * record a leaf whose "pre-existing" is the FIRST bind's own value, so a * later removal from a re-bind lock would restore the binding instead of * pruning the container AIH created. * * A no-op when there is no prior lock, no ownership intent to mutate, the * intent isn't a depth-2 pointer, or neither shape is found in the prior * lock (nothing to carry forward — the fresh disk-read `preExisting` the * engine already captured stands). */ export declare function carryForwardOwnership(managedPlan: ClaudeManagedPlan, previousLock: BindingLock | undefined, target: string): void;